Skip to main content

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