Skip to main content

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::{FontFamily, Length, LengthUnit, parse_font_families};
8
9use super::svgtree::{AId, EId, FromValue, SvgNode};
10use super::{OptionLog, converter, style};
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, cache).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 font_optical_sizing = match parent.find_attribute::<&str>(AId::FontOpticalSizing) {
267            Some("none") => crate::FontOpticalSizing::None,
268            _ => crate::FontOpticalSizing::Auto, // "auto" or missing (browser default)
269        };
270
271        let mut text_length =
272            parent.try_convert_length(AId::TextLength, Units::UserSpaceOnUse, state);
273        // Negative values should be ignored.
274        if let Some(n) = text_length {
275            if n < 0.0 {
276                text_length = None;
277            }
278        }
279
280        let visibility: Visibility = parent.find_attribute(AId::Visibility).unwrap_or_default();
281
282        let span = TextSpan {
283            start: 0,
284            end: 0,
285            fill: style::resolve_fill(parent, true, state, cache),
286            stroke: style::resolve_stroke(parent, true, state, cache),
287            paint_order,
288            font,
289            font_size,
290            small_caps: parent.find_attribute::<&str>(AId::FontVariant) == Some("small-caps"),
291            apply_kerning,
292            font_optical_sizing,
293            decoration: resolve_decoration(parent, state, cache),
294            visible: visibility == Visibility::Visible,
295            dominant_baseline,
296            alignment_baseline: parent
297                .find_attribute(AId::AlignmentBaseline)
298                .unwrap_or_default(),
299            baseline_shift: convert_baseline_shift(parent, state),
300            letter_spacing: parent.resolve_length(AId::LetterSpacing, state, 0.0),
301            word_spacing: parent.resolve_length(AId::WordSpacing, state, 0.0),
302            text_length,
303            length_adjust: parent.find_attribute(AId::LengthAdjust).unwrap_or_default(),
304        };
305
306        let mut is_new_span = true;
307        for c in child.text().chars() {
308            let char_len = c.len_utf8();
309
310            // Create a new chunk if:
311            // - this is the first span (yes, position can be None)
312            // - text character has an absolute coordinate assigned to it (via x/y attribute)
313            // - `c` is the first char of the `textPath`
314            // - `c` is the first char after `textPath`
315            let is_new_chunk = pos_list[iter_state.chars_count].x.is_some()
316                || pos_list[iter_state.chars_count].y.is_some()
317                || iter_state.split_chunk
318                || iter_state.chunks.is_empty();
319
320            iter_state.split_chunk = false;
321
322            if is_new_chunk {
323                iter_state.chunk_bytes_count = 0;
324
325                let mut span2 = span.clone();
326                span2.start = 0;
327                span2.end = char_len;
328
329                iter_state.chunks.push(TextChunk {
330                    x: pos_list[iter_state.chars_count].x,
331                    y: pos_list[iter_state.chars_count].y,
332                    anchor,
333                    spans: vec![span2],
334                    text_flow: iter_state.text_flow.clone(),
335                    text: c.to_string(),
336                });
337            } else if is_new_span {
338                // Add this span to the last text chunk.
339                let mut span2 = span.clone();
340                span2.start = iter_state.chunk_bytes_count;
341                span2.end = iter_state.chunk_bytes_count + char_len;
342
343                if let Some(chunk) = iter_state.chunks.last_mut() {
344                    chunk.text.push(c);
345                    chunk.spans.push(span2);
346                }
347            } else {
348                // Extend the last span.
349                if let Some(chunk) = iter_state.chunks.last_mut() {
350                    chunk.text.push(c);
351                    if let Some(span) = chunk.spans.last_mut() {
352                        debug_assert_ne!(span.end, 0);
353                        span.end += char_len;
354                    }
355                }
356            }
357
358            is_new_span = false;
359            iter_state.chars_count += 1;
360            iter_state.chunk_bytes_count += char_len;
361        }
362    }
363}
364
365fn resolve_text_flow(node: SvgNode, state: &converter::State) -> Option<TextFlow> {
366    let linked_node = node.attribute::<SvgNode>(AId::Href)?;
367    let path = super::shapes::convert(linked_node, state)?;
368
369    // The reference path's transform needs to be applied
370    let transform = linked_node.resolve_transform(AId::Transform, state);
371    let path = if !transform.is_identity() {
372        let mut path_copy = path.as_ref().clone();
373        path_copy = path_copy.transform(transform)?;
374        Arc::new(path_copy)
375    } else {
376        path
377    };
378
379    let start_offset: Length = node.attribute(AId::StartOffset).unwrap_or_default();
380    let start_offset = if start_offset.unit == LengthUnit::Percent {
381        // 'If a percentage is given, then the `startOffset` represents
382        // a percentage distance along the entire path.'
383        let path_len = path_length(&path);
384        (path_len * (start_offset.number / 100.0)) as f32
385    } else {
386        node.resolve_length(AId::StartOffset, state, 0.0)
387    };
388
389    let id = NonEmptyString::new(linked_node.element_id().to_string())?;
390    Some(TextFlow::Path(Arc::new(TextPath {
391        id,
392        start_offset,
393        path,
394    })))
395}
396
397fn convert_font(node: SvgNode, state: &converter::State) -> Font {
398    let style: FontStyle = node.find_attribute(AId::FontStyle).unwrap_or_default();
399    let stretch = conv_font_stretch(node);
400    let weight = resolve_font_weight(node);
401    let mut variations = parse_font_variation_settings(node);
402
403    // Auto-map standard font properties to variation axes if not explicitly set.
404    // This allows variable fonts to work with regular font-weight/font-stretch properties.
405    let has_wght = variations.iter().any(|v| &v.tag == b"wght");
406    let has_wdth = variations.iter().any(|v| &v.tag == b"wdth");
407    let has_ital = variations.iter().any(|v| &v.tag == b"ital");
408    let has_slnt = variations.iter().any(|v| &v.tag == b"slnt");
409
410    // Map font-weight to wght axis (if not already set)
411    if !has_wght && weight != 400 {
412        variations.push(FontVariation::new(*b"wght", weight as f32));
413    }
414
415    // Map font-stretch to wdth axis (if not already set)
416    // CSS font-stretch percentages: ultra-condensed=50%, condensed=75%, normal=100%, expanded=125%, ultra-expanded=200%
417    if !has_wdth {
418        let wdth = match stretch {
419            FontStretch::UltraCondensed => 50.0,
420            FontStretch::ExtraCondensed => 62.5,
421            FontStretch::Condensed => 75.0,
422            FontStretch::SemiCondensed => 87.5,
423            FontStretch::Normal => 100.0,
424            FontStretch::SemiExpanded => 112.5,
425            FontStretch::Expanded => 125.0,
426            FontStretch::ExtraExpanded => 150.0,
427            FontStretch::UltraExpanded => 200.0,
428        };
429        if wdth != 100.0 {
430            variations.push(FontVariation::new(*b"wdth", wdth));
431        }
432    }
433
434    // Map font-style: italic to ital axis (if not already set)
435    if !has_ital && style == FontStyle::Italic {
436        variations.push(FontVariation::new(*b"ital", 1.0));
437    }
438
439    // Map font-style: oblique to slnt axis (if not already set)
440    // Default oblique angle is typically 12-14 degrees
441    if !has_slnt && style == FontStyle::Oblique {
442        variations.push(FontVariation::new(*b"slnt", -12.0));
443    }
444
445    let font_families = if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::FontFamily))
446    {
447        n.attribute(AId::FontFamily).unwrap_or("")
448    } else {
449        ""
450    };
451
452    let mut families = parse_font_families(font_families)
453        .ok()
454        .log_none(|| {
455            log::warn!(
456                "Failed to parse {} value: '{}'. Falling back to {}.",
457                AId::FontFamily,
458                font_families,
459                state.opt.font_family
460            );
461        })
462        .unwrap_or_default();
463
464    if families.is_empty() {
465        families.push(FontFamily::Named(state.opt.font_family.clone()));
466    }
467
468    Font {
469        families,
470        style,
471        stretch,
472        weight,
473        variations,
474    }
475}
476
477/// Parses the `font-variation-settings` CSS property.
478///
479/// Syntax: `normal | [ <string> <number> ]#`
480/// Example: `"wght" 700, "wdth" 50`
481fn parse_font_variation_settings(node: SvgNode) -> Vec<FontVariation> {
482    let value = if let Some(n) = node
483        .ancestors()
484        .find(|n| n.has_attribute(AId::FontVariationSettings))
485    {
486        let v = n.attribute(AId::FontVariationSettings).unwrap_or("");
487        log::debug!("Found font-variation-settings: '{}'", v);
488        v
489    } else {
490        return Vec::new();
491    };
492
493    // "normal" means no variations
494    if value.eq_ignore_ascii_case("normal") || value.is_empty() {
495        return Vec::new();
496    }
497
498    let mut variations = Vec::new();
499
500    // Parse comma-separated list of "tag" value pairs
501    for part in value.split(',') {
502        let part = part.trim();
503        if part.is_empty() {
504            continue;
505        }
506
507        // Find the tag (quoted string) and value
508        // Format: "wght" 700 or 'wght' 700
509        let mut chars = part.chars().peekable();
510
511        // Skip whitespace
512        while chars.peek().map_or(false, |c| c.is_whitespace()) {
513            chars.next();
514        }
515
516        // Parse quoted tag
517        let quote = match chars.next() {
518            Some('"') => '"',
519            Some('\'') => '\'',
520            _ => continue, // Invalid format
521        };
522
523        let mut tag_str = String::new();
524        for c in chars.by_ref() {
525            if c == quote {
526                break;
527            }
528            tag_str.push(c);
529        }
530
531        // Tag must be exactly 4 characters
532        if tag_str.len() != 4 {
533            log::warn!(
534                "Invalid font-variation-settings tag: '{}' (must be 4 characters)",
535                tag_str
536            );
537            continue;
538        }
539
540        // Skip whitespace before value
541        while chars.peek().map_or(false, |c| c.is_whitespace()) {
542            chars.next();
543        }
544
545        // Parse the numeric value
546        let value_str: String = chars.collect();
547        let value_str = value_str.trim();
548
549        let value = match value_str.parse::<f32>() {
550            Ok(v) => v,
551            Err(_) => {
552                log::warn!("Invalid font-variation-settings value: '{}'", value_str);
553                continue;
554            }
555        };
556
557        let tag_bytes = tag_str.as_bytes();
558        let tag = [tag_bytes[0], tag_bytes[1], tag_bytes[2], tag_bytes[3]];
559
560        variations.push(FontVariation::new(tag, value));
561    }
562
563    variations
564}
565
566// TODO: properly resolve narrower/wider
567fn conv_font_stretch(node: SvgNode) -> FontStretch {
568    if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::FontStretch)) {
569        match n.attribute(AId::FontStretch).unwrap_or("") {
570            "narrower" | "condensed" => FontStretch::Condensed,
571            "ultra-condensed" => FontStretch::UltraCondensed,
572            "extra-condensed" => FontStretch::ExtraCondensed,
573            "semi-condensed" => FontStretch::SemiCondensed,
574            "semi-expanded" => FontStretch::SemiExpanded,
575            "wider" | "expanded" => FontStretch::Expanded,
576            "extra-expanded" => FontStretch::ExtraExpanded,
577            "ultra-expanded" => FontStretch::UltraExpanded,
578            _ => FontStretch::Normal,
579        }
580    } else {
581        FontStretch::Normal
582    }
583}
584
585fn resolve_font_weight(node: SvgNode) -> u16 {
586    fn bound(min: usize, val: usize, max: usize) -> usize {
587        std::cmp::max(min, std::cmp::min(max, val))
588    }
589
590    let nodes: Vec<_> = node.ancestors().collect();
591    let mut weight = 400;
592    for n in nodes.iter().rev().skip(1) {
593        // skip Root
594        weight = match n.attribute(AId::FontWeight).unwrap_or("") {
595            "normal" => 400,
596            "bold" => 700,
597            "100" => 100,
598            "200" => 200,
599            "300" => 300,
600            "400" => 400,
601            "500" => 500,
602            "600" => 600,
603            "700" => 700,
604            "800" => 800,
605            "900" => 900,
606            "bolder" => {
607                // By the CSS2 spec the default value should be 400
608                // so `bolder` will result in 500.
609                // But Chrome and Inkscape will give us 700.
610                // Have no idea is it a bug or something, but
611                // we will follow such behavior for now.
612                let step = if weight == 400 { 300 } else { 100 };
613
614                bound(100, weight + step, 900)
615            }
616            "lighter" => {
617                // By the CSS2 spec the default value should be 400
618                // so `lighter` will result in 300.
619                // But Chrome and Inkscape will give us 200.
620                // Have no idea is it a bug or something, but
621                // we will follow such behavior for now.
622                let step = if weight == 400 { 200 } else { 100 };
623
624                bound(100, weight - step, 900)
625            }
626            _ => weight,
627        };
628    }
629
630    weight as u16
631}
632
633/// Resolves text's character positions.
634///
635/// This includes: x, y, dx, dy.
636///
637/// # The character
638///
639/// The first problem with this task is that the *character* itself
640/// is basically undefined in the SVG spec. Sometimes it's an *XML character*,
641/// sometimes a *glyph*, and sometimes just a *character*.
642///
643/// There is an ongoing [discussion](https://github.com/w3c/svgwg/issues/537)
644/// on the SVG working group that addresses this by stating that a character
645/// is a Unicode code point. But it's not final.
646///
647/// Also, according to the SVG 2 spec, *character* is *a Unicode code point*.
648///
649/// Anyway, we treat a character as a Unicode code point.
650///
651/// # Algorithm
652///
653/// To resolve positions, we have to iterate over descendant nodes and
654/// if the current node is a `tspan` and has x/y/dx/dy attribute,
655/// than the positions from this attribute should be assigned to the characters
656/// of this `tspan` and it's descendants.
657///
658/// Positions list can have more values than characters in the `tspan`,
659/// so we have to clamp it, because values should not overlap, e.g.:
660///
661/// (we ignore whitespaces for example purposes,
662/// so the `text` content is `Text` and not `T ex t`)
663///
664/// ```text
665/// <text>
666///   a
667///   <tspan x="10 20 30">
668///     bc
669///   </tspan>
670///   d
671/// </text>
672/// ```
673///
674/// In this example, the `d` position should not be set to `30`.
675/// And the result should be: `[None, 10, 20, None]`
676///
677/// Another example:
678///
679/// ```text
680/// <text>
681///   <tspan x="100 110 120 130">
682///     a
683///     <tspan x="50">
684///       bc
685///     </tspan>
686///   </tspan>
687///   d
688/// </text>
689/// ```
690///
691/// The result should be: `[100, 50, 120, None]`
692fn resolve_positions_list(text_node: SvgNode, state: &converter::State) -> Vec<CharacterPosition> {
693    // Allocate a list that has all characters positions set to `None`.
694    let total_chars = count_chars(text_node);
695    let mut list = vec![
696        CharacterPosition {
697            x: None,
698            y: None,
699            dx: None,
700            dy: None,
701        };
702        total_chars
703    ];
704
705    let mut offset = 0;
706    for child in text_node.descendants() {
707        if child.is_element() {
708            // We must ignore text positions on `textPath`.
709            if !matches!(child.tag_name(), Some(EId::Text) | Some(EId::Tspan)) {
710                continue;
711            }
712
713            let child_chars = count_chars(child);
714            macro_rules! push_list {
715                ($aid:expr, $field:ident) => {
716                    if let Some(num_list) = super::units::convert_list(child, $aid, state) {
717                        // Note that we are using not the total count,
718                        // but the amount of characters in the current `tspan` (with children).
719                        let len = std::cmp::min(num_list.len(), child_chars);
720                        for i in 0..len {
721                            list[offset + i].$field = Some(num_list[i]);
722                        }
723                    }
724                };
725            }
726
727            push_list!(AId::X, x);
728            push_list!(AId::Y, y);
729            push_list!(AId::Dx, dx);
730            push_list!(AId::Dy, dy);
731        } else if child.is_text() {
732            // Advance the offset.
733            offset += child.text().chars().count();
734        }
735    }
736
737    list
738}
739
740/// Resolves characters rotation.
741///
742/// The algorithm is well explained
743/// [in the SVG spec](https://www.w3.org/TR/SVG11/text.html#TSpanElement) (scroll down a bit).
744///
745/// ![](https://www.w3.org/TR/SVG11/images/text/tspan05-diagram.png)
746///
747/// Note: this algorithm differs from the position resolving one.
748fn resolve_rotate_list(text_node: SvgNode) -> Vec<f32> {
749    // Allocate a list that has all characters angles set to `0.0`.
750    let mut list = vec![0.0; count_chars(text_node)];
751    let mut last = 0.0;
752    let mut offset = 0;
753    for child in text_node.descendants() {
754        if child.is_element() {
755            if let Some(rotate) = child.attribute::<Vec<f32>>(AId::Rotate) {
756                for i in 0..count_chars(child) {
757                    if let Some(a) = rotate.get(i).cloned() {
758                        list[offset + i] = a;
759                        last = a;
760                    } else {
761                        // If the rotate list doesn't specify the rotation for
762                        // this character - use the last one.
763                        list[offset + i] = last;
764                    }
765                }
766            }
767        } else if child.is_text() {
768            // Advance the offset.
769            offset += child.text().chars().count();
770        }
771    }
772
773    list
774}
775
776/// Resolves node's `text-decoration` property.
777fn resolve_decoration(
778    tspan: SvgNode,
779    state: &converter::State,
780    cache: &mut converter::Cache,
781) -> TextDecoration {
782    // Checks if a decoration is present in a single node.
783    fn find_decoration(node: SvgNode, value: &str) -> bool {
784        if let Some(str_value) = node.attribute::<&str>(AId::TextDecoration) {
785            str_value.split(' ').any(|v| v == value)
786        } else {
787            false
788        }
789    }
790
791    // The algorithm is as follows: First, we check whether the given text decoration appears in ANY
792    // ancestor, i.e. it can also appear in ancestors outside of the <text> element. If the text
793    // decoration is declared somewhere, it means that this tspan will have it. However, we still
794    // need to find the corresponding fill/stroke for it. To do this, we iterate through all
795    // ancestors (i.e. tspans) until we find the text decoration declared. If not, we will
796    // stop at latest at the text node, and use its fill/stroke.
797    let mut gen_style = |text_decoration: &str| {
798        if !tspan
799            .ancestors()
800            .any(|n| find_decoration(n, text_decoration))
801        {
802            return None;
803        }
804
805        let mut fill_node = None;
806        let mut stroke_node = None;
807
808        for node in tspan.ancestors() {
809            if find_decoration(node, text_decoration) || node.tag_name() == Some(EId::Text) {
810                fill_node = fill_node.map_or(Some(node), Some);
811                stroke_node = stroke_node.map_or(Some(node), Some);
812                break;
813            }
814        }
815
816        Some(TextDecorationStyle {
817            fill: fill_node.and_then(|node| style::resolve_fill(node, true, state, cache)),
818            stroke: stroke_node.and_then(|node| style::resolve_stroke(node, true, state, cache)),
819        })
820    };
821
822    TextDecoration {
823        underline: gen_style("underline"),
824        overline: gen_style("overline"),
825        line_through: gen_style("line-through"),
826    }
827}
828
829fn convert_baseline_shift(node: SvgNode, state: &converter::State) -> Vec<BaselineShift> {
830    let mut shift = Vec::new();
831    let nodes: Vec<_> = node
832        .ancestors()
833        .take_while(|n| n.tag_name() != Some(EId::Text))
834        .collect();
835    for n in nodes {
836        if let Some(len) = n.try_attribute::<Length>(AId::BaselineShift) {
837            if len.unit == LengthUnit::Percent {
838                let n = super::units::resolve_font_size(n, state) * (len.number as f32 / 100.0);
839                shift.push(BaselineShift::Number(n));
840            } else {
841                let n = super::units::convert_length(
842                    len,
843                    n,
844                    AId::BaselineShift,
845                    Units::ObjectBoundingBox,
846                    state,
847                );
848                shift.push(BaselineShift::Number(n));
849            }
850        } else if let Some(s) = n.attribute(AId::BaselineShift) {
851            match s {
852                "sub" => shift.push(BaselineShift::Subscript),
853                "super" => shift.push(BaselineShift::Superscript),
854                _ => shift.push(BaselineShift::Baseline),
855            }
856        }
857    }
858
859    if shift
860        .iter()
861        .all(|base| matches!(base, BaselineShift::Baseline))
862    {
863        shift.clear();
864    }
865
866    shift
867}
868
869fn count_chars(node: SvgNode) -> usize {
870    node.descendants()
871        .filter(|n| n.is_text())
872        .fold(0, |w, n| w + n.text().chars().count())
873}
874
875/// Converts the writing mode.
876///
877/// [SVG 2] references [CSS Writing Modes Level 3] for the definition of the
878/// 'writing-mode' property, there are only two writing modes:
879/// horizontal left-to-right and vertical right-to-left.
880///
881/// That specification introduces new values for the property. The SVG 1.1
882/// values are obsolete but must still be supported by converting the specified
883/// values to computed values as follows:
884///
885/// - `lr`, `lr-tb`, `rl`, `rl-tb` => `horizontal-tb`
886/// - `tb`, `tb-rl` => `vertical-rl`
887///
888/// The current `vertical-lr` behaves exactly the same as `vertical-rl`.
889///
890/// Also, looks like no one really supports the `rl` and `rl-tb`, except `Batik`.
891/// And I'm not sure if its behaviour is correct.
892///
893/// So we will ignore it as well, mainly because I have no idea how exactly
894/// it should affect the rendering.
895///
896/// [SVG 2]: https://www.w3.org/TR/SVG2/text.html#WritingModeProperty
897/// [CSS Writing Modes Level 3]: https://www.w3.org/TR/css-writing-modes-3/#svg-writing-mode-css
898fn convert_writing_mode(text_node: SvgNode) -> WritingMode {
899    if let Some(n) = text_node
900        .ancestors()
901        .find(|n| n.has_attribute(AId::WritingMode))
902    {
903        match n.attribute(AId::WritingMode).unwrap_or("lr-tb") {
904            "tb" | "tb-rl" | "vertical-rl" | "vertical-lr" => WritingMode::TopToBottom,
905            _ => WritingMode::LeftToRight,
906        }
907    } else {
908        WritingMode::LeftToRight
909    }
910}
911
912fn path_length(path: &tiny_skia_path::Path) -> f64 {
913    let mut prev_mx = path.points()[0].x;
914    let mut prev_my = path.points()[0].y;
915    let mut prev_x = prev_mx;
916    let mut prev_y = prev_my;
917
918    fn create_curve_from_line(px: f32, py: f32, x: f32, y: f32) -> kurbo::CubicBez {
919        let line = kurbo::Line::new(
920            kurbo::Point::new(px as f64, py as f64),
921            kurbo::Point::new(x as f64, y as f64),
922        );
923        let p1 = line.eval(0.33);
924        let p2 = line.eval(0.66);
925        kurbo::CubicBez::new(line.p0, p1, p2, line.p1)
926    }
927
928    let mut length = 0.0;
929    for seg in path.segments() {
930        let curve = match seg {
931            tiny_skia_path::PathSegment::MoveTo(p) => {
932                prev_mx = p.x;
933                prev_my = p.y;
934                prev_x = p.x;
935                prev_y = p.y;
936                continue;
937            }
938            tiny_skia_path::PathSegment::LineTo(p) => {
939                create_curve_from_line(prev_x, prev_y, p.x, p.y)
940            }
941            tiny_skia_path::PathSegment::QuadTo(p1, p) => kurbo::QuadBez::new(
942                kurbo::Point::new(prev_x as f64, prev_y as f64),
943                kurbo::Point::new(p1.x as f64, p1.y as f64),
944                kurbo::Point::new(p.x as f64, p.y as f64),
945            )
946            .raise(),
947            tiny_skia_path::PathSegment::CubicTo(p1, p2, p) => kurbo::CubicBez::new(
948                kurbo::Point::new(prev_x as f64, prev_y as f64),
949                kurbo::Point::new(p1.x as f64, p1.y as f64),
950                kurbo::Point::new(p2.x as f64, p2.y as f64),
951                kurbo::Point::new(p.x as f64, p.y as f64),
952            ),
953            tiny_skia_path::PathSegment::Close => {
954                create_curve_from_line(prev_x, prev_y, prev_mx, prev_my)
955            }
956        };
957
958        length += curve.arclen(0.5);
959        prev_x = curve.p3.x as f32;
960        prev_y = curve.p3.y as f32;
961    }
962
963    length
964}