Skip to main content

usvg/text/
layout.rs

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