Skip to main content

ai_usvg/text/
layout.rs

1// Copyright 2022 the Resvg Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use alloc::string::String;
5use alloc::string::ToString;
6use alloc::sync::Arc;
7use alloc::vec;
8use alloc::vec::Vec;
9use core::num::NonZeroU16;
10use hashbrown::{HashMap, HashSet};
11
12use fontdb::{Database, ID};
13use kurbo::{ParamCurve, ParamCurveArclen, ParamCurveDeriv};
14use rustybuzz::ttf_parser;
15use rustybuzz::ttf_parser::{GlyphId, Tag};
16use strict_num::NonZeroPositiveF32;
17use tiny_skia_path::{NonZeroRect, Transform};
18use unicode_script::UnicodeScript;
19
20use crate::tree::{BBox, IsValidLength};
21use crate::{
22    AlignmentBaseline, ApproxZeroUlps, BaselineShift, DominantBaseline, Fill, FillRule, Font,
23    FontResolver, LengthAdjust, PaintOrder, Path, ShapeRendering, Stroke, Text, TextAnchor,
24    TextChunk, TextDecorationStyle, TextFlow, TextPath, TextSpan, WritingMode,
25};
26
27/// A glyph that has already been positioned correctly.
28///
29/// Note that the transform already takes the font size into consideration, so applying the
30/// transform to the outline of the glyphs is all that is necessary to display it correctly.
31#[derive(Clone, Debug)]
32pub struct PositionedGlyph {
33    /// Returns the transform of the glyph itself within the cluster. For example,
34    /// for zalgo text, it contains the transform to position the glyphs above/below
35    /// the main glyph.
36    glyph_ts: Transform,
37    /// Returns the transform of the whole cluster that the glyph is part of.
38    cluster_ts: Transform,
39    /// Returns the transform of the span that the glyph is a part of.
40    span_ts: Transform,
41    /// The units per em of the font the glyph belongs to.
42    units_per_em: u16,
43    /// The font size the glyph should be scaled to.
44    font_size: f32,
45    /// The ID of the glyph.
46    pub id: GlyphId,
47    /// The text from the original string that corresponds to that glyph.
48    pub text: String,
49    /// The ID of the font the glyph should be taken from. Can be used with the
50    /// [font database of the tree](crate::Tree::fontdb) this glyph is part of.
51    pub font: ID,
52}
53
54impl PositionedGlyph {
55    /// Returns the font size for this glyph.
56    pub fn font_size(&self) -> f32 {
57        self.font_size
58    }
59
60    /// Returns the transform of glyph.
61    pub fn transform(&self) -> Transform {
62        let sx = self.font_size / self.units_per_em as f32;
63
64        self.span_ts
65            .pre_concat(self.cluster_ts)
66            .pre_concat(Transform::from_scale(sx, sx))
67            .pre_concat(self.glyph_ts)
68    }
69
70    /// Returns the transform of glyph, assuming that an outline
71    /// glyph is being used (i.e. from the `glyf` or `CFF/CFF2` table).
72    pub fn outline_transform(&self) -> Transform {
73        // Outlines are mirrored by default.
74        self.transform()
75            .pre_concat(Transform::from_scale(1.0, -1.0))
76    }
77
78    /// Returns the transform for the glyph, assuming that a CBTD-based raster glyph
79    /// is being used.
80    pub fn cbdt_transform(&self, x: f32, y: f32, pixels_per_em: f32, height: f32) -> Transform {
81        self.transform()
82            .pre_concat(Transform::from_scale(
83                self.units_per_em as f32 / pixels_per_em,
84                self.units_per_em as f32 / pixels_per_em,
85            ))
86            // Right now, the top-left corner of the image would be placed in
87            // on the "text cursor", but we want the bottom-left corner to be there,
88            // so we need to shift it up and also apply the x/y offset.
89            .pre_translate(x, -height - y)
90    }
91
92    /// Returns the transform for the glyph, assuming that a sbix-based raster glyph
93    /// is being used.
94    pub fn sbix_transform(
95        &self,
96        x: f32,
97        y: f32,
98        x_min: f32,
99        y_min: f32,
100        pixels_per_em: f32,
101        height: f32,
102    ) -> Transform {
103        // In contrast to CBDT, we also need to look at the outline bbox of the glyph and add a shift if necessary.
104        let bbox_x_shift = -x_min;
105
106        let bbox_y_shift = if y_min.approx_zero_ulps(4) {
107            // For unknown reasons, using Apple Color Emoji will lead to a vertical shift on MacOS, but this shift
108            // doesn't seem to be coming from the font and most likely is somehow hardcoded. On Windows,
109            // this shift will not be applied. However, if this shift is not applied the emojis are a bit
110            // too high up when being together with other text, so we try to imitate this.
111            // See also https://github.com/harfbuzz/harfbuzz/issues/2679#issuecomment-1345595425
112            // So whenever the y-shift is 0, we approximate this vertical shift that seems to be produced by it.
113            // This value seems to be pretty close to what is happening on MacOS.
114            // We can still remove this if it turns out to be a problem, but Apple Color Emoji is pretty
115            // much the only `sbix` font out there and they all seem to have a y-shift of 0, so it
116            // makes sense to keep it.
117            0.128 * self.units_per_em as f32
118        } else {
119            -y_min
120        };
121
122        self.transform()
123            .pre_concat(Transform::from_translate(bbox_x_shift, bbox_y_shift))
124            .pre_concat(Transform::from_scale(
125                self.units_per_em as f32 / pixels_per_em,
126                self.units_per_em as f32 / pixels_per_em,
127            ))
128            // Right now, the top-left corner of the image would be placed in
129            // on the "text cursor", but we want the bottom-left corner to be there,
130            // so we need to shift it up and also apply the x/y offset.
131            .pre_translate(x, -height - y)
132    }
133
134    /// Returns the transform for the glyph, assuming that an SVG glyph is
135    /// being used.
136    pub fn svg_transform(&self) -> Transform {
137        self.transform()
138    }
139
140    /// Returns the transform for the glyph, assuming that a COLR glyph is
141    /// being used.
142    pub fn colr_transform(&self) -> Transform {
143        self.outline_transform()
144    }
145}
146
147/// A span contains a number of layouted glyphs that share the same fill, stroke, paint order and
148/// visibility.
149#[derive(Clone, Debug)]
150pub struct Span {
151    /// The fill of the span.
152    pub fill: Option<Fill>,
153    /// The stroke of the span.
154    pub stroke: Option<Stroke>,
155    /// The paint order of the span.
156    pub paint_order: PaintOrder,
157    /// The font size of the span.
158    pub font_size: NonZeroPositiveF32,
159    /// Font variation settings for variable fonts.
160    pub variations: Vec<crate::FontVariation>,
161    /// Font optical sizing mode.
162    pub font_optical_sizing: crate::FontOpticalSizing,
163    /// The visibility of the span.
164    pub visible: bool,
165    /// The glyphs that make up the span.
166    pub positioned_glyphs: Vec<PositionedGlyph>,
167    /// An underline text decoration of the span.
168    /// Needs to be rendered before all glyphs.
169    pub underline: Option<Path>,
170    /// An overline text decoration of the span.
171    /// Needs to be rendered before all glyphs.
172    pub overline: Option<Path>,
173    /// A line-through text decoration of the span.
174    /// Needs to be rendered after all glyphs.
175    pub line_through: Option<Path>,
176}
177
178#[derive(Clone, Debug)]
179struct GlyphCluster {
180    byte_idx: ByteIndex,
181    codepoint: char,
182    width: f32,
183    advance: 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) =
214                    (resolver.select_font)(&span.font, fontdb).and_then(|id| fontdb.load_font(id))
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                core::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_clusters_positions_horizontal(
550    text: &Text,
551    chunk: &TextChunk,
552    offset: usize,
553    writing_mode: WritingMode,
554    clusters: &mut [GlyphCluster],
555) -> (f32, f32) {
556    let mut x = process_anchor(chunk.anchor, clusters_length(clusters));
557    let mut y = 0.0;
558
559    for cluster in clusters {
560        let cp = offset + cluster.byte_idx.code_point_at(&chunk.text);
561        if let (Some(dx), Some(dy)) = (text.dx.get(cp), text.dy.get(cp)) {
562            if writing_mode == WritingMode::LeftToRight {
563                x += dx;
564                y += dy;
565            } else {
566                y -= dx;
567                x += dy;
568            }
569            cluster.has_relative_shift = !dx.approx_zero_ulps(4) || !dy.approx_zero_ulps(4);
570        }
571
572        cluster.transform = cluster.transform.pre_translate(x, y);
573
574        if let Some(angle) = text.rotate.get(cp).cloned() {
575            if !angle.approx_zero_ulps(4) {
576                cluster.transform = cluster.transform.pre_rotate(angle);
577                cluster.has_relative_shift = true;
578            }
579        }
580
581        x += cluster.advance;
582    }
583
584    (x, y)
585}
586
587// Baseline resolving in SVG is a mess.
588// Not only it's poorly documented, but as soon as you start mixing
589// `dominant-baseline` and `alignment-baseline` each application/browser will produce
590// different results.
591//
592// For now, resvg simply tries to match Chrome's output and not the mythical SVG spec output.
593//
594// See `alignment_baseline_shift` method comment for more details.
595pub(crate) fn resolve_baseline(
596    span: &TextSpan,
597    font: &ResolvedFont,
598    writing_mode: WritingMode,
599) -> f32 {
600    let mut shift = -resolve_baseline_shift(&span.baseline_shift, font, span.font_size.get());
601
602    // TODO: support vertical layout as well
603    if writing_mode == WritingMode::LeftToRight {
604        if span.alignment_baseline == AlignmentBaseline::Auto
605            || span.alignment_baseline == AlignmentBaseline::Baseline
606        {
607            shift += font.dominant_baseline_shift(span.dominant_baseline, span.font_size.get());
608        } else {
609            shift += font.alignment_baseline_shift(span.alignment_baseline, span.font_size.get());
610        }
611    }
612
613    shift
614}
615
616fn resolve_baseline_shift(baselines: &[BaselineShift], font: &ResolvedFont, font_size: f32) -> f32 {
617    let mut shift = 0.0;
618    for baseline in baselines.iter().rev() {
619        match baseline {
620            BaselineShift::Baseline => {}
621            BaselineShift::Subscript => shift -= font.subscript_offset(font_size),
622            BaselineShift::Superscript => shift += font.superscript_offset(font_size),
623            BaselineShift::Number(n) => shift += n,
624        }
625    }
626
627    shift
628}
629
630fn resolve_clusters_positions_path(
631    text: &Text,
632    chunk: &TextChunk,
633    char_offset: usize,
634    path: &TextPath,
635    writing_mode: WritingMode,
636    fonts_cache: &FontsCache,
637    clusters: &mut [GlyphCluster],
638) -> (f32, f32) {
639    let mut last_x = 0.0;
640    let mut last_y = 0.0;
641
642    let mut dy = 0.0;
643
644    // In the text path mode, chunk's x/y coordinates provide an additional offset along the path.
645    // The X coordinate is used in a horizontal mode, and Y in vertical.
646    let chunk_offset = match writing_mode {
647        WritingMode::LeftToRight => chunk.x.unwrap_or(0.0),
648        WritingMode::TopToBottom => chunk.y.unwrap_or(0.0),
649    };
650
651    let start_offset =
652        chunk_offset + path.start_offset + process_anchor(chunk.anchor, clusters_length(clusters));
653
654    let normals = collect_normals(text, chunk, clusters, &path.path, char_offset, start_offset);
655    for (cluster, normal) in clusters.iter_mut().zip(normals) {
656        let (x, y, angle) = match normal {
657            Some(normal) => (normal.x, normal.y, normal.angle),
658            None => {
659                // Hide clusters that are outside the text path.
660                cluster.visible = false;
661                continue;
662            }
663        };
664
665        // We have to break a decoration line for each cluster during text-on-path.
666        cluster.has_relative_shift = true;
667
668        let orig_ts = cluster.transform;
669
670        // Clusters should be rotated by the x-midpoint x baseline position.
671        let half_width = cluster.width / 2.0;
672        cluster.transform = Transform::default();
673        cluster.transform = cluster.transform.pre_translate(x - half_width, y);
674        cluster.transform = cluster.transform.pre_rotate_at(angle, half_width, 0.0);
675
676        let cp = char_offset + cluster.byte_idx.code_point_at(&chunk.text);
677        dy += text.dy.get(cp).cloned().unwrap_or(0.0);
678
679        let baseline_shift = chunk_span_at(chunk, cluster.byte_idx)
680            .map(|span| {
681                let font = match fonts_cache.get(&span.font) {
682                    Some(v) => v,
683                    None => return 0.0,
684                };
685                -resolve_baseline(span, font, writing_mode)
686            })
687            .unwrap_or(0.0);
688
689        // Shift only by `dy` since we already applied `dx`
690        // during offset along the path calculation.
691        if !dy.approx_zero_ulps(4) || !baseline_shift.approx_zero_ulps(4) {
692            let shift = kurbo::Vec2::new(0.0, (dy - baseline_shift) as f64);
693            cluster.transform = cluster
694                .transform
695                .pre_translate(shift.x as f32, shift.y as f32);
696        }
697
698        if let Some(angle) = text.rotate.get(cp).cloned() {
699            if !angle.approx_zero_ulps(4) {
700                cluster.transform = cluster.transform.pre_rotate(angle);
701            }
702        }
703
704        // The possible `lengthAdjust` transform should be applied after text-on-path positioning.
705        cluster.transform = cluster.transform.pre_concat(orig_ts);
706
707        last_x = x + cluster.advance;
708        last_y = y;
709    }
710
711    (last_x, last_y)
712}
713
714pub(crate) fn process_anchor(a: TextAnchor, text_width: f32) -> f32 {
715    match a {
716        TextAnchor::Start => 0.0, // Nothing.
717        TextAnchor::Middle => -text_width / 2.0,
718        TextAnchor::End => -text_width,
719    }
720}
721
722pub(crate) struct PathNormal {
723    pub(crate) x: f32,
724    pub(crate) y: f32,
725    pub(crate) angle: f32,
726}
727
728fn collect_normals(
729    text: &Text,
730    chunk: &TextChunk,
731    clusters: &[GlyphCluster],
732    path: &tiny_skia_path::Path,
733    char_offset: usize,
734    offset: f32,
735) -> Vec<Option<PathNormal>> {
736    let mut offsets = Vec::with_capacity(clusters.len());
737    let mut normals = Vec::with_capacity(clusters.len());
738    {
739        let mut advance = offset;
740        for cluster in clusters {
741            // Clusters should be rotated by the x-midpoint x baseline position.
742            let half_width = cluster.width / 2.0;
743
744            // Include relative position.
745            let cp = char_offset + cluster.byte_idx.code_point_at(&chunk.text);
746            advance += text.dx.get(cp).cloned().unwrap_or(0.0);
747
748            let offset = advance + half_width;
749
750            // Clusters outside the path have no normals.
751            if offset < 0.0 {
752                normals.push(None);
753            }
754
755            offsets.push(offset as f64);
756            advance += cluster.advance;
757        }
758    }
759
760    let mut prev_mx = path.points()[0].x;
761    let mut prev_my = path.points()[0].y;
762    let mut prev_x = prev_mx;
763    let mut prev_y = prev_my;
764
765    fn create_curve_from_line(px: f32, py: f32, x: f32, y: f32) -> kurbo::CubicBez {
766        let line = kurbo::Line::new(
767            kurbo::Point::new(px as f64, py as f64),
768            kurbo::Point::new(x as f64, y as f64),
769        );
770        let p1 = line.eval(0.33);
771        let p2 = line.eval(0.66);
772        kurbo::CubicBez {
773            p0: line.p0,
774            p1,
775            p2,
776            p3: line.p1,
777        }
778    }
779
780    let mut length: f64 = 0.0;
781    for seg in path.segments() {
782        let curve = match seg {
783            tiny_skia_path::PathSegment::MoveTo(p) => {
784                prev_mx = p.x;
785                prev_my = p.y;
786                prev_x = p.x;
787                prev_y = p.y;
788                continue;
789            }
790            tiny_skia_path::PathSegment::LineTo(p) => {
791                create_curve_from_line(prev_x, prev_y, p.x, p.y)
792            }
793            tiny_skia_path::PathSegment::QuadTo(p1, p) => kurbo::QuadBez {
794                p0: kurbo::Point::new(prev_x as f64, prev_y as f64),
795                p1: kurbo::Point::new(p1.x as f64, p1.y as f64),
796                p2: kurbo::Point::new(p.x as f64, p.y as f64),
797            }
798            .raise(),
799            tiny_skia_path::PathSegment::CubicTo(p1, p2, p) => kurbo::CubicBez {
800                p0: kurbo::Point::new(prev_x as f64, prev_y as f64),
801                p1: kurbo::Point::new(p1.x as f64, p1.y as f64),
802                p2: kurbo::Point::new(p2.x as f64, p2.y as f64),
803                p3: kurbo::Point::new(p.x as f64, p.y as f64),
804            },
805            tiny_skia_path::PathSegment::Close => {
806                create_curve_from_line(prev_x, prev_y, prev_mx, prev_my)
807            }
808        };
809
810        let arclen_accuracy = {
811            let base_arclen_accuracy = 0.5;
812            // Accuracy depends on a current scale.
813            // When we have a tiny path scaled by a large value,
814            // we have to increase out accuracy accordingly.
815            let (sx, sy) = text.abs_transform.get_scale();
816            // 1.0 acts as a threshold to prevent division by 0 and/or low accuracy.
817            base_arclen_accuracy / (sx * sy).sqrt().max(1.0)
818        };
819
820        let curve_len = curve.arclen(arclen_accuracy as f64);
821
822        for offset in &offsets[normals.len()..] {
823            if *offset >= length && *offset <= length + curve_len {
824                let mut offset = curve.inv_arclen(offset - length, arclen_accuracy as f64);
825                // some rounding error may occur, so we give offset a little tolerance
826                debug_assert!((-1.0e-3..=1.0 + 1.0e-3).contains(&offset));
827                offset = offset.clamp(0.0, 1.0);
828
829                let pos = curve.eval(offset);
830                let d = curve.deriv().eval(offset);
831                let d = kurbo::Vec2::new(-d.y, d.x); // tangent
832                let angle = d.atan2().to_degrees() - 90.0;
833
834                normals.push(Some(PathNormal {
835                    x: pos.x as f32,
836                    y: pos.y as f32,
837                    angle: angle as f32,
838                }));
839
840                if normals.len() == offsets.len() {
841                    break;
842                }
843            }
844        }
845
846        length += curve_len;
847        prev_x = curve.p3.x as f32;
848        prev_y = curve.p3.y as f32;
849    }
850
851    // If path ended and we still have unresolved normals - set them to `None`.
852    for _ in 0..(offsets.len() - normals.len()) {
853        normals.push(None);
854    }
855
856    normals
857}
858
859/// Converts a text chunk into a list of outlined clusters.
860///
861/// This function will do the BIDI reordering, text shaping and glyphs outlining,
862/// but not the text layouting. So all clusters are in the 0x0 position.
863fn process_chunk(
864    chunk: &TextChunk,
865    fonts_cache: &FontsCache,
866    resolver: &FontResolver,
867    fontdb: &mut Arc<fontdb::Database>,
868) -> Vec<GlyphCluster> {
869    // The way this function works is a bit tricky.
870    //
871    // The first problem is BIDI reordering.
872    // We cannot shape text span-by-span, because glyph clusters are not guarantee to be continuous.
873    //
874    // For example:
875    // <text>Hel<tspan fill="url(#lg1)">lo של</tspan>ום.</text>
876    //
877    // Would be shaped as:
878    // H e l l o   ש ל  ו  ם .   (characters)
879    // 0 1 2 3 4 5 12 10 8 6 14  (cluster indices in UTF-8)
880    //       ---         ---     (green span)
881    //
882    // As you can see, our continuous `lo של` span was split into two separated one.
883    // So our 3 spans: black - green - black, become 5 spans: black - green - black - green - black.
884    // If we shape `Hel`, then `lo של` an then `ום` separately - we would get an incorrect output.
885    // To properly handle this we simply shape the whole chunk.
886    //
887    // But this introduces another issue - what to do when we have multiple fonts?
888    // The easy solution would be to simply shape text with each font,
889    // where the first font output is used as a base one and all others overwrite it.
890    // This way in case of:
891    // <text font-family="Arial">Hello <tspan font-family="Helvetica">world</tspan></text>
892    // we would replace Arial glyphs for `world` with Helvetica one. Pretty simple.
893    //
894    // Well, it would work most of the time, but not always.
895    // This is because different fonts can produce different amount of glyphs for the same text.
896    // The most common example are ligatures. Some fonts can shape `fi` as two glyphs `f` and `i`,
897    // but some can use `fi` (U+FB01) instead.
898    // Meaning that during merging we have to overwrite not individual glyphs, but clusters.
899
900    // Glyph splitting assigns distinct glyphs to the same index in the original text, we need to
901    // store previously used indices to make sure we do not re-use the same index while overwriting
902    // span glyphs.
903    let mut positions = HashSet::new();
904
905    let mut glyphs = Vec::new();
906    for span in &chunk.spans {
907        let font = match fonts_cache.get(&span.font) {
908            Some(v) => v.clone(),
909            None => continue,
910        };
911
912        let tmp_glyphs = shape_text(
913            &chunk.text,
914            font,
915            span.small_caps,
916            span.apply_kerning,
917            &span.font.variations,
918            span.font_size.get(),
919            span.font_optical_sizing,
920            resolver,
921            fontdb,
922        );
923
924        // Do nothing with the first run.
925        if glyphs.is_empty() {
926            glyphs = tmp_glyphs;
927            continue;
928        }
929
930        positions.clear();
931
932        // Overwrite span's glyphs.
933        let mut iter = tmp_glyphs.into_iter();
934        while let Some(new_glyph) = iter.next() {
935            if !span_contains(span, new_glyph.byte_idx) {
936                continue;
937            }
938
939            let Some(idx) = glyphs
940                .iter()
941                .position(|g| g.byte_idx == new_glyph.byte_idx)
942                .filter(|pos| !positions.contains(pos))
943            else {
944                continue;
945            };
946
947            positions.insert(idx);
948
949            let prev_cluster_len = glyphs[idx].cluster_len;
950            if prev_cluster_len < new_glyph.cluster_len {
951                // If the new font represents the same cluster with fewer glyphs
952                // then remove remaining glyphs.
953                for _ in 1..new_glyph.cluster_len {
954                    glyphs.remove(idx + 1);
955                }
956            } else if prev_cluster_len > new_glyph.cluster_len {
957                // If the new font represents the same cluster with more glyphs
958                // then insert them after the current one.
959                for j in 1..prev_cluster_len {
960                    if let Some(g) = iter.next() {
961                        glyphs.insert(idx + j, g);
962                    }
963                }
964            }
965
966            glyphs[idx] = new_glyph;
967        }
968    }
969
970    // Convert glyphs to clusters.
971    let mut clusters = Vec::new();
972    for (range, byte_idx) in GlyphClusters::new(&glyphs) {
973        if let Some(span) = chunk_span_at(chunk, byte_idx) {
974            clusters.push(form_glyph_clusters(
975                &glyphs[range],
976                &chunk.text,
977                span.font_size.get(),
978            ));
979        }
980    }
981
982    clusters
983}
984
985fn apply_length_adjust(chunk: &TextChunk, clusters: &mut [GlyphCluster]) {
986    let is_horizontal = matches!(chunk.text_flow, TextFlow::Linear);
987
988    for span in &chunk.spans {
989        let target_width = match span.text_length {
990            Some(v) => v,
991            None => continue,
992        };
993
994        let mut width = 0.0;
995        let mut cluster_indexes = Vec::new();
996        for i in span.start..span.end {
997            if let Some(index) = clusters.iter().position(|c| c.byte_idx.value() == i) {
998                cluster_indexes.push(index);
999            }
1000        }
1001        // Complex scripts can have multi-codepoint clusters therefore we have to remove duplicates.
1002        cluster_indexes.sort();
1003        cluster_indexes.dedup();
1004
1005        for i in &cluster_indexes {
1006            // Use the original cluster `width` and not `advance`.
1007            // This method essentially discards any `word-spacing` and `letter-spacing`.
1008            width += clusters[*i].width;
1009        }
1010
1011        if cluster_indexes.is_empty() {
1012            continue;
1013        }
1014
1015        if span.length_adjust == LengthAdjust::Spacing {
1016            let factor = if cluster_indexes.len() > 1 {
1017                (target_width - width) / (cluster_indexes.len() - 1) as f32
1018            } else {
1019                0.0
1020            };
1021
1022            for i in cluster_indexes {
1023                clusters[i].advance = clusters[i].width + factor;
1024            }
1025        } else {
1026            let factor = target_width / width;
1027            // Prevent multiplying by zero.
1028            if factor < 0.001 {
1029                continue;
1030            }
1031
1032            for i in cluster_indexes {
1033                clusters[i].transform = clusters[i].transform.pre_scale(factor, 1.0);
1034
1035                // Technically just a hack to support the current text-on-path algorithm.
1036                if !is_horizontal {
1037                    clusters[i].advance *= factor;
1038                    clusters[i].width *= factor;
1039                }
1040            }
1041        }
1042    }
1043}
1044
1045/// Rotates clusters according to
1046/// [Unicode Vertical_Orientation Property](https://www.unicode.org/reports/tr50/tr50-19.html).
1047fn apply_writing_mode(writing_mode: WritingMode, clusters: &mut [GlyphCluster]) {
1048    if writing_mode != WritingMode::TopToBottom {
1049        return;
1050    }
1051
1052    for cluster in clusters {
1053        let orientation = unicode_vo::char_orientation(cluster.codepoint);
1054        if orientation == unicode_vo::Orientation::Upright {
1055            let mut ts = Transform::default();
1056            // Position glyph in the center of vertical axis.
1057            ts = ts.pre_translate(0.0, (cluster.ascent + cluster.descent) / 2.0);
1058            // Rotate by 90 degrees in the center.
1059            ts = ts.pre_rotate_at(
1060                -90.0,
1061                cluster.width / 2.0,
1062                -(cluster.ascent + cluster.descent) / 2.0,
1063            );
1064
1065            cluster.path_transform = ts;
1066
1067            // Move "baseline" to the middle and make height equal to width.
1068            cluster.ascent = cluster.width / 2.0;
1069            cluster.descent = -cluster.width / 2.0;
1070        } else {
1071            // Could not find a spec that explains this,
1072            // but this is how other applications are shifting the "rotated" characters
1073            // in the top-to-bottom mode.
1074            cluster.transform = cluster
1075                .transform
1076                .pre_translate(0.0, (cluster.ascent + cluster.descent) / 2.0);
1077        }
1078    }
1079}
1080
1081/// Applies the `letter-spacing` property to a text chunk clusters.
1082///
1083/// [In the CSS spec](https://www.w3.org/TR/css-text-3/#letter-spacing-property).
1084fn apply_letter_spacing(chunk: &TextChunk, clusters: &mut [GlyphCluster]) {
1085    // At least one span should have a non-zero spacing.
1086    if !chunk
1087        .spans
1088        .iter()
1089        .any(|span| !span.letter_spacing.approx_zero_ulps(4))
1090    {
1091        return;
1092    }
1093
1094    let num_clusters = clusters.len();
1095    for (i, cluster) in clusters.iter_mut().enumerate() {
1096        // Spacing must be applied only to characters that belongs to the script
1097        // that supports spacing.
1098        // We are checking only the first code point, since it should be enough.
1099        // https://www.w3.org/TR/css-text-3/#cursive-tracking
1100        let script = cluster.codepoint.script();
1101        if script_supports_letter_spacing(script) {
1102            if let Some(span) = chunk_span_at(chunk, cluster.byte_idx) {
1103                // A space after the last cluster should be ignored,
1104                // since it affects the bbox and text alignment.
1105                if i != num_clusters - 1 {
1106                    cluster.advance += span.letter_spacing;
1107                }
1108
1109                // If the cluster advance became negative - clear it.
1110                // This is an UB so we can do whatever we want, and we mimic Chrome's behavior.
1111                if !cluster.advance.is_valid_length() {
1112                    cluster.width = 0.0;
1113                    cluster.advance = 0.0;
1114                    cluster.glyphs = vec![];
1115                }
1116            }
1117        }
1118    }
1119}
1120
1121/// Applies the `word-spacing` property to a text chunk clusters.
1122///
1123/// [In the CSS spec](https://www.w3.org/TR/css-text-3/#propdef-word-spacing).
1124fn apply_word_spacing(chunk: &TextChunk, clusters: &mut [GlyphCluster]) {
1125    // At least one span should have a non-zero spacing.
1126    if !chunk
1127        .spans
1128        .iter()
1129        .any(|span| !span.word_spacing.approx_zero_ulps(4))
1130    {
1131        return;
1132    }
1133
1134    for cluster in clusters {
1135        if is_word_separator_characters(cluster.codepoint) {
1136            if let Some(span) = chunk_span_at(chunk, cluster.byte_idx) {
1137                // Technically, word spacing 'should be applied half on each
1138                // side of the character', but it doesn't affect us in any way,
1139                // so we are ignoring this.
1140                cluster.advance += span.word_spacing;
1141
1142                // After word spacing, `advance` can be negative.
1143            }
1144        }
1145    }
1146}
1147
1148fn form_glyph_clusters(glyphs: &[Glyph], text: &str, font_size: f32) -> GlyphCluster {
1149    debug_assert!(!glyphs.is_empty());
1150
1151    let mut width = 0.0;
1152    let mut x: f32 = 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        if glyph_width > width {
1184            width = glyph_width;
1185        }
1186    }
1187
1188    let byte_idx = glyphs[0].byte_idx;
1189    let font = glyphs[0].font.clone();
1190    GlyphCluster {
1191        byte_idx,
1192        codepoint: byte_idx.char_from(text),
1193        width,
1194        advance: width,
1195        ascent: font.ascent(font_size),
1196        descent: font.descent(font_size),
1197        has_relative_shift: false,
1198        transform: Transform::default(),
1199        path_transform: Transform::default(),
1200        glyphs: positioned_glyphs,
1201        visible: true,
1202    }
1203}
1204
1205pub(crate) trait DatabaseExt {
1206    fn load_font(&self, id: ID) -> Option<ResolvedFont>;
1207    fn has_char(&self, id: ID, c: char) -> bool;
1208}
1209
1210impl DatabaseExt for Database {
1211    #[inline(never)]
1212    fn load_font(&self, id: ID) -> Option<ResolvedFont> {
1213        self.with_face_data(id, |data, face_index| -> Option<ResolvedFont> {
1214            let font = ttf_parser::Face::parse(data, face_index).ok()?;
1215
1216            let units_per_em = NonZeroU16::new(font.units_per_em())?;
1217
1218            let ascent = font.ascender();
1219            let descent = font.descender();
1220
1221            let x_height = font
1222                .x_height()
1223                .and_then(|x| u16::try_from(x).ok())
1224                .and_then(NonZeroU16::new);
1225            let x_height = match x_height {
1226                Some(height) => height,
1227                None => {
1228                    // If not set - fallback to height * 45%.
1229                    // 45% is what Firefox uses.
1230                    u16::try_from((f32::from(ascent - descent) * 0.45) as i32)
1231                        .ok()
1232                        .and_then(NonZeroU16::new)?
1233                }
1234            };
1235
1236            let line_through = font.strikeout_metrics();
1237            let line_through_position = match line_through {
1238                Some(metrics) => metrics.position,
1239                None => x_height.get() as i16 / 2,
1240            };
1241
1242            let (underline_position, underline_thickness) = match font.underline_metrics() {
1243                Some(metrics) => {
1244                    let thickness = u16::try_from(metrics.thickness)
1245                        .ok()
1246                        .and_then(NonZeroU16::new)
1247                        // `ttf_parser` guarantees that units_per_em is >= 16
1248                        .unwrap_or_else(|| NonZeroU16::new(units_per_em.get() / 12).unwrap());
1249
1250                    (metrics.position, thickness)
1251                }
1252                None => (
1253                    -(units_per_em.get() as i16) / 9,
1254                    NonZeroU16::new(units_per_em.get() / 12).unwrap(),
1255                ),
1256            };
1257
1258            // 0.2 and 0.4 are generic offsets used by some applications (Inkscape/librsvg).
1259            let mut subscript_offset = libm::roundf(units_per_em.get() as f32 / 0.2) as i16;
1260            let mut superscript_offset = libm::roundf(units_per_em.get() as f32 / 0.4) as i16;
1261            if let Some(metrics) = font.subscript_metrics() {
1262                subscript_offset = metrics.y_offset;
1263            }
1264
1265            if let Some(metrics) = font.superscript_metrics() {
1266                superscript_offset = metrics.y_offset;
1267            }
1268
1269            Some(ResolvedFont {
1270                id,
1271                units_per_em,
1272                ascent,
1273                descent,
1274                x_height,
1275                underline_position,
1276                underline_thickness,
1277                line_through_position,
1278                subscript_offset,
1279                superscript_offset,
1280            })
1281        })?
1282    }
1283
1284    #[inline(never)]
1285    fn has_char(&self, id: ID, c: char) -> bool {
1286        let res = self.with_face_data(id, |font_data, face_index| -> Option<bool> {
1287            let font = ttf_parser::Face::parse(font_data, face_index).ok()?;
1288            font.glyph_index(c)?;
1289            Some(true)
1290        });
1291
1292        res == Some(Some(true))
1293    }
1294}
1295
1296/// Text shaping with font fallback.
1297pub(crate) fn shape_text(
1298    text: &str,
1299    font: Arc<ResolvedFont>,
1300    small_caps: bool,
1301    apply_kerning: bool,
1302    variations: &[crate::FontVariation],
1303    font_size: f32,
1304    font_optical_sizing: crate::FontOpticalSizing,
1305    resolver: &FontResolver,
1306    fontdb: &mut Arc<fontdb::Database>,
1307) -> Vec<Glyph> {
1308    let mut glyphs = shape_text_with_font(
1309        text,
1310        font.clone(),
1311        small_caps,
1312        apply_kerning,
1313        variations,
1314        font_size,
1315        font_optical_sizing,
1316        fontdb,
1317    )
1318    .unwrap_or_default();
1319
1320    // Remember all fonts used for shaping.
1321    let mut used_fonts = vec![font.id];
1322
1323    // Loop until all glyphs become resolved or until no more fonts are left.
1324    'outer: loop {
1325        let mut missing = None;
1326        for glyph in &glyphs {
1327            if glyph.is_missing() {
1328                missing = Some(glyph.byte_idx.char_from(text));
1329                break;
1330            }
1331        }
1332
1333        if let Some(c) = missing {
1334            let fallback_font = match (resolver.select_fallback)(c, &used_fonts, fontdb)
1335                .and_then(|id| fontdb.load_font(id))
1336            {
1337                Some(v) => Arc::new(v),
1338                None => break 'outer,
1339            };
1340
1341            // Shape again, using a new font.
1342            let fallback_glyphs = shape_text_with_font(
1343                text,
1344                fallback_font.clone(),
1345                small_caps,
1346                apply_kerning,
1347                variations,
1348                font_size,
1349                font_optical_sizing,
1350                fontdb,
1351            )
1352            .unwrap_or_default();
1353
1354            let all_matched = fallback_glyphs.iter().all(|g| !g.is_missing());
1355            if all_matched {
1356                // Replace all glyphs when all of them were matched.
1357                glyphs = fallback_glyphs;
1358                break 'outer;
1359            }
1360
1361            // We assume, that shaping with an any font will produce the same amount of glyphs.
1362            // This is incorrect, but good enough for now.
1363            if glyphs.len() != fallback_glyphs.len() {
1364                break 'outer;
1365            }
1366
1367            // TODO: Replace clusters and not glyphs. This should be more accurate.
1368
1369            // Copy new glyphs.
1370            for i in 0..glyphs.len() {
1371                if glyphs[i].is_missing() && !fallback_glyphs[i].is_missing() {
1372                    glyphs[i] = fallback_glyphs[i].clone();
1373                }
1374            }
1375
1376            // Remember this font.
1377            used_fonts.push(fallback_font.id);
1378        } else {
1379            break 'outer;
1380        }
1381    }
1382
1383    // Warn about missing glyphs.
1384    for glyph in &glyphs {
1385        if glyph.is_missing() {
1386            let c = glyph.byte_idx.char_from(text);
1387            // TODO: print a full grapheme
1388            log::warn!(
1389                "No fonts with a {}/U+{:X} character were found.",
1390                c,
1391                c as u32
1392            );
1393        }
1394    }
1395
1396    glyphs
1397}
1398
1399/// Converts a text into a list of glyph IDs.
1400///
1401/// This function will do the BIDI reordering and text shaping.
1402fn shape_text_with_font(
1403    text: &str,
1404    font: Arc<ResolvedFont>,
1405    small_caps: bool,
1406    apply_kerning: bool,
1407    variations: &[crate::FontVariation],
1408    font_size: f32,
1409    font_optical_sizing: crate::FontOpticalSizing,
1410    fontdb: &fontdb::Database,
1411) -> Option<Vec<Glyph>> {
1412    fontdb.with_face_data(font.id, |font_data, face_index| -> Option<Vec<Glyph>> {
1413        let mut rb_font = rustybuzz::Face::from_slice(font_data, face_index)?;
1414
1415        // Build the list of variations to apply
1416        let mut final_variations: Vec<rustybuzz::Variation> = variations
1417            .iter()
1418            .map(|v| rustybuzz::Variation {
1419                tag: Tag::from_bytes(&v.tag),
1420                value: v.value,
1421            })
1422            .collect();
1423
1424        // Automatic optical sizing: if font-optical-sizing is auto and the font has
1425        // an 'opsz' axis that isn't explicitly set, auto-set it to match font size.
1426        // This matches browser behavior (CSS font-optical-sizing: auto).
1427        if font_optical_sizing == crate::FontOpticalSizing::Auto {
1428            let has_explicit_opsz = variations.iter().any(|v| v.tag == *b"opsz");
1429            if !has_explicit_opsz {
1430                // Check if font has opsz axis using the already parsed rb_font
1431                if let Some(axes) = rb_font.tables().fvar {
1432                    let has_opsz_axis = axes
1433                        .axes
1434                        .into_iter()
1435                        .any(|axis| axis.tag == ttf_parser::Tag::from_bytes(b"opsz"));
1436                    if has_opsz_axis {
1437                        final_variations.push(rustybuzz::Variation {
1438                            tag: Tag::from_bytes(b"opsz"),
1439                            value: font_size,
1440                        });
1441                    }
1442                }
1443            }
1444        }
1445
1446        // Apply font variations for variable fonts
1447        if !final_variations.is_empty() {
1448            rb_font.set_variations(&final_variations);
1449        }
1450
1451        let bidi_info = unicode_bidi::BidiInfo::new(text, Some(unicode_bidi::Level::ltr()));
1452        let paragraph = &bidi_info.paragraphs[0];
1453        let line = paragraph.range.clone();
1454
1455        let mut glyphs = Vec::new();
1456
1457        let (levels, runs) = bidi_info.visual_runs(paragraph, line);
1458        for run in runs.iter() {
1459            let sub_text = &text[run.clone()];
1460            if sub_text.is_empty() {
1461                continue;
1462            }
1463
1464            let ltr = levels[run.start].is_ltr();
1465            let hb_direction = if ltr {
1466                rustybuzz::Direction::LeftToRight
1467            } else {
1468                rustybuzz::Direction::RightToLeft
1469            };
1470
1471            let mut buffer = rustybuzz::UnicodeBuffer::new();
1472            buffer.push_str(sub_text);
1473            buffer.set_direction(hb_direction);
1474
1475            let mut features = Vec::new();
1476            if small_caps {
1477                features.push(rustybuzz::Feature::new(Tag::from_bytes(b"smcp"), 1, ..));
1478            }
1479
1480            if !apply_kerning {
1481                features.push(rustybuzz::Feature::new(Tag::from_bytes(b"kern"), 0, ..));
1482            }
1483
1484            let output = rustybuzz::shape(&rb_font, &features, buffer);
1485
1486            let positions = output.glyph_positions();
1487            let infos = output.glyph_infos();
1488
1489            for i in 0..output.len() {
1490                let pos = positions[i];
1491                let info = infos[i];
1492                let idx = run.start + info.cluster as usize;
1493
1494                let start = info.cluster as usize;
1495
1496                let end = if ltr {
1497                    i.checked_add(1)
1498                } else {
1499                    i.checked_sub(1)
1500                }
1501                .and_then(|last| infos.get(last))
1502                .map_or(sub_text.len(), |info| info.cluster as usize);
1503
1504                glyphs.push(Glyph {
1505                    byte_idx: ByteIndex::new(idx),
1506                    cluster_len: end.checked_sub(start).unwrap_or(0), // TODO: can fail?
1507                    text: sub_text[start..end].to_string(),
1508                    id: GlyphId(info.glyph_id as u16),
1509                    dx: pos.x_offset,
1510                    dy: pos.y_offset,
1511                    width: pos.x_advance,
1512                    font: font.clone(),
1513                });
1514            }
1515        }
1516
1517        Some(glyphs)
1518    })?
1519}
1520
1521/// An iterator over glyph clusters.
1522///
1523/// Input:  0 2 2 2 3 4 4 5 5
1524/// Result: 0 1     4 5   7
1525pub(crate) struct GlyphClusters<'a> {
1526    data: &'a [Glyph],
1527    idx: usize,
1528}
1529
1530impl<'a> GlyphClusters<'a> {
1531    pub(crate) fn new(data: &'a [Glyph]) -> Self {
1532        GlyphClusters { data, idx: 0 }
1533    }
1534}
1535
1536impl Iterator for GlyphClusters<'_> {
1537    type Item = (core::ops::Range<usize>, ByteIndex);
1538
1539    fn next(&mut self) -> Option<Self::Item> {
1540        if self.idx == self.data.len() {
1541            return None;
1542        }
1543
1544        let start = self.idx;
1545        let cluster = self.data[self.idx].byte_idx;
1546        for g in &self.data[self.idx..] {
1547            if g.byte_idx != cluster {
1548                break;
1549            }
1550
1551            self.idx += 1;
1552        }
1553
1554        Some((start..self.idx, cluster))
1555    }
1556}
1557
1558/// Checks that selected script supports letter spacing.
1559///
1560/// [In the CSS spec](https://www.w3.org/TR/css-text-3/#cursive-tracking).
1561///
1562/// The list itself is from: https://github.com/harfbuzz/harfbuzz/issues/64
1563pub(crate) fn script_supports_letter_spacing(script: unicode_script::Script) -> bool {
1564    use unicode_script::Script;
1565
1566    !matches!(
1567        script,
1568        Script::Arabic
1569            | Script::Syriac
1570            | Script::Nko
1571            | Script::Manichaean
1572            | Script::Psalter_Pahlavi
1573            | Script::Mandaic
1574            | Script::Mongolian
1575            | Script::Phags_Pa
1576            | Script::Devanagari
1577            | Script::Bengali
1578            | Script::Gurmukhi
1579            | Script::Modi
1580            | Script::Sharada
1581            | Script::Syloti_Nagri
1582            | Script::Tirhuta
1583            | Script::Ogham
1584    )
1585}
1586
1587/// A glyph.
1588///
1589/// Basically, a glyph ID and it's metrics.
1590#[derive(Clone)]
1591pub(crate) struct Glyph {
1592    /// The glyph ID in the font.
1593    pub(crate) id: GlyphId,
1594
1595    /// Position in bytes in the original string.
1596    ///
1597    /// We use it to match a glyph with a character in the text chunk and therefore with the style.
1598    pub(crate) byte_idx: ByteIndex,
1599
1600    // The length of the cluster in bytes.
1601    pub(crate) cluster_len: usize,
1602
1603    /// The text from the original string that corresponds to that glyph.
1604    pub(crate) text: String,
1605
1606    /// The glyph offset in font units.
1607    pub(crate) dx: i32,
1608
1609    /// The glyph offset in font units.
1610    pub(crate) dy: i32,
1611
1612    /// The glyph width / X-advance in font units.
1613    pub(crate) width: i32,
1614
1615    /// Reference to the source font.
1616    ///
1617    /// Each glyph can have it's own source font.
1618    pub(crate) font: Arc<ResolvedFont>,
1619}
1620
1621impl Glyph {
1622    fn is_missing(&self) -> bool {
1623        self.id.0 == 0
1624    }
1625}
1626
1627#[derive(Clone, Copy, Debug)]
1628pub(crate) struct ResolvedFont {
1629    pub(crate) id: ID,
1630
1631    units_per_em: NonZeroU16,
1632
1633    // All values below are in font units.
1634    ascent: i16,
1635    descent: i16,
1636    x_height: NonZeroU16,
1637
1638    underline_position: i16,
1639    underline_thickness: NonZeroU16,
1640
1641    // line-through thickness should be the the same as underline thickness
1642    // according to the TrueType spec:
1643    // https://docs.microsoft.com/en-us/typography/opentype/spec/os2#ystrikeoutsize
1644    line_through_position: i16,
1645
1646    subscript_offset: i16,
1647    superscript_offset: i16,
1648}
1649
1650pub(crate) fn chunk_span_at(chunk: &TextChunk, byte_offset: ByteIndex) -> Option<&TextSpan> {
1651    chunk
1652        .spans
1653        .iter()
1654        .find(|&span| span_contains(span, byte_offset))
1655}
1656
1657pub(crate) fn span_contains(span: &TextSpan, byte_offset: ByteIndex) -> bool {
1658    byte_offset.value() >= span.start && byte_offset.value() < span.end
1659}
1660
1661/// Checks that the selected character is a word separator.
1662///
1663/// According to: https://www.w3.org/TR/css-text-3/#word-separator
1664pub(crate) fn is_word_separator_characters(c: char) -> bool {
1665    matches!(
1666        c as u32,
1667        0x0020 | 0x00A0 | 0x1361 | 0x010100 | 0x010101 | 0x01039F | 0x01091F
1668    )
1669}
1670
1671impl ResolvedFont {
1672    #[inline]
1673    pub(crate) fn scale(&self, font_size: f32) -> f32 {
1674        font_size / self.units_per_em.get() as f32
1675    }
1676
1677    #[inline]
1678    pub(crate) fn ascent(&self, font_size: f32) -> f32 {
1679        self.ascent as f32 * self.scale(font_size)
1680    }
1681
1682    #[inline]
1683    pub(crate) fn descent(&self, font_size: f32) -> f32 {
1684        self.descent as f32 * self.scale(font_size)
1685    }
1686
1687    #[inline]
1688    pub(crate) fn height(&self, font_size: f32) -> f32 {
1689        self.ascent(font_size) - self.descent(font_size)
1690    }
1691
1692    #[inline]
1693    pub(crate) fn x_height(&self, font_size: f32) -> f32 {
1694        self.x_height.get() as f32 * self.scale(font_size)
1695    }
1696
1697    #[inline]
1698    pub(crate) fn underline_position(&self, font_size: f32) -> f32 {
1699        self.underline_position as f32 * self.scale(font_size)
1700    }
1701
1702    #[inline]
1703    fn underline_thickness(&self, font_size: f32) -> f32 {
1704        self.underline_thickness.get() as f32 * self.scale(font_size)
1705    }
1706
1707    #[inline]
1708    pub(crate) fn line_through_position(&self, font_size: f32) -> f32 {
1709        self.line_through_position as f32 * self.scale(font_size)
1710    }
1711
1712    #[inline]
1713    fn subscript_offset(&self, font_size: f32) -> f32 {
1714        self.subscript_offset as f32 * self.scale(font_size)
1715    }
1716
1717    #[inline]
1718    fn superscript_offset(&self, font_size: f32) -> f32 {
1719        self.superscript_offset as f32 * self.scale(font_size)
1720    }
1721
1722    fn dominant_baseline_shift(&self, baseline: DominantBaseline, font_size: f32) -> f32 {
1723        let alignment = match baseline {
1724            DominantBaseline::Auto => AlignmentBaseline::Auto,
1725            DominantBaseline::UseScript => AlignmentBaseline::Auto, // unsupported
1726            DominantBaseline::NoChange => AlignmentBaseline::Auto,  // already resolved
1727            DominantBaseline::ResetSize => AlignmentBaseline::Auto, // unsupported
1728            DominantBaseline::Ideographic => AlignmentBaseline::Ideographic,
1729            DominantBaseline::Alphabetic => AlignmentBaseline::Alphabetic,
1730            DominantBaseline::Hanging => AlignmentBaseline::Hanging,
1731            DominantBaseline::Mathematical => AlignmentBaseline::Mathematical,
1732            DominantBaseline::Central => AlignmentBaseline::Central,
1733            DominantBaseline::Middle => AlignmentBaseline::Middle,
1734            DominantBaseline::TextAfterEdge => AlignmentBaseline::TextAfterEdge,
1735            DominantBaseline::TextBeforeEdge => AlignmentBaseline::TextBeforeEdge,
1736        };
1737
1738        self.alignment_baseline_shift(alignment, font_size)
1739    }
1740
1741    // The `alignment-baseline` property is a mess.
1742    //
1743    // The SVG 1.1 spec (https://www.w3.org/TR/SVG11/text.html#BaselineAlignmentProperties)
1744    // goes on and on about what this property suppose to do, but doesn't actually explain
1745    // how it should be implemented. It's just a very verbose overview.
1746    //
1747    // As of Nov 2022, only Chrome and Safari support `alignment-baseline`. Firefox isn't.
1748    // Same goes for basically every SVG library in existence.
1749    // Meaning we have no idea how exactly it should be implemented.
1750    //
1751    // And even Chrome and Safari cannot agree on how to handle `baseline`, `after-edge`,
1752    // `text-after-edge` and `ideographic` variants. Producing vastly different output.
1753    //
1754    // As per spec, a proper implementation should get baseline values from the font itself,
1755    // using `BASE` and `bsln` TrueType tables. If those tables are not present,
1756    // we have to synthesize them (https://drafts.csswg.org/css-inline/#baseline-synthesis-fonts).
1757    // And in the worst case scenario simply fallback to hardcoded values.
1758    //
1759    // Also, most fonts do not provide `BASE` and `bsln` tables to begin with.
1760    //
1761    // Again, as of Nov 2022, Chrome does only the latter:
1762    // https://github.com/chromium/chromium/blob/main/third_party/blink/renderer/platform/fonts/font_metrics.cc#L153
1763    //
1764    // Since baseline TrueType tables parsing and baseline synthesis are pretty hard,
1765    // we do what Chrome does - use hardcoded values. And it seems like Safari does the same.
1766    //
1767    //
1768    // But that's not all! SVG 2 and CSS Inline Layout 3 did a baseline handling overhaul,
1769    // and it's far more complex now. Not sure if anyone actually supports it.
1770    fn alignment_baseline_shift(&self, alignment: AlignmentBaseline, font_size: f32) -> f32 {
1771        match alignment {
1772            AlignmentBaseline::Auto => 0.0,
1773            AlignmentBaseline::Baseline => 0.0,
1774            AlignmentBaseline::BeforeEdge | AlignmentBaseline::TextBeforeEdge => {
1775                self.ascent(font_size)
1776            }
1777            AlignmentBaseline::Middle => self.x_height(font_size) * 0.5,
1778            AlignmentBaseline::Central => self.ascent(font_size) - self.height(font_size) * 0.5,
1779            AlignmentBaseline::AfterEdge | AlignmentBaseline::TextAfterEdge => {
1780                self.descent(font_size)
1781            }
1782            AlignmentBaseline::Ideographic => self.descent(font_size),
1783            AlignmentBaseline::Alphabetic => 0.0,
1784            AlignmentBaseline::Hanging => self.ascent(font_size) * 0.8,
1785            AlignmentBaseline::Mathematical => self.ascent(font_size) * 0.5,
1786        }
1787    }
1788}
1789
1790pub(crate) type FontsCache = HashMap<Font, Arc<ResolvedFont>>;
1791
1792/// A read-only text index in bytes.
1793///
1794/// Guarantee to be on a char boundary and in text bounds.
1795#[derive(Clone, Copy, PartialEq, Debug)]
1796pub(crate) struct ByteIndex(usize);
1797
1798impl ByteIndex {
1799    fn new(i: usize) -> Self {
1800        ByteIndex(i)
1801    }
1802
1803    pub(crate) fn value(&self) -> usize {
1804        self.0
1805    }
1806
1807    /// Converts byte position into a code point position.
1808    pub(crate) fn code_point_at(&self, text: &str) -> usize {
1809        text.char_indices()
1810            .take_while(|(i, _)| *i != self.0)
1811            .count()
1812    }
1813
1814    /// Converts byte position into a character.
1815    pub(crate) fn char_from(&self, text: &str) -> char {
1816        text[self.0..].chars().next().unwrap()
1817    }
1818}