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