usvg/parser/
text.rs

1// Copyright 2019 the Resvg Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use std::sync::Arc;
5
6use kurbo::{ParamCurve, ParamCurveArclen};
7use svgtypes::{parse_font_families, FontFamily, Length, LengthUnit};
8
9use super::svgtree::{AId, EId, FromValue, SvgNode};
10use super::{converter, style, OptionLog};
11use crate::*;
12
13impl<'a, 'input: 'a> FromValue<'a, 'input> for TextAnchor {
14    fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> {
15        match value {
16            "start" => Some(TextAnchor::Start),
17            "middle" => Some(TextAnchor::Middle),
18            "end" => Some(TextAnchor::End),
19            _ => None,
20        }
21    }
22}
23
24impl<'a, 'input: 'a> FromValue<'a, 'input> for AlignmentBaseline {
25    fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> {
26        match value {
27            "auto" => Some(AlignmentBaseline::Auto),
28            "baseline" => Some(AlignmentBaseline::Baseline),
29            "before-edge" => Some(AlignmentBaseline::BeforeEdge),
30            "text-before-edge" => Some(AlignmentBaseline::TextBeforeEdge),
31            "middle" => Some(AlignmentBaseline::Middle),
32            "central" => Some(AlignmentBaseline::Central),
33            "after-edge" => Some(AlignmentBaseline::AfterEdge),
34            "text-after-edge" => Some(AlignmentBaseline::TextAfterEdge),
35            "ideographic" => Some(AlignmentBaseline::Ideographic),
36            "alphabetic" => Some(AlignmentBaseline::Alphabetic),
37            "hanging" => Some(AlignmentBaseline::Hanging),
38            "mathematical" => Some(AlignmentBaseline::Mathematical),
39            _ => None,
40        }
41    }
42}
43
44impl<'a, 'input: 'a> FromValue<'a, 'input> for DominantBaseline {
45    fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> {
46        match value {
47            "auto" => Some(DominantBaseline::Auto),
48            "use-script" => Some(DominantBaseline::UseScript),
49            "no-change" => Some(DominantBaseline::NoChange),
50            "reset-size" => Some(DominantBaseline::ResetSize),
51            "ideographic" => Some(DominantBaseline::Ideographic),
52            "alphabetic" => Some(DominantBaseline::Alphabetic),
53            "hanging" => Some(DominantBaseline::Hanging),
54            "mathematical" => Some(DominantBaseline::Mathematical),
55            "central" => Some(DominantBaseline::Central),
56            "middle" => Some(DominantBaseline::Middle),
57            "text-after-edge" => Some(DominantBaseline::TextAfterEdge),
58            "text-before-edge" => Some(DominantBaseline::TextBeforeEdge),
59            _ => None,
60        }
61    }
62}
63
64impl<'a, 'input: 'a> FromValue<'a, 'input> for LengthAdjust {
65    fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> {
66        match value {
67            "spacing" => Some(LengthAdjust::Spacing),
68            "spacingAndGlyphs" => Some(LengthAdjust::SpacingAndGlyphs),
69            _ => None,
70        }
71    }
72}
73
74impl<'a, 'input: 'a> FromValue<'a, 'input> for FontStyle {
75    fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> {
76        match value {
77            "normal" => Some(FontStyle::Normal),
78            "italic" => Some(FontStyle::Italic),
79            "oblique" => Some(FontStyle::Oblique),
80            _ => None,
81        }
82    }
83}
84
85/// A text character position.
86///
87/// _Character_ is a Unicode codepoint.
88#[derive(Clone, Copy, Debug)]
89struct CharacterPosition {
90    /// An absolute X axis position.
91    x: Option<f32>,
92    /// An absolute Y axis position.
93    y: Option<f32>,
94    /// A relative X axis offset.
95    dx: Option<f32>,
96    /// A relative Y axis offset.
97    dy: Option<f32>,
98}
99
100pub(crate) fn convert(
101    text_node: SvgNode,
102    state: &converter::State,
103    cache: &mut converter::Cache,
104    parent: &mut Group,
105) {
106    let pos_list = resolve_positions_list(text_node, state);
107    let rotate_list = resolve_rotate_list(text_node);
108    let writing_mode = convert_writing_mode(text_node);
109
110    let chunks = collect_text_chunks(text_node, &pos_list, state, cache);
111
112    let rendering_mode: TextRendering = text_node
113        .find_attribute(AId::TextRendering)
114        .unwrap_or(state.opt.text_rendering);
115
116    // Nodes generated by markers must not have an ID. Otherwise we would have duplicates.
117    let id = if state.parent_markers.is_empty() {
118        text_node.element_id().to_string()
119    } else {
120        String::new()
121    };
122
123    let dummy = Rect::from_xywh(0.0, 0.0, 0.0, 0.0).unwrap();
124
125    let mut text = Text {
126        id,
127        rendering_mode,
128        dx: pos_list.iter().map(|v| v.dx.unwrap_or(0.0)).collect(),
129        dy: pos_list.iter().map(|v| v.dy.unwrap_or(0.0)).collect(),
130        rotate: rotate_list,
131        writing_mode,
132        chunks,
133        abs_transform: parent.abs_transform,
134        // All fields below will be reset by `text_to_paths`.
135        bounding_box: dummy,
136        abs_bounding_box: dummy,
137        stroke_bounding_box: dummy,
138        abs_stroke_bounding_box: dummy,
139        flattened: Box::new(Group::empty()),
140        layouted: vec![],
141    };
142
143    if text::convert(&mut text, &state.opt.font_resolver, &mut cache.fontdb).is_none() {
144        return;
145    }
146
147    parent.children.push(Node::Text(Box::new(text)));
148}
149
150struct IterState {
151    chars_count: usize,
152    chunk_bytes_count: usize,
153    split_chunk: bool,
154    text_flow: TextFlow,
155    chunks: Vec<TextChunk>,
156}
157
158fn collect_text_chunks(
159    text_node: SvgNode,
160    pos_list: &[CharacterPosition],
161    state: &converter::State,
162    cache: &mut converter::Cache,
163) -> Vec<TextChunk> {
164    let mut iter_state = IterState {
165        chars_count: 0,
166        chunk_bytes_count: 0,
167        split_chunk: false,
168        text_flow: TextFlow::Linear,
169        chunks: Vec::new(),
170    };
171
172    collect_text_chunks_impl(text_node, pos_list, state, cache, &mut iter_state);
173
174    iter_state.chunks
175}
176
177fn collect_text_chunks_impl(
178    parent: SvgNode,
179    pos_list: &[CharacterPosition],
180    state: &converter::State,
181    cache: &mut converter::Cache,
182    iter_state: &mut IterState,
183) {
184    for child in parent.children() {
185        if child.is_element() {
186            if child.tag_name() == Some(EId::TextPath) {
187                if parent.tag_name() != Some(EId::Text) {
188                    // `textPath` can be set only as a direct `text` element child.
189                    iter_state.chars_count += count_chars(child);
190                    continue;
191                }
192
193                match resolve_text_flow(child, state) {
194                    Some(v) => {
195                        iter_state.text_flow = v;
196                    }
197                    None => {
198                        // Skip an invalid text path and all it's children.
199                        // We have to update the chars count,
200                        // because `pos_list` was calculated including this text path.
201                        iter_state.chars_count += count_chars(child);
202                        continue;
203                    }
204                }
205
206                iter_state.split_chunk = true;
207            }
208
209            collect_text_chunks_impl(child, pos_list, state, cache, iter_state);
210
211            iter_state.text_flow = TextFlow::Linear;
212
213            // Next char after `textPath` should be split too.
214            if child.tag_name() == Some(EId::TextPath) {
215                iter_state.split_chunk = true;
216            }
217
218            continue;
219        }
220
221        if !parent.is_visible_element(state.opt) {
222            iter_state.chars_count += child.text().chars().count();
223            continue;
224        }
225
226        let anchor = parent.find_attribute(AId::TextAnchor).unwrap_or_default();
227
228        // TODO: what to do when <= 0? UB?
229        let font_size = super::units::resolve_font_size(parent, state);
230        let font_size = match NonZeroPositiveF32::new(font_size) {
231            Some(n) => n,
232            None => {
233                // Skip this span.
234                iter_state.chars_count += child.text().chars().count();
235                continue;
236            }
237        };
238
239        let font = convert_font(parent, state);
240
241        let raw_paint_order: svgtypes::PaintOrder =
242            parent.find_attribute(AId::PaintOrder).unwrap_or_default();
243        let paint_order = super::converter::svg_paint_order_to_usvg(raw_paint_order);
244
245        let mut dominant_baseline = parent
246            .find_attribute(AId::DominantBaseline)
247            .unwrap_or_default();
248
249        // `no-change` means "use parent".
250        if dominant_baseline == DominantBaseline::NoChange {
251            dominant_baseline = parent
252                .parent_element()
253                .unwrap()
254                .find_attribute(AId::DominantBaseline)
255                .unwrap_or_default();
256        }
257
258        let mut apply_kerning = true;
259        #[allow(clippy::if_same_then_else)]
260        if parent.resolve_length(AId::Kerning, state, -1.0) == 0.0 {
261            apply_kerning = false;
262        } else if parent.find_attribute::<&str>(AId::FontKerning) == Some("none") {
263            apply_kerning = false;
264        }
265
266        let mut text_length =
267            parent.try_convert_length(AId::TextLength, Units::UserSpaceOnUse, state);
268        // Negative values should be ignored.
269        if let Some(n) = text_length {
270            if n < 0.0 {
271                text_length = None;
272            }
273        }
274
275        let visibility: Visibility = parent.find_attribute(AId::Visibility).unwrap_or_default();
276
277        let span = TextSpan {
278            start: 0,
279            end: 0,
280            fill: style::resolve_fill(parent, true, state, cache),
281            stroke: style::resolve_stroke(parent, true, state, cache),
282            paint_order,
283            font,
284            font_size,
285            small_caps: parent.find_attribute::<&str>(AId::FontVariant) == Some("small-caps"),
286            apply_kerning,
287            decoration: resolve_decoration(parent, state, cache),
288            visible: visibility == Visibility::Visible,
289            dominant_baseline,
290            alignment_baseline: parent
291                .find_attribute(AId::AlignmentBaseline)
292                .unwrap_or_default(),
293            baseline_shift: convert_baseline_shift(parent, state),
294            letter_spacing: parent.resolve_length(AId::LetterSpacing, state, 0.0),
295            word_spacing: parent.resolve_length(AId::WordSpacing, state, 0.0),
296            text_length,
297            length_adjust: parent.find_attribute(AId::LengthAdjust).unwrap_or_default(),
298        };
299
300        let mut is_new_span = true;
301        for c in child.text().chars() {
302            let char_len = c.len_utf8();
303
304            // Create a new chunk if:
305            // - this is the first span (yes, position can be None)
306            // - text character has an absolute coordinate assigned to it (via x/y attribute)
307            // - `c` is the first char of the `textPath`
308            // - `c` is the first char after `textPath`
309            let is_new_chunk = pos_list[iter_state.chars_count].x.is_some()
310                || pos_list[iter_state.chars_count].y.is_some()
311                || iter_state.split_chunk
312                || iter_state.chunks.is_empty();
313
314            iter_state.split_chunk = false;
315
316            if is_new_chunk {
317                iter_state.chunk_bytes_count = 0;
318
319                let mut span2 = span.clone();
320                span2.start = 0;
321                span2.end = char_len;
322
323                iter_state.chunks.push(TextChunk {
324                    x: pos_list[iter_state.chars_count].x,
325                    y: pos_list[iter_state.chars_count].y,
326                    anchor,
327                    spans: vec![span2],
328                    text_flow: iter_state.text_flow.clone(),
329                    text: c.to_string(),
330                });
331            } else if is_new_span {
332                // Add this span to the last text chunk.
333                let mut span2 = span.clone();
334                span2.start = iter_state.chunk_bytes_count;
335                span2.end = iter_state.chunk_bytes_count + char_len;
336
337                if let Some(chunk) = iter_state.chunks.last_mut() {
338                    chunk.text.push(c);
339                    chunk.spans.push(span2);
340                }
341            } else {
342                // Extend the last span.
343                if let Some(chunk) = iter_state.chunks.last_mut() {
344                    chunk.text.push(c);
345                    if let Some(span) = chunk.spans.last_mut() {
346                        debug_assert_ne!(span.end, 0);
347                        span.end += char_len;
348                    }
349                }
350            }
351
352            is_new_span = false;
353            iter_state.chars_count += 1;
354            iter_state.chunk_bytes_count += char_len;
355        }
356    }
357}
358
359fn resolve_text_flow(node: SvgNode, state: &converter::State) -> Option<TextFlow> {
360    let linked_node = node.attribute::<SvgNode>(AId::Href)?;
361    let path = super::shapes::convert(linked_node, state)?;
362
363    // The reference path's transform needs to be applied
364    let transform = linked_node.resolve_transform(AId::Transform, state);
365    let path = if !transform.is_identity() {
366        let mut path_copy = path.as_ref().clone();
367        path_copy = path_copy.transform(transform)?;
368        Arc::new(path_copy)
369    } else {
370        path
371    };
372
373    let start_offset: Length = node.attribute(AId::StartOffset).unwrap_or_default();
374    let start_offset = if start_offset.unit == LengthUnit::Percent {
375        // 'If a percentage is given, then the `startOffset` represents
376        // a percentage distance along the entire path.'
377        let path_len = path_length(&path);
378        (path_len * (start_offset.number / 100.0)) as f32
379    } else {
380        node.resolve_length(AId::StartOffset, state, 0.0)
381    };
382
383    let id = NonEmptyString::new(linked_node.element_id().to_string())?;
384    Some(TextFlow::Path(Arc::new(TextPath {
385        id,
386        start_offset,
387        path,
388    })))
389}
390
391fn convert_font(node: SvgNode, state: &converter::State) -> Font {
392    let style: FontStyle = node.find_attribute(AId::FontStyle).unwrap_or_default();
393    let stretch = conv_font_stretch(node);
394    let weight = resolve_font_weight(node);
395
396    let font_families = if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::FontFamily))
397    {
398        n.attribute(AId::FontFamily).unwrap_or("")
399    } else {
400        ""
401    };
402
403    let mut families = parse_font_families(font_families)
404        .ok()
405        .log_none(|| {
406            log::warn!(
407                "Failed to parse {} value: '{}'. Falling back to {}.",
408                AId::FontFamily,
409                font_families,
410                state.opt.font_family
411            )
412        })
413        .unwrap_or_default();
414
415    if families.is_empty() {
416        families.push(FontFamily::Named(state.opt.font_family.clone()))
417    }
418
419    Font {
420        families,
421        style,
422        stretch,
423        weight,
424    }
425}
426
427// TODO: properly resolve narrower/wider
428fn conv_font_stretch(node: SvgNode) -> FontStretch {
429    if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::FontStretch)) {
430        match n.attribute(AId::FontStretch).unwrap_or("") {
431            "narrower" | "condensed" => FontStretch::Condensed,
432            "ultra-condensed" => FontStretch::UltraCondensed,
433            "extra-condensed" => FontStretch::ExtraCondensed,
434            "semi-condensed" => FontStretch::SemiCondensed,
435            "semi-expanded" => FontStretch::SemiExpanded,
436            "wider" | "expanded" => FontStretch::Expanded,
437            "extra-expanded" => FontStretch::ExtraExpanded,
438            "ultra-expanded" => FontStretch::UltraExpanded,
439            _ => FontStretch::Normal,
440        }
441    } else {
442        FontStretch::Normal
443    }
444}
445
446fn resolve_font_weight(node: SvgNode) -> u16 {
447    fn bound(min: usize, val: usize, max: usize) -> usize {
448        std::cmp::max(min, std::cmp::min(max, val))
449    }
450
451    let nodes: Vec<_> = node.ancestors().collect();
452    let mut weight = 400;
453    for n in nodes.iter().rev().skip(1) {
454        // skip Root
455        weight = match n.attribute(AId::FontWeight).unwrap_or("") {
456            "normal" => 400,
457            "bold" => 700,
458            "100" => 100,
459            "200" => 200,
460            "300" => 300,
461            "400" => 400,
462            "500" => 500,
463            "600" => 600,
464            "700" => 700,
465            "800" => 800,
466            "900" => 900,
467            "bolder" => {
468                // By the CSS2 spec the default value should be 400
469                // so `bolder` will result in 500.
470                // But Chrome and Inkscape will give us 700.
471                // Have no idea is it a bug or something, but
472                // we will follow such behavior for now.
473                let step = if weight == 400 { 300 } else { 100 };
474
475                bound(100, weight + step, 900)
476            }
477            "lighter" => {
478                // By the CSS2 spec the default value should be 400
479                // so `lighter` will result in 300.
480                // But Chrome and Inkscape will give us 200.
481                // Have no idea is it a bug or something, but
482                // we will follow such behavior for now.
483                let step = if weight == 400 { 200 } else { 100 };
484
485                bound(100, weight - step, 900)
486            }
487            _ => weight,
488        };
489    }
490
491    weight as u16
492}
493
494/// Resolves text's character positions.
495///
496/// This includes: x, y, dx, dy.
497///
498/// # The character
499///
500/// The first problem with this task is that the *character* itself
501/// is basically undefined in the SVG spec. Sometimes it's an *XML character*,
502/// sometimes a *glyph*, and sometimes just a *character*.
503///
504/// There is an ongoing [discussion](https://github.com/w3c/svgwg/issues/537)
505/// on the SVG working group that addresses this by stating that a character
506/// is a Unicode code point. But it's not final.
507///
508/// Also, according to the SVG 2 spec, *character* is *a Unicode code point*.
509///
510/// Anyway, we treat a character as a Unicode code point.
511///
512/// # Algorithm
513///
514/// To resolve positions, we have to iterate over descendant nodes and
515/// if the current node is a `tspan` and has x/y/dx/dy attribute,
516/// than the positions from this attribute should be assigned to the characters
517/// of this `tspan` and it's descendants.
518///
519/// Positions list can have more values than characters in the `tspan`,
520/// so we have to clamp it, because values should not overlap, e.g.:
521///
522/// (we ignore whitespaces for example purposes,
523/// so the `text` content is `Text` and not `T ex t`)
524///
525/// ```text
526/// <text>
527///   a
528///   <tspan x="10 20 30">
529///     bc
530///   </tspan>
531///   d
532/// </text>
533/// ```
534///
535/// In this example, the `d` position should not be set to `30`.
536/// And the result should be: `[None, 10, 20, None]`
537///
538/// Another example:
539///
540/// ```text
541/// <text>
542///   <tspan x="100 110 120 130">
543///     a
544///     <tspan x="50">
545///       bc
546///     </tspan>
547///   </tspan>
548///   d
549/// </text>
550/// ```
551///
552/// The result should be: `[100, 50, 120, None]`
553fn resolve_positions_list(text_node: SvgNode, state: &converter::State) -> Vec<CharacterPosition> {
554    // Allocate a list that has all characters positions set to `None`.
555    let total_chars = count_chars(text_node);
556    let mut list = vec![
557        CharacterPosition {
558            x: None,
559            y: None,
560            dx: None,
561            dy: None,
562        };
563        total_chars
564    ];
565
566    let mut offset = 0;
567    for child in text_node.descendants() {
568        if child.is_element() {
569            // We must ignore text positions on `textPath`.
570            if !matches!(child.tag_name(), Some(EId::Text) | Some(EId::Tspan)) {
571                continue;
572            }
573
574            let child_chars = count_chars(child);
575            macro_rules! push_list {
576                ($aid:expr, $field:ident) => {
577                    if let Some(num_list) = super::units::convert_list(child, $aid, state) {
578                        // Note that we are using not the total count,
579                        // but the amount of characters in the current `tspan` (with children).
580                        let len = std::cmp::min(num_list.len(), child_chars);
581                        for i in 0..len {
582                            list[offset + i].$field = Some(num_list[i]);
583                        }
584                    }
585                };
586            }
587
588            push_list!(AId::X, x);
589            push_list!(AId::Y, y);
590            push_list!(AId::Dx, dx);
591            push_list!(AId::Dy, dy);
592        } else if child.is_text() {
593            // Advance the offset.
594            offset += child.text().chars().count();
595        }
596    }
597
598    list
599}
600
601/// Resolves characters rotation.
602///
603/// The algorithm is well explained
604/// [in the SVG spec](https://www.w3.org/TR/SVG11/text.html#TSpanElement) (scroll down a bit).
605///
606/// ![](https://www.w3.org/TR/SVG11/images/text/tspan05-diagram.png)
607///
608/// Note: this algorithm differs from the position resolving one.
609fn resolve_rotate_list(text_node: SvgNode) -> Vec<f32> {
610    // Allocate a list that has all characters angles set to `0.0`.
611    let mut list = vec![0.0; count_chars(text_node)];
612    let mut last = 0.0;
613    let mut offset = 0;
614    for child in text_node.descendants() {
615        if child.is_element() {
616            if let Some(rotate) = child.attribute::<Vec<f32>>(AId::Rotate) {
617                for i in 0..count_chars(child) {
618                    if let Some(a) = rotate.get(i).cloned() {
619                        list[offset + i] = a;
620                        last = a;
621                    } else {
622                        // If the rotate list doesn't specify the rotation for
623                        // this character - use the last one.
624                        list[offset + i] = last;
625                    }
626                }
627            }
628        } else if child.is_text() {
629            // Advance the offset.
630            offset += child.text().chars().count();
631        }
632    }
633
634    list
635}
636
637/// Resolves node's `text-decoration` property.
638fn resolve_decoration(
639    tspan: SvgNode,
640    state: &converter::State,
641    cache: &mut converter::Cache,
642) -> TextDecoration {
643    // Checks if a decoration is present in a single node.
644    fn find_decoration(node: SvgNode, value: &str) -> bool {
645        if let Some(str_value) = node.attribute::<&str>(AId::TextDecoration) {
646            str_value.split(' ').any(|v| v == value)
647        } else {
648            false
649        }
650    }
651
652    // The algorithm is as follows: First, we check whether the given text decoration appears in ANY
653    // ancestor, i.e. it can also appear in ancestors outside of the <text> element. If the text
654    // decoration is declared somewhere, it means that this tspan will have it. However, we still
655    // need to find the corresponding fill/stroke for it. To do this, we iterate through all
656    // ancestors (i.e. tspans) until we find the text decoration declared. If not, we will
657    // stop at latest at the text node, and use its fill/stroke.
658    let mut gen_style = |text_decoration: &str| {
659        if !tspan
660            .ancestors()
661            .any(|n| find_decoration(n, text_decoration))
662        {
663            return None;
664        }
665
666        let mut fill_node = None;
667        let mut stroke_node = None;
668
669        for node in tspan.ancestors() {
670            if find_decoration(node, text_decoration) || node.tag_name() == Some(EId::Text) {
671                fill_node = fill_node.map_or(Some(node), Some);
672                stroke_node = stroke_node.map_or(Some(node), Some);
673                break;
674            }
675        }
676
677        Some(TextDecorationStyle {
678            fill: fill_node.and_then(|node| style::resolve_fill(node, true, state, cache)),
679            stroke: stroke_node.and_then(|node| style::resolve_stroke(node, true, state, cache)),
680        })
681    };
682
683    TextDecoration {
684        underline: gen_style("underline"),
685        overline: gen_style("overline"),
686        line_through: gen_style("line-through"),
687    }
688}
689
690fn convert_baseline_shift(node: SvgNode, state: &converter::State) -> Vec<BaselineShift> {
691    let mut shift = Vec::new();
692    let nodes: Vec<_> = node
693        .ancestors()
694        .take_while(|n| n.tag_name() != Some(EId::Text))
695        .collect();
696    for n in nodes {
697        if let Some(len) = n.try_attribute::<Length>(AId::BaselineShift) {
698            if len.unit == LengthUnit::Percent {
699                let n = super::units::resolve_font_size(n, state) * (len.number as f32 / 100.0);
700                shift.push(BaselineShift::Number(n));
701            } else {
702                let n = super::units::convert_length(
703                    len,
704                    n,
705                    AId::BaselineShift,
706                    Units::ObjectBoundingBox,
707                    state,
708                );
709                shift.push(BaselineShift::Number(n));
710            }
711        } else if let Some(s) = n.attribute(AId::BaselineShift) {
712            match s {
713                "sub" => shift.push(BaselineShift::Subscript),
714                "super" => shift.push(BaselineShift::Superscript),
715                _ => shift.push(BaselineShift::Baseline),
716            }
717        }
718    }
719
720    if shift
721        .iter()
722        .all(|base| matches!(base, BaselineShift::Baseline))
723    {
724        shift.clear();
725    }
726
727    shift
728}
729
730fn count_chars(node: SvgNode) -> usize {
731    node.descendants()
732        .filter(|n| n.is_text())
733        .fold(0, |w, n| w + n.text().chars().count())
734}
735
736/// Converts the writing mode.
737///
738/// [SVG 2] references [CSS Writing Modes Level 3] for the definition of the
739/// 'writing-mode' property, there are only two writing modes:
740/// horizontal left-to-right and vertical right-to-left.
741///
742/// That specification introduces new values for the property. The SVG 1.1
743/// values are obsolete but must still be supported by converting the specified
744/// values to computed values as follows:
745///
746/// - `lr`, `lr-tb`, `rl`, `rl-tb` => `horizontal-tb`
747/// - `tb`, `tb-rl` => `vertical-rl`
748///
749/// The current `vertical-lr` behaves exactly the same as `vertical-rl`.
750///
751/// Also, looks like no one really supports the `rl` and `rl-tb`, except `Batik`.
752/// And I'm not sure if its behaviour is correct.
753///
754/// So we will ignore it as well, mainly because I have no idea how exactly
755/// it should affect the rendering.
756///
757/// [SVG 2]: https://www.w3.org/TR/SVG2/text.html#WritingModeProperty
758/// [CSS Writing Modes Level 3]: https://www.w3.org/TR/css-writing-modes-3/#svg-writing-mode-css
759fn convert_writing_mode(text_node: SvgNode) -> WritingMode {
760    if let Some(n) = text_node
761        .ancestors()
762        .find(|n| n.has_attribute(AId::WritingMode))
763    {
764        match n.attribute(AId::WritingMode).unwrap_or("lr-tb") {
765            "tb" | "tb-rl" | "vertical-rl" | "vertical-lr" => WritingMode::TopToBottom,
766            _ => WritingMode::LeftToRight,
767        }
768    } else {
769        WritingMode::LeftToRight
770    }
771}
772
773fn path_length(path: &tiny_skia_path::Path) -> f64 {
774    let mut prev_mx = path.points()[0].x;
775    let mut prev_my = path.points()[0].y;
776    let mut prev_x = prev_mx;
777    let mut prev_y = prev_my;
778
779    fn create_curve_from_line(px: f32, py: f32, x: f32, y: f32) -> kurbo::CubicBez {
780        let line = kurbo::Line::new(
781            kurbo::Point::new(px as f64, py as f64),
782            kurbo::Point::new(x as f64, y as f64),
783        );
784        let p1 = line.eval(0.33);
785        let p2 = line.eval(0.66);
786        kurbo::CubicBez::new(line.p0, p1, p2, line.p1)
787    }
788
789    let mut length = 0.0;
790    for seg in path.segments() {
791        let curve = match seg {
792            tiny_skia_path::PathSegment::MoveTo(p) => {
793                prev_mx = p.x;
794                prev_my = p.y;
795                prev_x = p.x;
796                prev_y = p.y;
797                continue;
798            }
799            tiny_skia_path::PathSegment::LineTo(p) => {
800                create_curve_from_line(prev_x, prev_y, p.x, p.y)
801            }
802            tiny_skia_path::PathSegment::QuadTo(p1, p) => kurbo::QuadBez::new(
803                kurbo::Point::new(prev_x as f64, prev_y as f64),
804                kurbo::Point::new(p1.x as f64, p1.y as f64),
805                kurbo::Point::new(p.x as f64, p.y as f64),
806            )
807            .raise(),
808            tiny_skia_path::PathSegment::CubicTo(p1, p2, p) => kurbo::CubicBez::new(
809                kurbo::Point::new(prev_x as f64, prev_y as f64),
810                kurbo::Point::new(p1.x as f64, p1.y as f64),
811                kurbo::Point::new(p2.x as f64, p2.y as f64),
812                kurbo::Point::new(p.x as f64, p.y as f64),
813            ),
814            tiny_skia_path::PathSegment::Close => {
815                create_curve_from_line(prev_x, prev_y, prev_mx, prev_my)
816            }
817        };
818
819        length += curve.arclen(0.5);
820        prev_x = curve.p3.x as f32;
821        prev_y = curve.p3.y as f32;
822    }
823
824    length
825}