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