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