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