usvg_text_layout/
lib.rs

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