usvg/text/
layout.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5use std::collections::HashMap;
6use std::num::NonZeroU16;
7use std::sync::Arc;
8
9use fontdb::{Database, ID};
10use kurbo::{ParamCurve, ParamCurveArclen, ParamCurveDeriv};
11use rustybuzz::ttf_parser;
12use rustybuzz::ttf_parser::{GlyphId, Tag};
13use strict_num::NonZeroPositiveF32;
14use tiny_skia_path::{NonZeroRect, Transform};
15use unicode_script::UnicodeScript;
16
17use crate::tree::{BBox, IsValidLength};
18use crate::{
19    AlignmentBaseline, ApproxZeroUlps, BaselineShift, DominantBaseline, Fill, FillRule, Font,
20    FontResolver, LengthAdjust, PaintOrder, Path, ShapeRendering, Stroke, Text, TextAnchor,
21    TextChunk, TextDecorationStyle, TextFlow, TextPath, TextSpan, WritingMode,
22};
23
24/// A glyph that has already been positioned correctly.
25///
26/// Note that the transform already takes the font size into consideration, so applying the
27/// transform to the outline of the glyphs is all that is necessary to display it correctly.
28#[derive(Clone, Debug)]
29pub struct PositionedGlyph {
30    /// Returns the transform of the glyph itself within the cluster. For example,
31    /// for zalgo text, it contains the transform to position the glyphs above/below
32    /// the main glyph.
33    glyph_ts: Transform,
34    /// Returns the transform of the whole cluster that the glyph is part of.
35    cluster_ts: Transform,
36    /// Returns the transform of the span that the glyph is a part of.
37    span_ts: Transform,
38    /// The units per em of the font the glyph belongs to.
39    units_per_em: u16,
40    /// The font size the glyph should be scaled to.
41    font_size: f32,
42    /// The ID of the glyph.
43    pub id: GlyphId,
44    /// The text from the original string that corresponds to that glyph.
45    pub text: String,
46    /// The ID of the font the glyph should be taken from. Can be used with the
47    /// [font database of the tree](crate::Tree::fontdb) this glyph is part of.
48    pub font: ID,
49}
50
51impl PositionedGlyph {
52    /// Returns the transform of glyph, assuming that an outline
53    /// glyph is being used (i.e. from the `glyf` or `CFF/CFF2` table).
54    pub fn outline_transform(&self) -> Transform {
55        let mut ts = Transform::identity();
56
57        // Outlines are mirrored by default.
58        ts = ts.pre_scale(1.0, -1.0);
59
60        let sx = self.font_size / self.units_per_em as f32;
61
62        ts = ts.pre_scale(sx, sx);
63        ts = ts
64            .pre_concat(self.glyph_ts)
65            .post_concat(self.cluster_ts)
66            .post_concat(self.span_ts);
67
68        ts
69    }
70
71    /// Returns the transform for the glyph, assuming that a CBTD-based raster glyph
72    /// is being used.
73    pub fn cbdt_transform(&self, x: f32, y: f32, pixels_per_em: f32, height: f32) -> Transform {
74        self.span_ts
75            .pre_concat(self.cluster_ts)
76            .pre_concat(self.glyph_ts)
77            .pre_concat(Transform::from_scale(
78                self.font_size / pixels_per_em,
79                self.font_size / pixels_per_em,
80            ))
81            // Right now, the top-left corner of the image would be placed in
82            // on the "text cursor", but we want the bottom-left corner to be there,
83            // so we need to shift it up and also apply the x/y offset.
84            .pre_translate(x, -height - y)
85    }
86
87    /// Returns the transform for the glyph, assuming that a sbix-based raster glyph
88    /// is being used.
89    pub fn sbix_transform(
90        &self,
91        x: f32,
92        y: f32,
93        x_min: f32,
94        y_min: f32,
95        pixels_per_em: f32,
96        height: f32,
97    ) -> Transform {
98        // In contrast to CBDT, we also need to look at the outline bbox of the glyph and add a shift if necessary.
99        let bbox_x_shift = self.font_size * (-x_min / self.units_per_em as f32);
100
101        let bbox_y_shift = if y_min.approx_zero_ulps(4) {
102            // For unknown reasons, using Apple Color Emoji will lead to a vertical shift on MacOS, but this shift
103            // doesn't seem to be coming from the font and most likely is somehow hardcoded. On Windows,
104            // this shift will not be applied. However, if this shift is not applied the emojis are a bit
105            // too high up when being together with other text, so we try to imitate this.
106            // See also https://github.com/harfbuzz/harfbuzz/issues/2679#issuecomment-1345595425
107            // So whenever the y-shift is 0, we approximate this vertical shift that seems to be produced by it.
108            // This value seems to be pretty close to what is happening on MacOS.
109            // We can still remove this if it turns out to be a problem, but Apple Color Emoji is pretty
110            // much the only `sbix` font out there and they all seem to have a y-shift of 0, so it
111            // makes sense to keep it.
112            0.128 * self.font_size
113        } else {
114            self.font_size * (-y_min / self.units_per_em as f32)
115        };
116
117        self.span_ts
118            .pre_concat(self.cluster_ts)
119            .pre_concat(self.glyph_ts)
120            .pre_concat(Transform::from_translate(bbox_x_shift, bbox_y_shift))
121            .pre_concat(Transform::from_scale(
122                self.font_size / pixels_per_em,
123                self.font_size / pixels_per_em,
124            ))
125            // Right now, the top-left corner of the image would be placed in
126            // on the "text cursor", but we want the bottom-left corner to be there,
127            // so we need to shift it up and also apply the x/y offset.
128            .pre_translate(x, -height - y)
129    }
130
131    /// Returns the transform for the glyph, assuming that an SVG glyph is
132    /// being used.
133    pub fn svg_transform(&self) -> Transform {
134        let mut ts = Transform::identity();
135
136        let sx = self.font_size / self.units_per_em as f32;
137
138        ts = ts.pre_scale(sx, sx);
139        ts = ts
140            .pre_concat(self.glyph_ts)
141            .post_concat(self.cluster_ts)
142            .post_concat(self.span_ts);
143
144        ts
145    }
146
147    /// Returns the transform for the glyph, assuming that a COLR glyph is
148    /// being used.
149    pub fn colr_transform(&self) -> Transform {
150        self.outline_transform()
151    }
152}
153
154/// A span contains a number of layouted glyphs that share the same fill, stroke, paint order and
155/// visibility.
156#[derive(Clone, Debug)]
157pub struct Span {
158    /// The fill of the span.
159    pub fill: Option<Fill>,
160    /// The stroke of the span.
161    pub stroke: Option<Stroke>,
162    /// The paint order of the span.
163    pub paint_order: PaintOrder,
164    /// The font size of the span.
165    pub font_size: NonZeroPositiveF32,
166    /// The visibility of the span.
167    pub visible: bool,
168    /// The glyphs that make up the span.
169    pub positioned_glyphs: Vec<PositionedGlyph>,
170    /// An underline text decoration of the span.
171    /// Needs to be rendered before all glyphs.
172    pub underline: Option<Path>,
173    /// An overline text decoration of the span.
174    /// Needs to be rendered before all glyphs.
175    pub overline: Option<Path>,
176    /// A line-through text decoration of the span.
177    /// Needs to be rendered after all glyphs.
178    pub line_through: Option<Path>,
179}
180
181#[derive(Clone, Debug)]
182struct GlyphCluster {
183    byte_idx: ByteIndex,
184    codepoint: char,
185    width: f32,
186    advance: f32,
187    ascent: f32,
188    descent: f32,
189    has_relative_shift: bool,
190    glyphs: Vec<PositionedGlyph>,
191    transform: Transform,
192    path_transform: Transform,
193    visible: bool,
194}
195
196impl GlyphCluster {
197    pub(crate) fn height(&self) -> f32 {
198        self.ascent - self.descent
199    }
200
201    pub(crate) fn transform(&self) -> Transform {
202        self.path_transform.post_concat(self.transform)
203    }
204}
205
206pub(crate) fn layout_text(
207    text_node: &Text,
208    resolver: &FontResolver,
209    fontdb: &mut Arc<fontdb::Database>,
210) -> Option<(Vec<Span>, NonZeroRect)> {
211    let mut fonts_cache: FontsCache = HashMap::new();
212
213    for chunk in &text_node.chunks {
214        for span in &chunk.spans {
215            if !fonts_cache.contains_key(&span.font) {
216                if let Some(font) =
217                    (resolver.select_font)(&span.font, fontdb).and_then(|id| fontdb.load_font(id))
218                {
219                    fonts_cache.insert(span.font.clone(), Arc::new(font));
220                }
221            }
222        }
223    }
224
225    let mut spans = vec![];
226    let mut char_offset = 0;
227    let mut last_x = 0.0;
228    let mut last_y = 0.0;
229    let mut bbox = BBox::default();
230    for chunk in &text_node.chunks {
231        let (x, y) = match chunk.text_flow {
232            TextFlow::Linear => (chunk.x.unwrap_or(last_x), chunk.y.unwrap_or(last_y)),
233            TextFlow::Path(_) => (0.0, 0.0),
234        };
235
236        let mut clusters = process_chunk(chunk, &fonts_cache, resolver, fontdb);
237        if clusters.is_empty() {
238            char_offset += chunk.text.chars().count();
239            continue;
240        }
241
242        apply_writing_mode(text_node.writing_mode, &mut clusters);
243        apply_letter_spacing(chunk, &mut clusters);
244        apply_word_spacing(chunk, &mut clusters);
245
246        apply_length_adjust(chunk, &mut clusters);
247        let mut curr_pos = resolve_clusters_positions(
248            text_node,
249            chunk,
250            char_offset,
251            text_node.writing_mode,
252            &fonts_cache,
253            &mut clusters,
254        );
255
256        let mut text_ts = Transform::default();
257        if text_node.writing_mode == WritingMode::TopToBottom {
258            if let TextFlow::Linear = chunk.text_flow {
259                text_ts = text_ts.pre_rotate_at(90.0, x, y);
260            }
261        }
262
263        for span in &chunk.spans {
264            let font = match fonts_cache.get(&span.font) {
265                Some(v) => v,
266                None => continue,
267            };
268
269            let decoration_spans = collect_decoration_spans(span, &clusters);
270
271            let mut span_ts = text_ts;
272            span_ts = span_ts.pre_translate(x, y);
273            if let TextFlow::Linear = chunk.text_flow {
274                let shift = resolve_baseline(span, font, text_node.writing_mode);
275
276                // In case of a horizontal flow, shift transform and not clusters,
277                // because clusters can be rotated and an additional shift will lead
278                // to invalid results.
279                span_ts = span_ts.pre_translate(0.0, shift);
280            }
281
282            let mut underline = None;
283            let mut overline = None;
284            let mut line_through = None;
285
286            if let Some(decoration) = span.decoration.underline.clone() {
287                // TODO: No idea what offset should be used for top-to-bottom layout.
288                // There is
289                // https://www.w3.org/TR/css-text-decor-3/#text-underline-position-property
290                // but it doesn't go into details.
291                let offset = match text_node.writing_mode {
292                    WritingMode::LeftToRight => -font.underline_position(span.font_size.get()),
293                    WritingMode::TopToBottom => font.height(span.font_size.get()) / 2.0,
294                };
295
296                if let Some(path) =
297                    convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts)
298                {
299                    bbox = bbox.expand(path.data.bounds());
300                    underline = Some(path);
301                }
302            }
303
304            if let Some(decoration) = span.decoration.overline.clone() {
305                let offset = match text_node.writing_mode {
306                    WritingMode::LeftToRight => -font.ascent(span.font_size.get()),
307                    WritingMode::TopToBottom => -font.height(span.font_size.get()) / 2.0,
308                };
309
310                if let Some(path) =
311                    convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts)
312                {
313                    bbox = bbox.expand(path.data.bounds());
314                    overline = Some(path);
315                }
316            }
317
318            if let Some(decoration) = span.decoration.line_through.clone() {
319                let offset = match text_node.writing_mode {
320                    WritingMode::LeftToRight => -font.line_through_position(span.font_size.get()),
321                    WritingMode::TopToBottom => 0.0,
322                };
323
324                if let Some(path) =
325                    convert_decoration(offset, span, font, decoration, &decoration_spans, span_ts)
326                {
327                    bbox = bbox.expand(path.data.bounds());
328                    line_through = Some(path);
329                }
330            }
331
332            let mut fill = span.fill.clone();
333            if let Some(ref mut fill) = fill {
334                // The `fill-rule` should be ignored.
335                // https://www.w3.org/TR/SVG2/text.html#TextRenderingOrder
336                //
337                // 'Since the fill-rule property does not apply to SVG text elements,
338                // the specific order of the subpaths within the equivalent path does not matter.'
339                fill.rule = FillRule::NonZero;
340            }
341
342            if let Some((span_fragments, span_bbox)) = convert_span(span, &clusters, span_ts) {
343                bbox = bbox.expand(span_bbox);
344
345                let positioned_glyphs = span_fragments
346                    .into_iter()
347                    .flat_map(|mut gc| {
348                        let cluster_ts = gc.transform();
349                        gc.glyphs.iter_mut().for_each(|pg| {
350                            pg.cluster_ts = cluster_ts;
351                            pg.span_ts = span_ts;
352                        });
353                        gc.glyphs
354                    })
355                    .collect();
356
357                spans.push(Span {
358                    fill,
359                    stroke: span.stroke.clone(),
360                    paint_order: span.paint_order,
361                    font_size: span.font_size,
362                    visible: span.visible,
363                    positioned_glyphs,
364                    underline,
365                    overline,
366                    line_through,
367                });
368            }
369        }
370
371        char_offset += chunk.text.chars().count();
372
373        if text_node.writing_mode == WritingMode::TopToBottom {
374            if let TextFlow::Linear = chunk.text_flow {
375                std::mem::swap(&mut curr_pos.0, &mut curr_pos.1);
376            }
377        }
378
379        last_x = x + curr_pos.0;
380        last_y = y + curr_pos.1;
381    }
382
383    let bbox = bbox.to_non_zero_rect()?;
384
385    Some((spans, bbox))
386}
387
388fn convert_span(
389    span: &TextSpan,
390    clusters: &[GlyphCluster],
391    text_ts: Transform,
392) -> Option<(Vec<GlyphCluster>, NonZeroRect)> {
393    let mut span_clusters = vec![];
394    let mut bboxes_builder = tiny_skia_path::PathBuilder::new();
395
396    for cluster in clusters {
397        if !cluster.visible {
398            continue;
399        }
400
401        if span_contains(span, cluster.byte_idx) {
402            span_clusters.push(cluster.clone());
403        }
404
405        let mut advance = cluster.advance;
406        if advance <= 0.0 {
407            advance = 1.0;
408        }
409
410        // We have to calculate text bbox using font metrics and not glyph shape.
411        if let Some(r) = NonZeroRect::from_xywh(0.0, -cluster.ascent, advance, cluster.height()) {
412            if let Some(r) = r.transform(cluster.transform()) {
413                bboxes_builder.push_rect(r.to_rect());
414            }
415        }
416    }
417
418    let mut bboxes = bboxes_builder.finish()?;
419    bboxes = bboxes.transform(text_ts)?;
420    let bbox = bboxes.compute_tight_bounds()?.to_non_zero_rect()?;
421
422    Some((span_clusters, bbox))
423}
424
425fn collect_decoration_spans(span: &TextSpan, clusters: &[GlyphCluster]) -> Vec<DecorationSpan> {
426    let mut spans = Vec::new();
427
428    let mut started = false;
429    let mut width = 0.0;
430    let mut transform = Transform::default();
431
432    for cluster in clusters {
433        if span_contains(span, cluster.byte_idx) {
434            if started && cluster.has_relative_shift {
435                started = false;
436                spans.push(DecorationSpan { width, transform });
437            }
438
439            if !started {
440                width = cluster.advance;
441                started = true;
442                transform = cluster.transform;
443            } else {
444                width += cluster.advance;
445            }
446        } else if started {
447            spans.push(DecorationSpan { width, transform });
448            started = false;
449        }
450    }
451
452    if started {
453        spans.push(DecorationSpan { width, transform });
454    }
455
456    spans
457}
458
459pub(crate) fn convert_decoration(
460    dy: f32,
461    span: &TextSpan,
462    font: &ResolvedFont,
463    mut decoration: TextDecorationStyle,
464    decoration_spans: &[DecorationSpan],
465    transform: Transform,
466) -> Option<Path> {
467    debug_assert!(!decoration_spans.is_empty());
468
469    let thickness = font.underline_thickness(span.font_size.get());
470
471    let mut builder = tiny_skia_path::PathBuilder::new();
472    for dec_span in decoration_spans {
473        let rect = match NonZeroRect::from_xywh(0.0, -thickness / 2.0, dec_span.width, thickness) {
474            Some(v) => v,
475            None => {
476                log::warn!("a decoration span has a malformed bbox");
477                continue;
478            }
479        };
480
481        let ts = dec_span.transform.pre_translate(0.0, dy);
482
483        let mut path = tiny_skia_path::PathBuilder::from_rect(rect.to_rect());
484        path = match path.transform(ts) {
485            Some(v) => v,
486            None => continue,
487        };
488
489        builder.push_path(&path);
490    }
491
492    let mut path_data = builder.finish()?;
493    path_data = path_data.transform(transform)?;
494
495    Path::new(
496        String::new(),
497        span.visible,
498        decoration.fill.take(),
499        decoration.stroke.take(),
500        PaintOrder::default(),
501        ShapeRendering::default(),
502        Arc::new(path_data),
503        Transform::default(),
504    )
505}
506
507/// A text decoration span.
508///
509/// Basically a horizontal line, that will be used for underline, overline and line-through.
510/// It doesn't have a height, since it depends on the Font metrics.
511#[derive(Clone, Copy)]
512pub(crate) struct DecorationSpan {
513    pub(crate) width: f32,
514    pub(crate) transform: Transform,
515}
516
517/// Resolves clusters positions.
518///
519/// Mainly sets the `transform` property.
520///
521/// Returns the last text position. The next text chunk should start from that position.
522fn resolve_clusters_positions(
523    text: &Text,
524    chunk: &TextChunk,
525    char_offset: usize,
526    writing_mode: WritingMode,
527    fonts_cache: &FontsCache,
528    clusters: &mut [GlyphCluster],
529) -> (f32, f32) {
530    match chunk.text_flow {
531        TextFlow::Linear => {
532            resolve_clusters_positions_horizontal(text, chunk, char_offset, writing_mode, clusters)
533        }
534        TextFlow::Path(ref path) => resolve_clusters_positions_path(
535            text,
536            chunk,
537            char_offset,
538            path,
539            writing_mode,
540            fonts_cache,
541            clusters,
542        ),
543    }
544}
545
546fn clusters_length(clusters: &[GlyphCluster]) -> f32 {
547    clusters.iter().fold(0.0, |w, cluster| w + cluster.advance)
548}
549
550fn resolve_clusters_positions_horizontal(
551    text: &Text,
552    chunk: &TextChunk,
553    offset: usize,
554    writing_mode: WritingMode,
555    clusters: &mut [GlyphCluster],
556) -> (f32, f32) {
557    let mut x = process_anchor(chunk.anchor, clusters_length(clusters));
558    let mut y = 0.0;
559
560    for cluster in clusters {
561        let cp = offset + cluster.byte_idx.code_point_at(&chunk.text);
562        if let (Some(dx), Some(dy)) = (text.dx.get(cp), text.dy.get(cp)) {
563            if writing_mode == WritingMode::LeftToRight {
564                x += dx;
565                y += dy;
566            } else {
567                y -= dx;
568                x += dy;
569            }
570            cluster.has_relative_shift = !dx.approx_zero_ulps(4) || !dy.approx_zero_ulps(4);
571        }
572
573        cluster.transform = cluster.transform.pre_translate(x, y);
574
575        if let Some(angle) = text.rotate.get(cp).cloned() {
576            if !angle.approx_zero_ulps(4) {
577                cluster.transform = cluster.transform.pre_rotate(angle);
578                cluster.has_relative_shift = true;
579            }
580        }
581
582        x += cluster.advance;
583    }
584
585    (x, y)
586}
587
588// Baseline resolving in SVG is a mess.
589// Not only it's poorly documented, but as soon as you start mixing
590// `dominant-baseline` and `alignment-baseline` each application/browser will produce
591// different results.
592//
593// For now, resvg simply tries to match Chrome's output and not the mythical SVG spec output.
594//
595// See `alignment_baseline_shift` method comment for more details.
596pub(crate) fn resolve_baseline(
597    span: &TextSpan,
598    font: &ResolvedFont,
599    writing_mode: WritingMode,
600) -> f32 {
601    let mut shift = -resolve_baseline_shift(&span.baseline_shift, font, span.font_size.get());
602
603    // TODO: support vertical layout as well
604    if writing_mode == WritingMode::LeftToRight {
605        if span.alignment_baseline == AlignmentBaseline::Auto
606            || span.alignment_baseline == AlignmentBaseline::Baseline
607        {
608            shift += font.dominant_baseline_shift(span.dominant_baseline, span.font_size.get());
609        } else {
610            shift += font.alignment_baseline_shift(span.alignment_baseline, span.font_size.get());
611        }
612    }
613
614    shift
615}
616
617fn resolve_baseline_shift(baselines: &[BaselineShift], font: &ResolvedFont, font_size: f32) -> f32 {
618    let mut shift = 0.0;
619    for baseline in baselines.iter().rev() {
620        match baseline {
621            BaselineShift::Baseline => {}
622            BaselineShift::Subscript => shift -= font.subscript_offset(font_size),
623            BaselineShift::Superscript => shift += font.superscript_offset(font_size),
624            BaselineShift::Number(n) => shift += n,
625        }
626    }
627
628    shift
629}
630
631fn resolve_clusters_positions_path(
632    text: &Text,
633    chunk: &TextChunk,
634    char_offset: usize,
635    path: &TextPath,
636    writing_mode: WritingMode,
637    fonts_cache: &FontsCache,
638    clusters: &mut [GlyphCluster],
639) -> (f32, f32) {
640    let mut last_x = 0.0;
641    let mut last_y = 0.0;
642
643    let mut dy = 0.0;
644
645    // In the text path mode, chunk's x/y coordinates provide an additional offset along the path.
646    // The X coordinate is used in a horizontal mode, and Y in vertical.
647    let chunk_offset = match writing_mode {
648        WritingMode::LeftToRight => chunk.x.unwrap_or(0.0),
649        WritingMode::TopToBottom => chunk.y.unwrap_or(0.0),
650    };
651
652    let start_offset =
653        chunk_offset + path.start_offset + process_anchor(chunk.anchor, clusters_length(clusters));
654
655    let normals = collect_normals(text, chunk, clusters, &path.path, char_offset, start_offset);
656    for (cluster, normal) in clusters.iter_mut().zip(normals) {
657        let (x, y, angle) = match normal {
658            Some(normal) => (normal.x, normal.y, normal.angle),
659            None => {
660                // Hide clusters that are outside the text path.
661                cluster.visible = false;
662                continue;
663            }
664        };
665
666        // We have to break a decoration line for each cluster during text-on-path.
667        cluster.has_relative_shift = true;
668
669        let orig_ts = cluster.transform;
670
671        // Clusters should be rotated by the x-midpoint x baseline position.
672        let half_width = cluster.width / 2.0;
673        cluster.transform = Transform::default();
674        cluster.transform = cluster.transform.pre_translate(x - half_width, y);
675        cluster.transform = cluster.transform.pre_rotate_at(angle, half_width, 0.0);
676
677        let cp = char_offset + cluster.byte_idx.code_point_at(&chunk.text);
678        dy += text.dy.get(cp).cloned().unwrap_or(0.0);
679
680        let baseline_shift = chunk_span_at(chunk, cluster.byte_idx)
681            .map(|span| {
682                let font = match fonts_cache.get(&span.font) {
683                    Some(v) => v,
684                    None => return 0.0,
685                };
686                -resolve_baseline(span, font, writing_mode)
687            })
688            .unwrap_or(0.0);
689
690        // Shift only by `dy` since we already applied `dx`
691        // during offset along the path calculation.
692        if !dy.approx_zero_ulps(4) || !baseline_shift.approx_zero_ulps(4) {
693            let shift = kurbo::Vec2::new(0.0, (dy - baseline_shift) as f64);
694            cluster.transform = cluster
695                .transform
696                .pre_translate(shift.x as f32, shift.y as f32);
697        }
698
699        if let Some(angle) = text.rotate.get(cp).cloned() {
700            if !angle.approx_zero_ulps(4) {
701                cluster.transform = cluster.transform.pre_rotate(angle);
702            }
703        }
704
705        // The possible `lengthAdjust` transform should be applied after text-on-path positioning.
706        cluster.transform = cluster.transform.pre_concat(orig_ts);
707
708        last_x = x + cluster.advance;
709        last_y = y;
710    }
711
712    (last_x, last_y)
713}
714
715pub(crate) fn process_anchor(a: TextAnchor, text_width: f32) -> f32 {
716    match a {
717        TextAnchor::Start => 0.0, // Nothing.
718        TextAnchor::Middle => -text_width / 2.0,
719        TextAnchor::End => -text_width,
720    }
721}
722
723pub(crate) struct PathNormal {
724    pub(crate) x: f32,
725    pub(crate) y: f32,
726    pub(crate) angle: f32,
727}
728
729fn collect_normals(
730    text: &Text,
731    chunk: &TextChunk,
732    clusters: &[GlyphCluster],
733    path: &tiny_skia_path::Path,
734    char_offset: usize,
735    offset: f32,
736) -> Vec<Option<PathNormal>> {
737    let mut offsets = Vec::with_capacity(clusters.len());
738    let mut normals = Vec::with_capacity(clusters.len());
739    {
740        let mut advance = offset;
741        for cluster in clusters {
742            // Clusters should be rotated by the x-midpoint x baseline position.
743            let half_width = cluster.width / 2.0;
744
745            // Include relative position.
746            let cp = char_offset + cluster.byte_idx.code_point_at(&chunk.text);
747            advance += text.dx.get(cp).cloned().unwrap_or(0.0);
748
749            let offset = advance + half_width;
750
751            // Clusters outside the path have no normals.
752            if offset < 0.0 {
753                normals.push(None);
754            }
755
756            offsets.push(offset as f64);
757            advance += cluster.advance;
758        }
759    }
760
761    let mut prev_mx = path.points()[0].x;
762    let mut prev_my = path.points()[0].y;
763    let mut prev_x = prev_mx;
764    let mut prev_y = prev_my;
765
766    fn create_curve_from_line(px: f32, py: f32, x: f32, y: f32) -> kurbo::CubicBez {
767        let line = kurbo::Line::new(
768            kurbo::Point::new(px as f64, py as f64),
769            kurbo::Point::new(x as f64, y as f64),
770        );
771        let p1 = line.eval(0.33);
772        let p2 = line.eval(0.66);
773        kurbo::CubicBez {
774            p0: line.p0,
775            p1,
776            p2,
777            p3: line.p1,
778        }
779    }
780
781    let mut length: f64 = 0.0;
782    for seg in path.segments() {
783        let curve = match seg {
784            tiny_skia_path::PathSegment::MoveTo(p) => {
785                prev_mx = p.x;
786                prev_my = p.y;
787                prev_x = p.x;
788                prev_y = p.y;
789                continue;
790            }
791            tiny_skia_path::PathSegment::LineTo(p) => {
792                create_curve_from_line(prev_x, prev_y, p.x, p.y)
793            }
794            tiny_skia_path::PathSegment::QuadTo(p1, p) => kurbo::QuadBez {
795                p0: kurbo::Point::new(prev_x as f64, prev_y as f64),
796                p1: kurbo::Point::new(p1.x as f64, p1.y as f64),
797                p2: kurbo::Point::new(p.x as f64, p.y as f64),
798            }
799            .raise(),
800            tiny_skia_path::PathSegment::CubicTo(p1, p2, p) => kurbo::CubicBez {
801                p0: kurbo::Point::new(prev_x as f64, prev_y as f64),
802                p1: kurbo::Point::new(p1.x as f64, p1.y as f64),
803                p2: kurbo::Point::new(p2.x as f64, p2.y as f64),
804                p3: kurbo::Point::new(p.x as f64, p.y as f64),
805            },
806            tiny_skia_path::PathSegment::Close => {
807                create_curve_from_line(prev_x, prev_y, prev_mx, prev_my)
808            }
809        };
810
811        let arclen_accuracy = {
812            let base_arclen_accuracy = 0.5;
813            // Accuracy depends on a current scale.
814            // When we have a tiny path scaled by a large value,
815            // we have to increase out accuracy accordingly.
816            let (sx, sy) = text.abs_transform.get_scale();
817            // 1.0 acts as a threshold to prevent division by 0 and/or low accuracy.
818            base_arclen_accuracy / (sx * sy).sqrt().max(1.0)
819        };
820
821        let curve_len = curve.arclen(arclen_accuracy as f64);
822
823        for offset in &offsets[normals.len()..] {
824            if *offset >= length && *offset <= length + curve_len {
825                let mut offset = curve.inv_arclen(offset - length, arclen_accuracy as f64);
826                // some rounding error may occur, so we give offset a little tolerance
827                debug_assert!((-1.0e-3..=1.0 + 1.0e-3).contains(&offset));
828                offset = offset.min(1.0).max(0.0);
829
830                let pos = curve.eval(offset);
831                let d = curve.deriv().eval(offset);
832                let d = kurbo::Vec2::new(-d.y, d.x); // tangent
833                let angle = d.atan2().to_degrees() - 90.0;
834
835                normals.push(Some(PathNormal {
836                    x: pos.x as f32,
837                    y: pos.y as f32,
838                    angle: angle as f32,
839                }));
840
841                if normals.len() == offsets.len() {
842                    break;
843                }
844            }
845        }
846
847        length += curve_len;
848        prev_x = curve.p3.x as f32;
849        prev_y = curve.p3.y as f32;
850    }
851
852    // If path ended and we still have unresolved normals - set them to `None`.
853    for _ in 0..(offsets.len() - normals.len()) {
854        normals.push(None);
855    }
856
857    normals
858}
859
860/// Converts a text chunk into a list of outlined clusters.
861///
862/// This function will do the BIDI reordering, text shaping and glyphs outlining,
863/// but not the text layouting. So all clusters are in the 0x0 position.
864fn process_chunk(
865    chunk: &TextChunk,
866    fonts_cache: &FontsCache,
867    resolver: &FontResolver,
868    fontdb: &mut Arc<fontdb::Database>,
869) -> Vec<GlyphCluster> {
870    // The way this function works is a bit tricky.
871    //
872    // The first problem is BIDI reordering.
873    // We cannot shape text span-by-span, because glyph clusters are not guarantee to be continuous.
874    //
875    // For example:
876    // <text>Hel<tspan fill="url(#lg1)">lo של</tspan>ום.</text>
877    //
878    // Would be shaped as:
879    // H e l l o   ש ל  ו  ם .   (characters)
880    // 0 1 2 3 4 5 12 10 8 6 14  (cluster indices in UTF-8)
881    //       ---         ---     (green span)
882    //
883    // As you can see, our continuous `lo של` span was split into two separated one.
884    // So our 3 spans: black - green - black, become 5 spans: black - green - black - green - black.
885    // If we shape `Hel`, then `lo של` an then `ום` separately - we would get an incorrect output.
886    // To properly handle this we simply shape the whole chunk.
887    //
888    // But this introduces another issue - what to do when we have multiple fonts?
889    // The easy solution would be to simply shape text with each font,
890    // where the first font output is used as a base one and all others overwrite it.
891    // This way in case of:
892    // <text font-family="Arial">Hello <tspan font-family="Helvetica">world</tspan></text>
893    // we would replace Arial glyphs for `world` with Helvetica one. Pretty simple.
894    //
895    // Well, it would work most of the time, but not always.
896    // This is because different fonts can produce different amount of glyphs for the same text.
897    // The most common example are ligatures. Some fonts can shape `fi` as two glyphs `f` and `i`,
898    // but some can use `fi` (U+FB01) instead.
899    // Meaning that during merging we have to overwrite not individual glyphs, but clusters.
900
901    let mut glyphs = Vec::new();
902    for span in &chunk.spans {
903        let font = match fonts_cache.get(&span.font) {
904            Some(v) => v.clone(),
905            None => continue,
906        };
907
908        let tmp_glyphs = shape_text(
909            &chunk.text,
910            font,
911            span.small_caps,
912            span.apply_kerning,
913            resolver,
914            fontdb,
915        );
916
917        // Do nothing with the first run.
918        if glyphs.is_empty() {
919            glyphs = tmp_glyphs;
920            continue;
921        }
922
923        // Overwrite span's glyphs.
924        let mut iter = tmp_glyphs.into_iter();
925        while let Some(new_glyph) = iter.next() {
926            if !span_contains(span, new_glyph.byte_idx) {
927                continue;
928            }
929
930            let Some(idx) = glyphs.iter().position(|g| g.byte_idx == new_glyph.byte_idx) else {
931                continue;
932            };
933
934            let prev_cluster_len = glyphs[idx].cluster_len;
935            if prev_cluster_len < new_glyph.cluster_len {
936                // If the new font represents the same cluster with fewer glyphs
937                // then remove remaining glyphs.
938                for _ in 1..new_glyph.cluster_len {
939                    glyphs.remove(idx + 1);
940                }
941            } else if prev_cluster_len > new_glyph.cluster_len {
942                // If the new font represents the same cluster with more glyphs
943                // then insert them after the current one.
944                for j in 1..prev_cluster_len {
945                    if let Some(g) = iter.next() {
946                        glyphs.insert(idx + j, g);
947                    }
948                }
949            }
950
951            glyphs[idx] = new_glyph;
952        }
953    }
954
955    // Convert glyphs to clusters.
956    let mut clusters = Vec::new();
957    for (range, byte_idx) in GlyphClusters::new(&glyphs) {
958        if let Some(span) = chunk_span_at(chunk, byte_idx) {
959            clusters.push(form_glyph_clusters(
960                &glyphs[range],
961                &chunk.text,
962                span.font_size.get(),
963            ));
964        }
965    }
966
967    clusters
968}
969
970fn apply_length_adjust(chunk: &TextChunk, clusters: &mut [GlyphCluster]) {
971    let is_horizontal = matches!(chunk.text_flow, TextFlow::Linear);
972
973    for span in &chunk.spans {
974        let target_width = match span.text_length {
975            Some(v) => v,
976            None => continue,
977        };
978
979        let mut width = 0.0;
980        let mut cluster_indexes = Vec::new();
981        for i in span.start..span.end {
982            if let Some(index) = clusters.iter().position(|c| c.byte_idx.value() == i) {
983                cluster_indexes.push(index);
984            }
985        }
986        // Complex scripts can have mutli-codepoint clusters therefore we have to remove duplicates.
987        cluster_indexes.sort();
988        cluster_indexes.dedup();
989
990        for i in &cluster_indexes {
991            // Use the original cluster `width` and not `advance`.
992            // This method essentially discards any `word-spacing` and `letter-spacing`.
993            width += clusters[*i].width;
994        }
995
996        if cluster_indexes.is_empty() {
997            continue;
998        }
999
1000        if span.length_adjust == LengthAdjust::Spacing {
1001            let factor = if cluster_indexes.len() > 1 {
1002                (target_width - width) / (cluster_indexes.len() - 1) as f32
1003            } else {
1004                0.0
1005            };
1006
1007            for i in cluster_indexes {
1008                clusters[i].advance = clusters[i].width + factor;
1009            }
1010        } else {
1011            let factor = target_width / width;
1012            // Prevent multiplying by zero.
1013            if factor < 0.001 {
1014                continue;
1015            }
1016
1017            for i in cluster_indexes {
1018                clusters[i].transform = clusters[i].transform.pre_scale(factor, 1.0);
1019
1020                // Technically just a hack to support the current text-on-path algorithm.
1021                if !is_horizontal {
1022                    clusters[i].advance *= factor;
1023                    clusters[i].width *= factor;
1024                }
1025            }
1026        }
1027    }
1028}
1029
1030/// Rotates clusters according to
1031/// [Unicode Vertical_Orientation Property](https://www.unicode.org/reports/tr50/tr50-19.html).
1032fn apply_writing_mode(writing_mode: WritingMode, clusters: &mut [GlyphCluster]) {
1033    if writing_mode != WritingMode::TopToBottom {
1034        return;
1035    }
1036
1037    for cluster in clusters {
1038        let orientation = unicode_vo::char_orientation(cluster.codepoint);
1039        if orientation == unicode_vo::Orientation::Upright {
1040            let mut ts = Transform::default();
1041            // Position glyph in the center of vertical axis.
1042            ts = ts.pre_translate(0.0, (cluster.ascent + cluster.descent) / 2.0);
1043            // Rotate by 90 degrees in the center.
1044            ts = ts.pre_rotate_at(
1045                -90.0,
1046                cluster.width / 2.0,
1047                -(cluster.ascent + cluster.descent) / 2.0,
1048            );
1049
1050            cluster.path_transform = ts;
1051
1052            // Move "baseline" to the middle and make height equal to width.
1053            cluster.ascent = cluster.width / 2.0;
1054            cluster.descent = -cluster.width / 2.0;
1055        } else {
1056            // Could not find a spec that explains this,
1057            // but this is how other applications are shifting the "rotated" characters
1058            // in the top-to-bottom mode.
1059            cluster.transform = cluster
1060                .transform
1061                .pre_translate(0.0, (cluster.ascent + cluster.descent) / 2.0);
1062        }
1063    }
1064}
1065
1066/// Applies the `letter-spacing` property to a text chunk clusters.
1067///
1068/// [In the CSS spec](https://www.w3.org/TR/css-text-3/#letter-spacing-property).
1069fn apply_letter_spacing(chunk: &TextChunk, clusters: &mut [GlyphCluster]) {
1070    // At least one span should have a non-zero spacing.
1071    if !chunk
1072        .spans
1073        .iter()
1074        .any(|span| !span.letter_spacing.approx_zero_ulps(4))
1075    {
1076        return;
1077    }
1078
1079    let num_clusters = clusters.len();
1080    for (i, cluster) in clusters.iter_mut().enumerate() {
1081        // Spacing must be applied only to characters that belongs to the script
1082        // that supports spacing.
1083        // We are checking only the first code point, since it should be enough.
1084        // https://www.w3.org/TR/css-text-3/#cursive-tracking
1085        let script = cluster.codepoint.script();
1086        if script_supports_letter_spacing(script) {
1087            if let Some(span) = chunk_span_at(chunk, cluster.byte_idx) {
1088                // A space after the last cluster should be ignored,
1089                // since it affects the bbox and text alignment.
1090                if i != num_clusters - 1 {
1091                    cluster.advance += span.letter_spacing;
1092                }
1093
1094                // If the cluster advance became negative - clear it.
1095                // This is an UB so we can do whatever we want, and we mimic Chrome's behavior.
1096                if !cluster.advance.is_valid_length() {
1097                    cluster.width = 0.0;
1098                    cluster.advance = 0.0;
1099                    cluster.glyphs = vec![];
1100                }
1101            }
1102        }
1103    }
1104}
1105
1106/// Applies the `word-spacing` property to a text chunk clusters.
1107///
1108/// [In the CSS spec](https://www.w3.org/TR/css-text-3/#propdef-word-spacing).
1109fn apply_word_spacing(chunk: &TextChunk, clusters: &mut [GlyphCluster]) {
1110    // At least one span should have a non-zero spacing.
1111    if !chunk
1112        .spans
1113        .iter()
1114        .any(|span| !span.word_spacing.approx_zero_ulps(4))
1115    {
1116        return;
1117    }
1118
1119    for cluster in clusters {
1120        if is_word_separator_characters(cluster.codepoint) {
1121            if let Some(span) = chunk_span_at(chunk, cluster.byte_idx) {
1122                // Technically, word spacing 'should be applied half on each
1123                // side of the character', but it doesn't affect us in any way,
1124                // so we are ignoring this.
1125                cluster.advance += span.word_spacing;
1126
1127                // After word spacing, `advance` can be negative.
1128            }
1129        }
1130    }
1131}
1132
1133fn form_glyph_clusters(glyphs: &[Glyph], text: &str, font_size: f32) -> GlyphCluster {
1134    debug_assert!(!glyphs.is_empty());
1135
1136    let mut width = 0.0;
1137    let mut x: f32 = 0.0;
1138
1139    let mut positioned_glyphs = vec![];
1140
1141    for glyph in glyphs {
1142        let sx = glyph.font.scale(font_size);
1143
1144        // Apply offset.
1145        //
1146        // The first glyph in the cluster will have an offset from 0x0,
1147        // but the later one will have an offset from the "current position".
1148        // So we have to keep an advance.
1149        // TODO: should be done only inside a single text span
1150        let ts = Transform::from_translate(x + glyph.dx as f32, glyph.dy as f32);
1151
1152        positioned_glyphs.push(PositionedGlyph {
1153            glyph_ts: ts,
1154            // Will be set later.
1155            cluster_ts: Transform::default(),
1156            // Will be set later.
1157            span_ts: Transform::default(),
1158            units_per_em: glyph.font.units_per_em.get(),
1159            font_size,
1160            font: glyph.font.id,
1161            text: glyph.text.clone(),
1162            id: glyph.id,
1163        });
1164
1165        x += glyph.width as f32;
1166
1167        let glyph_width = glyph.width as f32 * sx;
1168        if glyph_width > width {
1169            width = glyph_width;
1170        }
1171    }
1172
1173    let byte_idx = glyphs[0].byte_idx;
1174    let font = glyphs[0].font.clone();
1175    GlyphCluster {
1176        byte_idx,
1177        codepoint: byte_idx.char_from(text),
1178        width,
1179        advance: width,
1180        ascent: font.ascent(font_size),
1181        descent: font.descent(font_size),
1182        has_relative_shift: false,
1183        transform: Transform::default(),
1184        path_transform: Transform::default(),
1185        glyphs: positioned_glyphs,
1186        visible: true,
1187    }
1188}
1189
1190pub(crate) trait DatabaseExt {
1191    fn load_font(&self, id: ID) -> Option<ResolvedFont>;
1192    fn has_char(&self, id: ID, c: char) -> bool;
1193}
1194
1195impl DatabaseExt for Database {
1196    #[inline(never)]
1197    fn load_font(&self, id: ID) -> Option<ResolvedFont> {
1198        self.with_face_data(id, |data, face_index| -> Option<ResolvedFont> {
1199            let font = ttf_parser::Face::parse(data, face_index).ok()?;
1200
1201            let units_per_em = NonZeroU16::new(font.units_per_em())?;
1202
1203            let ascent = font.ascender();
1204            let descent = font.descender();
1205
1206            let x_height = font
1207                .x_height()
1208                .and_then(|x| u16::try_from(x).ok())
1209                .and_then(NonZeroU16::new);
1210            let x_height = match x_height {
1211                Some(height) => height,
1212                None => {
1213                    // If not set - fallback to height * 45%.
1214                    // 45% is what Firefox uses.
1215                    u16::try_from((f32::from(ascent - descent) * 0.45) as i32)
1216                        .ok()
1217                        .and_then(NonZeroU16::new)?
1218                }
1219            };
1220
1221            let line_through = font.strikeout_metrics();
1222            let line_through_position = match line_through {
1223                Some(metrics) => metrics.position,
1224                None => x_height.get() as i16 / 2,
1225            };
1226
1227            let (underline_position, underline_thickness) = match font.underline_metrics() {
1228                Some(metrics) => {
1229                    let thickness = u16::try_from(metrics.thickness)
1230                        .ok()
1231                        .and_then(NonZeroU16::new)
1232                        // `ttf_parser` guarantees that units_per_em is >= 16
1233                        .unwrap_or_else(|| NonZeroU16::new(units_per_em.get() / 12).unwrap());
1234
1235                    (metrics.position, thickness)
1236                }
1237                None => (
1238                    -(units_per_em.get() as i16) / 9,
1239                    NonZeroU16::new(units_per_em.get() / 12).unwrap(),
1240                ),
1241            };
1242
1243            // 0.2 and 0.4 are generic offsets used by some applications (Inkscape/librsvg).
1244            let mut subscript_offset = (units_per_em.get() as f32 / 0.2).round() as i16;
1245            let mut superscript_offset = (units_per_em.get() as f32 / 0.4).round() as i16;
1246            if let Some(metrics) = font.subscript_metrics() {
1247                subscript_offset = metrics.y_offset;
1248            }
1249
1250            if let Some(metrics) = font.superscript_metrics() {
1251                superscript_offset = metrics.y_offset;
1252            }
1253
1254            Some(ResolvedFont {
1255                id,
1256                units_per_em,
1257                ascent,
1258                descent,
1259                x_height,
1260                underline_position,
1261                underline_thickness,
1262                line_through_position,
1263                subscript_offset,
1264                superscript_offset,
1265            })
1266        })?
1267    }
1268
1269    #[inline(never)]
1270    fn has_char(&self, id: ID, c: char) -> bool {
1271        let res = self.with_face_data(id, |font_data, face_index| -> Option<bool> {
1272            let font = ttf_parser::Face::parse(font_data, face_index).ok()?;
1273            font.glyph_index(c)?;
1274            Some(true)
1275        });
1276
1277        res == Some(Some(true))
1278    }
1279}
1280
1281/// Text shaping with font fallback.
1282pub(crate) fn shape_text(
1283    text: &str,
1284    font: Arc<ResolvedFont>,
1285    small_caps: bool,
1286    apply_kerning: bool,
1287    resolver: &FontResolver,
1288    fontdb: &mut Arc<fontdb::Database>,
1289) -> Vec<Glyph> {
1290    let mut glyphs = shape_text_with_font(text, font.clone(), small_caps, apply_kerning, fontdb)
1291        .unwrap_or_default();
1292
1293    // Remember all fonts used for shaping.
1294    let mut used_fonts = vec![font.id];
1295
1296    // Loop until all glyphs become resolved or until no more fonts are left.
1297    'outer: loop {
1298        let mut missing = None;
1299        for glyph in &glyphs {
1300            if glyph.is_missing() {
1301                missing = Some(glyph.byte_idx.char_from(text));
1302                break;
1303            }
1304        }
1305
1306        if let Some(c) = missing {
1307            let fallback_font = match (resolver.select_fallback)(c, &used_fonts, fontdb)
1308                .and_then(|id| fontdb.load_font(id))
1309            {
1310                Some(v) => Arc::new(v),
1311                None => break 'outer,
1312            };
1313
1314            // Shape again, using a new font.
1315            let fallback_glyphs = shape_text_with_font(
1316                text,
1317                fallback_font.clone(),
1318                small_caps,
1319                apply_kerning,
1320                fontdb,
1321            )
1322            .unwrap_or_default();
1323
1324            let all_matched = fallback_glyphs.iter().all(|g| !g.is_missing());
1325            if all_matched {
1326                // Replace all glyphs when all of them were matched.
1327                glyphs = fallback_glyphs;
1328                break 'outer;
1329            }
1330
1331            // We assume, that shaping with an any font will produce the same amount of glyphs.
1332            // This is incorrect, but good enough for now.
1333            if glyphs.len() != fallback_glyphs.len() {
1334                break 'outer;
1335            }
1336
1337            // TODO: Replace clusters and not glyphs. This should be more accurate.
1338
1339            // Copy new glyphs.
1340            for i in 0..glyphs.len() {
1341                if glyphs[i].is_missing() && !fallback_glyphs[i].is_missing() {
1342                    glyphs[i] = fallback_glyphs[i].clone();
1343                }
1344            }
1345
1346            // Remember this font.
1347            used_fonts.push(fallback_font.id);
1348        } else {
1349            break 'outer;
1350        }
1351    }
1352
1353    // Warn about missing glyphs.
1354    for glyph in &glyphs {
1355        if glyph.is_missing() {
1356            let c = glyph.byte_idx.char_from(text);
1357            // TODO: print a full grapheme
1358            log::warn!(
1359                "No fonts with a {}/U+{:X} character were found.",
1360                c,
1361                c as u32
1362            );
1363        }
1364    }
1365
1366    glyphs
1367}
1368
1369/// Converts a text into a list of glyph IDs.
1370///
1371/// This function will do the BIDI reordering and text shaping.
1372fn shape_text_with_font(
1373    text: &str,
1374    font: Arc<ResolvedFont>,
1375    small_caps: bool,
1376    apply_kerning: bool,
1377    fontdb: &fontdb::Database,
1378) -> Option<Vec<Glyph>> {
1379    fontdb.with_face_data(font.id, |font_data, face_index| -> Option<Vec<Glyph>> {
1380        let rb_font = rustybuzz::Face::from_slice(font_data, face_index)?;
1381
1382        let bidi_info = unicode_bidi::BidiInfo::new(text, Some(unicode_bidi::Level::ltr()));
1383        let paragraph = &bidi_info.paragraphs[0];
1384        let line = paragraph.range.clone();
1385
1386        let mut glyphs = Vec::new();
1387
1388        let (levels, runs) = bidi_info.visual_runs(paragraph, line);
1389        for run in runs.iter() {
1390            let sub_text = &text[run.clone()];
1391            if sub_text.is_empty() {
1392                continue;
1393            }
1394
1395            let ltr = levels[run.start].is_ltr();
1396            let hb_direction = if ltr {
1397                rustybuzz::Direction::LeftToRight
1398            } else {
1399                rustybuzz::Direction::RightToLeft
1400            };
1401
1402            let mut buffer = rustybuzz::UnicodeBuffer::new();
1403            buffer.push_str(sub_text);
1404            buffer.set_direction(hb_direction);
1405
1406            let mut features = Vec::new();
1407            if small_caps {
1408                features.push(rustybuzz::Feature::new(Tag::from_bytes(b"smcp"), 1, ..));
1409            }
1410
1411            if !apply_kerning {
1412                features.push(rustybuzz::Feature::new(Tag::from_bytes(b"kern"), 0, ..));
1413            }
1414
1415            let output = rustybuzz::shape(&rb_font, &features, buffer);
1416
1417            let positions = output.glyph_positions();
1418            let infos = output.glyph_infos();
1419
1420            for i in 0..output.len() {
1421                let pos = positions[i];
1422                let info = infos[i];
1423                let idx = run.start + info.cluster as usize;
1424
1425                let start = info.cluster as usize;
1426
1427                let end = if ltr {
1428                    i.checked_add(1)
1429                } else {
1430                    i.checked_sub(1)
1431                }
1432                .and_then(|last| infos.get(last))
1433                .map_or(sub_text.len(), |info| info.cluster as usize);
1434
1435                glyphs.push(Glyph {
1436                    byte_idx: ByteIndex::new(idx),
1437                    cluster_len: end.checked_sub(start).unwrap_or(0), // TODO: can fail?
1438                    text: sub_text[start..end].to_string(),
1439                    id: GlyphId(info.glyph_id as u16),
1440                    dx: pos.x_offset,
1441                    dy: pos.y_offset,
1442                    width: pos.x_advance,
1443                    font: font.clone(),
1444                });
1445            }
1446        }
1447
1448        Some(glyphs)
1449    })?
1450}
1451
1452/// An iterator over glyph clusters.
1453///
1454/// Input:  0 2 2 2 3 4 4 5 5
1455/// Result: 0 1     4 5   7
1456pub(crate) struct GlyphClusters<'a> {
1457    data: &'a [Glyph],
1458    idx: usize,
1459}
1460
1461impl<'a> GlyphClusters<'a> {
1462    pub(crate) fn new(data: &'a [Glyph]) -> Self {
1463        GlyphClusters { data, idx: 0 }
1464    }
1465}
1466
1467impl<'a> Iterator for GlyphClusters<'a> {
1468    type Item = (std::ops::Range<usize>, ByteIndex);
1469
1470    fn next(&mut self) -> Option<Self::Item> {
1471        if self.idx == self.data.len() {
1472            return None;
1473        }
1474
1475        let start = self.idx;
1476        let cluster = self.data[self.idx].byte_idx;
1477        for g in &self.data[self.idx..] {
1478            if g.byte_idx != cluster {
1479                break;
1480            }
1481
1482            self.idx += 1;
1483        }
1484
1485        Some((start..self.idx, cluster))
1486    }
1487}
1488
1489/// Checks that selected script supports letter spacing.
1490///
1491/// [In the CSS spec](https://www.w3.org/TR/css-text-3/#cursive-tracking).
1492///
1493/// The list itself is from: https://github.com/harfbuzz/harfbuzz/issues/64
1494pub(crate) fn script_supports_letter_spacing(script: unicode_script::Script) -> bool {
1495    use unicode_script::Script;
1496
1497    !matches!(
1498        script,
1499        Script::Arabic
1500            | Script::Syriac
1501            | Script::Nko
1502            | Script::Manichaean
1503            | Script::Psalter_Pahlavi
1504            | Script::Mandaic
1505            | Script::Mongolian
1506            | Script::Phags_Pa
1507            | Script::Devanagari
1508            | Script::Bengali
1509            | Script::Gurmukhi
1510            | Script::Modi
1511            | Script::Sharada
1512            | Script::Syloti_Nagri
1513            | Script::Tirhuta
1514            | Script::Ogham
1515    )
1516}
1517
1518/// A glyph.
1519///
1520/// Basically, a glyph ID and it's metrics.
1521#[derive(Clone)]
1522pub(crate) struct Glyph {
1523    /// The glyph ID in the font.
1524    pub(crate) id: GlyphId,
1525
1526    /// Position in bytes in the original string.
1527    ///
1528    /// We use it to match a glyph with a character in the text chunk and therefore with the style.
1529    pub(crate) byte_idx: ByteIndex,
1530
1531    // The length of the cluster in bytes.
1532    pub(crate) cluster_len: usize,
1533
1534    /// The text from the original string that corresponds to that glyph.
1535    pub(crate) text: String,
1536
1537    /// The glyph offset in font units.
1538    pub(crate) dx: i32,
1539
1540    /// The glyph offset in font units.
1541    pub(crate) dy: i32,
1542
1543    /// The glyph width / X-advance in font units.
1544    pub(crate) width: i32,
1545
1546    /// Reference to the source font.
1547    ///
1548    /// Each glyph can have it's own source font.
1549    pub(crate) font: Arc<ResolvedFont>,
1550}
1551
1552impl Glyph {
1553    fn is_missing(&self) -> bool {
1554        self.id.0 == 0
1555    }
1556}
1557
1558#[derive(Clone, Copy, Debug)]
1559pub(crate) struct ResolvedFont {
1560    pub(crate) id: ID,
1561
1562    units_per_em: NonZeroU16,
1563
1564    // All values below are in font units.
1565    ascent: i16,
1566    descent: i16,
1567    x_height: NonZeroU16,
1568
1569    underline_position: i16,
1570    underline_thickness: NonZeroU16,
1571
1572    // line-through thickness should be the the same as underline thickness
1573    // according to the TrueType spec:
1574    // https://docs.microsoft.com/en-us/typography/opentype/spec/os2#ystrikeoutsize
1575    line_through_position: i16,
1576
1577    subscript_offset: i16,
1578    superscript_offset: i16,
1579}
1580
1581pub(crate) fn chunk_span_at(chunk: &TextChunk, byte_offset: ByteIndex) -> Option<&TextSpan> {
1582    chunk
1583        .spans
1584        .iter()
1585        .find(|&span| span_contains(span, byte_offset))
1586}
1587
1588pub(crate) fn span_contains(span: &TextSpan, byte_offset: ByteIndex) -> bool {
1589    byte_offset.value() >= span.start && byte_offset.value() < span.end
1590}
1591
1592/// Checks that the selected character is a word separator.
1593///
1594/// According to: https://www.w3.org/TR/css-text-3/#word-separator
1595pub(crate) fn is_word_separator_characters(c: char) -> bool {
1596    matches!(
1597        c as u32,
1598        0x0020 | 0x00A0 | 0x1361 | 0x010100 | 0x010101 | 0x01039F | 0x01091F
1599    )
1600}
1601
1602impl ResolvedFont {
1603    #[inline]
1604    pub(crate) fn scale(&self, font_size: f32) -> f32 {
1605        font_size / self.units_per_em.get() as f32
1606    }
1607
1608    #[inline]
1609    pub(crate) fn ascent(&self, font_size: f32) -> f32 {
1610        self.ascent as f32 * self.scale(font_size)
1611    }
1612
1613    #[inline]
1614    pub(crate) fn descent(&self, font_size: f32) -> f32 {
1615        self.descent as f32 * self.scale(font_size)
1616    }
1617
1618    #[inline]
1619    pub(crate) fn height(&self, font_size: f32) -> f32 {
1620        self.ascent(font_size) - self.descent(font_size)
1621    }
1622
1623    #[inline]
1624    pub(crate) fn x_height(&self, font_size: f32) -> f32 {
1625        self.x_height.get() as f32 * self.scale(font_size)
1626    }
1627
1628    #[inline]
1629    pub(crate) fn underline_position(&self, font_size: f32) -> f32 {
1630        self.underline_position as f32 * self.scale(font_size)
1631    }
1632
1633    #[inline]
1634    fn underline_thickness(&self, font_size: f32) -> f32 {
1635        self.underline_thickness.get() as f32 * self.scale(font_size)
1636    }
1637
1638    #[inline]
1639    pub(crate) fn line_through_position(&self, font_size: f32) -> f32 {
1640        self.line_through_position as f32 * self.scale(font_size)
1641    }
1642
1643    #[inline]
1644    fn subscript_offset(&self, font_size: f32) -> f32 {
1645        self.subscript_offset as f32 * self.scale(font_size)
1646    }
1647
1648    #[inline]
1649    fn superscript_offset(&self, font_size: f32) -> f32 {
1650        self.superscript_offset as f32 * self.scale(font_size)
1651    }
1652
1653    fn dominant_baseline_shift(&self, baseline: DominantBaseline, font_size: f32) -> f32 {
1654        let alignment = match baseline {
1655            DominantBaseline::Auto => AlignmentBaseline::Auto,
1656            DominantBaseline::UseScript => AlignmentBaseline::Auto, // unsupported
1657            DominantBaseline::NoChange => AlignmentBaseline::Auto,  // already resolved
1658            DominantBaseline::ResetSize => AlignmentBaseline::Auto, // unsupported
1659            DominantBaseline::Ideographic => AlignmentBaseline::Ideographic,
1660            DominantBaseline::Alphabetic => AlignmentBaseline::Alphabetic,
1661            DominantBaseline::Hanging => AlignmentBaseline::Hanging,
1662            DominantBaseline::Mathematical => AlignmentBaseline::Mathematical,
1663            DominantBaseline::Central => AlignmentBaseline::Central,
1664            DominantBaseline::Middle => AlignmentBaseline::Middle,
1665            DominantBaseline::TextAfterEdge => AlignmentBaseline::TextAfterEdge,
1666            DominantBaseline::TextBeforeEdge => AlignmentBaseline::TextBeforeEdge,
1667        };
1668
1669        self.alignment_baseline_shift(alignment, font_size)
1670    }
1671
1672    // The `alignment-baseline` property is a mess.
1673    //
1674    // The SVG 1.1 spec (https://www.w3.org/TR/SVG11/text.html#BaselineAlignmentProperties)
1675    // goes on and on about what this property suppose to do, but doesn't actually explain
1676    // how it should be implemented. It's just a very verbose overview.
1677    //
1678    // As of Nov 2022, only Chrome and Safari support `alignment-baseline`. Firefox isn't.
1679    // Same goes for basically every SVG library in existence.
1680    // Meaning we have no idea how exactly it should be implemented.
1681    //
1682    // And even Chrome and Safari cannot agree on how to handle `baseline`, `after-edge`,
1683    // `text-after-edge` and `ideographic` variants. Producing vastly different output.
1684    //
1685    // As per spec, a proper implementation should get baseline values from the font itself,
1686    // using `BASE` and `bsln` TrueType tables. If those tables are not present,
1687    // we have to synthesize them (https://drafts.csswg.org/css-inline/#baseline-synthesis-fonts).
1688    // And in the worst case scenario simply fallback to hardcoded values.
1689    //
1690    // Also, most fonts do not provide `BASE` and `bsln` tables to begin with.
1691    //
1692    // Again, as of Nov 2022, Chrome does only the latter:
1693    // https://github.com/chromium/chromium/blob/main/third_party/blink/renderer/platform/fonts/font_metrics.cc#L153
1694    //
1695    // Since baseline TrueType tables parsing and baseline synthesis are pretty hard,
1696    // we do what Chrome does - use hardcoded values. And it seems like Safari does the same.
1697    //
1698    //
1699    // But that's not all! SVG 2 and CSS Inline Layout 3 did a baseline handling overhaul,
1700    // and it's far more complex now. Not sure if anyone actually supports it.
1701    fn alignment_baseline_shift(&self, alignment: AlignmentBaseline, font_size: f32) -> f32 {
1702        match alignment {
1703            AlignmentBaseline::Auto => 0.0,
1704            AlignmentBaseline::Baseline => 0.0,
1705            AlignmentBaseline::BeforeEdge | AlignmentBaseline::TextBeforeEdge => {
1706                self.ascent(font_size)
1707            }
1708            AlignmentBaseline::Middle => self.x_height(font_size) * 0.5,
1709            AlignmentBaseline::Central => self.ascent(font_size) - self.height(font_size) * 0.5,
1710            AlignmentBaseline::AfterEdge | AlignmentBaseline::TextAfterEdge => {
1711                self.descent(font_size)
1712            }
1713            AlignmentBaseline::Ideographic => self.descent(font_size),
1714            AlignmentBaseline::Alphabetic => 0.0,
1715            AlignmentBaseline::Hanging => self.ascent(font_size) * 0.8,
1716            AlignmentBaseline::Mathematical => self.ascent(font_size) * 0.5,
1717        }
1718    }
1719}
1720
1721pub(crate) type FontsCache = HashMap<Font, Arc<ResolvedFont>>;
1722
1723/// A read-only text index in bytes.
1724///
1725/// Guarantee to be on a char boundary and in text bounds.
1726#[derive(Clone, Copy, PartialEq, Debug)]
1727pub(crate) struct ByteIndex(usize);
1728
1729impl ByteIndex {
1730    fn new(i: usize) -> Self {
1731        ByteIndex(i)
1732    }
1733
1734    pub(crate) fn value(&self) -> usize {
1735        self.0
1736    }
1737
1738    /// Converts byte position into a code point position.
1739    pub(crate) fn code_point_at(&self, text: &str) -> usize {
1740        text.char_indices()
1741            .take_while(|(i, _)| *i != self.0)
1742            .count()
1743    }
1744
1745    /// Converts byte position into a character.
1746    pub(crate) fn char_from(&self, text: &str) -> char {
1747        text[self.0..].chars().next().unwrap()
1748    }
1749}