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