1use std::sync::Arc;
5
6use kurbo::{ParamCurve, ParamCurveArclen};
7use svgtypes::{parse_font_families, FontFamily, Length, LengthUnit};
8
9use super::svgtree::{AId, EId, FromValue, SvgNode};
10use super::{converter, style, OptionLog};
11use crate::*;
12
13impl<'a, 'input: 'a> FromValue<'a, 'input> for TextAnchor {
14 fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> {
15 match value {
16 "start" => Some(TextAnchor::Start),
17 "middle" => Some(TextAnchor::Middle),
18 "end" => Some(TextAnchor::End),
19 _ => None,
20 }
21 }
22}
23
24impl<'a, 'input: 'a> FromValue<'a, 'input> for AlignmentBaseline {
25 fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> {
26 match value {
27 "auto" => Some(AlignmentBaseline::Auto),
28 "baseline" => Some(AlignmentBaseline::Baseline),
29 "before-edge" => Some(AlignmentBaseline::BeforeEdge),
30 "text-before-edge" => Some(AlignmentBaseline::TextBeforeEdge),
31 "middle" => Some(AlignmentBaseline::Middle),
32 "central" => Some(AlignmentBaseline::Central),
33 "after-edge" => Some(AlignmentBaseline::AfterEdge),
34 "text-after-edge" => Some(AlignmentBaseline::TextAfterEdge),
35 "ideographic" => Some(AlignmentBaseline::Ideographic),
36 "alphabetic" => Some(AlignmentBaseline::Alphabetic),
37 "hanging" => Some(AlignmentBaseline::Hanging),
38 "mathematical" => Some(AlignmentBaseline::Mathematical),
39 _ => None,
40 }
41 }
42}
43
44impl<'a, 'input: 'a> FromValue<'a, 'input> for DominantBaseline {
45 fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> {
46 match value {
47 "auto" => Some(DominantBaseline::Auto),
48 "use-script" => Some(DominantBaseline::UseScript),
49 "no-change" => Some(DominantBaseline::NoChange),
50 "reset-size" => Some(DominantBaseline::ResetSize),
51 "ideographic" => Some(DominantBaseline::Ideographic),
52 "alphabetic" => Some(DominantBaseline::Alphabetic),
53 "hanging" => Some(DominantBaseline::Hanging),
54 "mathematical" => Some(DominantBaseline::Mathematical),
55 "central" => Some(DominantBaseline::Central),
56 "middle" => Some(DominantBaseline::Middle),
57 "text-after-edge" => Some(DominantBaseline::TextAfterEdge),
58 "text-before-edge" => Some(DominantBaseline::TextBeforeEdge),
59 _ => None,
60 }
61 }
62}
63
64impl<'a, 'input: 'a> FromValue<'a, 'input> for LengthAdjust {
65 fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> {
66 match value {
67 "spacing" => Some(LengthAdjust::Spacing),
68 "spacingAndGlyphs" => Some(LengthAdjust::SpacingAndGlyphs),
69 _ => None,
70 }
71 }
72}
73
74impl<'a, 'input: 'a> FromValue<'a, 'input> for FontStyle {
75 fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> {
76 match value {
77 "normal" => Some(FontStyle::Normal),
78 "italic" => Some(FontStyle::Italic),
79 "oblique" => Some(FontStyle::Oblique),
80 _ => None,
81 }
82 }
83}
84
85#[derive(Clone, Copy, Debug)]
89struct CharacterPosition {
90 x: Option<f32>,
92 y: Option<f32>,
94 dx: Option<f32>,
96 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 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 bounding_box: dummy,
136 abs_bounding_box: dummy,
137 stroke_bounding_box: dummy,
138 abs_stroke_bounding_box: dummy,
139 flattened: Box::new(Group::empty()),
140 layouted: vec![],
141 };
142
143 if text::convert(&mut text, &state.opt.font_resolver, &mut cache.fontdb).is_none() {
144 return;
145 }
146
147 parent.children.push(Node::Text(Box::new(text)));
148}
149
150struct IterState {
151 chars_count: usize,
152 chunk_bytes_count: usize,
153 split_chunk: bool,
154 text_flow: TextFlow,
155 chunks: Vec<TextChunk>,
156}
157
158fn collect_text_chunks(
159 text_node: SvgNode,
160 pos_list: &[CharacterPosition],
161 state: &converter::State,
162 cache: &mut converter::Cache,
163) -> Vec<TextChunk> {
164 let mut iter_state = IterState {
165 chars_count: 0,
166 chunk_bytes_count: 0,
167 split_chunk: false,
168 text_flow: TextFlow::Linear,
169 chunks: Vec::new(),
170 };
171
172 collect_text_chunks_impl(text_node, pos_list, state, cache, &mut iter_state);
173
174 iter_state.chunks
175}
176
177fn collect_text_chunks_impl(
178 parent: SvgNode,
179 pos_list: &[CharacterPosition],
180 state: &converter::State,
181 cache: &mut converter::Cache,
182 iter_state: &mut IterState,
183) {
184 for child in parent.children() {
185 if child.is_element() {
186 if child.tag_name() == Some(EId::TextPath) {
187 if parent.tag_name() != Some(EId::Text) {
188 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 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 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 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 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 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 if let Some(n) = text_length {
270 if n < 0.0 {
271 text_length = None;
272 }
273 }
274
275 let visibility: Visibility = parent.find_attribute(AId::Visibility).unwrap_or_default();
276
277 let span = TextSpan {
278 start: 0,
279 end: 0,
280 fill: style::resolve_fill(parent, true, state, cache),
281 stroke: style::resolve_stroke(parent, true, state, cache),
282 paint_order,
283 font,
284 font_size,
285 small_caps: parent.find_attribute::<&str>(AId::FontVariant) == Some("small-caps"),
286 apply_kerning,
287 decoration: resolve_decoration(parent, state, cache),
288 visible: visibility == Visibility::Visible,
289 dominant_baseline,
290 alignment_baseline: parent
291 .find_attribute(AId::AlignmentBaseline)
292 .unwrap_or_default(),
293 baseline_shift: convert_baseline_shift(parent, state),
294 letter_spacing: parent.resolve_length(AId::LetterSpacing, state, 0.0),
295 word_spacing: parent.resolve_length(AId::WordSpacing, state, 0.0),
296 text_length,
297 length_adjust: parent.find_attribute(AId::LengthAdjust).unwrap_or_default(),
298 };
299
300 let mut is_new_span = true;
301 for c in child.text().chars() {
302 let char_len = c.len_utf8();
303
304 let is_new_chunk = pos_list[iter_state.chars_count].x.is_some()
310 || pos_list[iter_state.chars_count].y.is_some()
311 || iter_state.split_chunk
312 || iter_state.chunks.is_empty();
313
314 iter_state.split_chunk = false;
315
316 if is_new_chunk {
317 iter_state.chunk_bytes_count = 0;
318
319 let mut span2 = span.clone();
320 span2.start = 0;
321 span2.end = char_len;
322
323 iter_state.chunks.push(TextChunk {
324 x: pos_list[iter_state.chars_count].x,
325 y: pos_list[iter_state.chars_count].y,
326 anchor,
327 spans: vec![span2],
328 text_flow: iter_state.text_flow.clone(),
329 text: c.to_string(),
330 });
331 } else if is_new_span {
332 let mut span2 = span.clone();
334 span2.start = iter_state.chunk_bytes_count;
335 span2.end = iter_state.chunk_bytes_count + char_len;
336
337 if let Some(chunk) = iter_state.chunks.last_mut() {
338 chunk.text.push(c);
339 chunk.spans.push(span2);
340 }
341 } else {
342 if let Some(chunk) = iter_state.chunks.last_mut() {
344 chunk.text.push(c);
345 if let Some(span) = chunk.spans.last_mut() {
346 debug_assert_ne!(span.end, 0);
347 span.end += char_len;
348 }
349 }
350 }
351
352 is_new_span = false;
353 iter_state.chars_count += 1;
354 iter_state.chunk_bytes_count += char_len;
355 }
356 }
357}
358
359fn resolve_text_flow(node: SvgNode, state: &converter::State) -> Option<TextFlow> {
360 let linked_node = node.attribute::<SvgNode>(AId::Href)?;
361 let path = super::shapes::convert(linked_node, state)?;
362
363 let transform = linked_node.resolve_transform(AId::Transform, state);
365 let path = if !transform.is_identity() {
366 let mut path_copy = path.as_ref().clone();
367 path_copy = path_copy.transform(transform)?;
368 Arc::new(path_copy)
369 } else {
370 path
371 };
372
373 let start_offset: Length = node.attribute(AId::StartOffset).unwrap_or_default();
374 let start_offset = if start_offset.unit == LengthUnit::Percent {
375 let path_len = path_length(&path);
378 (path_len * (start_offset.number / 100.0)) as f32
379 } else {
380 node.resolve_length(AId::StartOffset, state, 0.0)
381 };
382
383 let id = NonEmptyString::new(linked_node.element_id().to_string())?;
384 Some(TextFlow::Path(Arc::new(TextPath {
385 id,
386 start_offset,
387 path,
388 })))
389}
390
391fn convert_font(node: SvgNode, state: &converter::State) -> Font {
392 let style: FontStyle = node.find_attribute(AId::FontStyle).unwrap_or_default();
393 let stretch = conv_font_stretch(node);
394 let weight = resolve_font_weight(node);
395
396 let font_families = if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::FontFamily))
397 {
398 n.attribute(AId::FontFamily).unwrap_or("")
399 } else {
400 ""
401 };
402
403 let mut families = parse_font_families(font_families)
404 .ok()
405 .log_none(|| {
406 log::warn!(
407 "Failed to parse {} value: '{}'. Falling back to {}.",
408 AId::FontFamily,
409 font_families,
410 state.opt.font_family
411 )
412 })
413 .unwrap_or_default();
414
415 if families.is_empty() {
416 families.push(FontFamily::Named(state.opt.font_family.clone()))
417 }
418
419 Font {
420 families,
421 style,
422 stretch,
423 weight,
424 }
425}
426
427fn conv_font_stretch(node: SvgNode) -> FontStretch {
429 if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::FontStretch)) {
430 match n.attribute(AId::FontStretch).unwrap_or("") {
431 "narrower" | "condensed" => FontStretch::Condensed,
432 "ultra-condensed" => FontStretch::UltraCondensed,
433 "extra-condensed" => FontStretch::ExtraCondensed,
434 "semi-condensed" => FontStretch::SemiCondensed,
435 "semi-expanded" => FontStretch::SemiExpanded,
436 "wider" | "expanded" => FontStretch::Expanded,
437 "extra-expanded" => FontStretch::ExtraExpanded,
438 "ultra-expanded" => FontStretch::UltraExpanded,
439 _ => FontStretch::Normal,
440 }
441 } else {
442 FontStretch::Normal
443 }
444}
445
446fn resolve_font_weight(node: SvgNode) -> u16 {
447 fn bound(min: usize, val: usize, max: usize) -> usize {
448 std::cmp::max(min, std::cmp::min(max, val))
449 }
450
451 let nodes: Vec<_> = node.ancestors().collect();
452 let mut weight = 400;
453 for n in nodes.iter().rev().skip(1) {
454 weight = match n.attribute(AId::FontWeight).unwrap_or("") {
456 "normal" => 400,
457 "bold" => 700,
458 "100" => 100,
459 "200" => 200,
460 "300" => 300,
461 "400" => 400,
462 "500" => 500,
463 "600" => 600,
464 "700" => 700,
465 "800" => 800,
466 "900" => 900,
467 "bolder" => {
468 let step = if weight == 400 { 300 } else { 100 };
474
475 bound(100, weight + step, 900)
476 }
477 "lighter" => {
478 let step = if weight == 400 { 200 } else { 100 };
484
485 bound(100, weight - step, 900)
486 }
487 _ => weight,
488 };
489 }
490
491 weight as u16
492}
493
494fn resolve_positions_list(text_node: SvgNode, state: &converter::State) -> Vec<CharacterPosition> {
554 let total_chars = count_chars(text_node);
556 let mut list = vec![
557 CharacterPosition {
558 x: None,
559 y: None,
560 dx: None,
561 dy: None,
562 };
563 total_chars
564 ];
565
566 let mut offset = 0;
567 for child in text_node.descendants() {
568 if child.is_element() {
569 if !matches!(child.tag_name(), Some(EId::Text) | Some(EId::Tspan)) {
571 continue;
572 }
573
574 let child_chars = count_chars(child);
575 macro_rules! push_list {
576 ($aid:expr, $field:ident) => {
577 if let Some(num_list) = super::units::convert_list(child, $aid, state) {
578 let len = std::cmp::min(num_list.len(), child_chars);
581 for i in 0..len {
582 list[offset + i].$field = Some(num_list[i]);
583 }
584 }
585 };
586 }
587
588 push_list!(AId::X, x);
589 push_list!(AId::Y, y);
590 push_list!(AId::Dx, dx);
591 push_list!(AId::Dy, dy);
592 } else if child.is_text() {
593 offset += child.text().chars().count();
595 }
596 }
597
598 list
599}
600
601fn resolve_rotate_list(text_node: SvgNode) -> Vec<f32> {
610 let mut list = vec![0.0; count_chars(text_node)];
612 let mut last = 0.0;
613 let mut offset = 0;
614 for child in text_node.descendants() {
615 if child.is_element() {
616 if let Some(rotate) = child.attribute::<Vec<f32>>(AId::Rotate) {
617 for i in 0..count_chars(child) {
618 if let Some(a) = rotate.get(i).cloned() {
619 list[offset + i] = a;
620 last = a;
621 } else {
622 list[offset + i] = last;
625 }
626 }
627 }
628 } else if child.is_text() {
629 offset += child.text().chars().count();
631 }
632 }
633
634 list
635}
636
637fn resolve_decoration(
639 tspan: SvgNode,
640 state: &converter::State,
641 cache: &mut converter::Cache,
642) -> TextDecoration {
643 fn find_decoration(node: SvgNode, value: &str) -> bool {
645 if let Some(str_value) = node.attribute::<&str>(AId::TextDecoration) {
646 str_value.split(' ').any(|v| v == value)
647 } else {
648 false
649 }
650 }
651
652 let mut gen_style = |text_decoration: &str| {
659 if !tspan
660 .ancestors()
661 .any(|n| find_decoration(n, text_decoration))
662 {
663 return None;
664 }
665
666 let mut fill_node = None;
667 let mut stroke_node = None;
668
669 for node in tspan.ancestors() {
670 if find_decoration(node, text_decoration) || node.tag_name() == Some(EId::Text) {
671 fill_node = fill_node.map_or(Some(node), Some);
672 stroke_node = stroke_node.map_or(Some(node), Some);
673 break;
674 }
675 }
676
677 Some(TextDecorationStyle {
678 fill: fill_node.and_then(|node| style::resolve_fill(node, true, state, cache)),
679 stroke: stroke_node.and_then(|node| style::resolve_stroke(node, true, state, cache)),
680 })
681 };
682
683 TextDecoration {
684 underline: gen_style("underline"),
685 overline: gen_style("overline"),
686 line_through: gen_style("line-through"),
687 }
688}
689
690fn convert_baseline_shift(node: SvgNode, state: &converter::State) -> Vec<BaselineShift> {
691 let mut shift = Vec::new();
692 let nodes: Vec<_> = node
693 .ancestors()
694 .take_while(|n| n.tag_name() != Some(EId::Text))
695 .collect();
696 for n in nodes {
697 if let Some(len) = n.try_attribute::<Length>(AId::BaselineShift) {
698 if len.unit == LengthUnit::Percent {
699 let n = super::units::resolve_font_size(n, state) * (len.number as f32 / 100.0);
700 shift.push(BaselineShift::Number(n));
701 } else {
702 let n = super::units::convert_length(
703 len,
704 n,
705 AId::BaselineShift,
706 Units::ObjectBoundingBox,
707 state,
708 );
709 shift.push(BaselineShift::Number(n));
710 }
711 } else if let Some(s) = n.attribute(AId::BaselineShift) {
712 match s {
713 "sub" => shift.push(BaselineShift::Subscript),
714 "super" => shift.push(BaselineShift::Superscript),
715 _ => shift.push(BaselineShift::Baseline),
716 }
717 }
718 }
719
720 if shift
721 .iter()
722 .all(|base| matches!(base, BaselineShift::Baseline))
723 {
724 shift.clear();
725 }
726
727 shift
728}
729
730fn count_chars(node: SvgNode) -> usize {
731 node.descendants()
732 .filter(|n| n.is_text())
733 .fold(0, |w, n| w + n.text().chars().count())
734}
735
736fn convert_writing_mode(text_node: SvgNode) -> WritingMode {
760 if let Some(n) = text_node
761 .ancestors()
762 .find(|n| n.has_attribute(AId::WritingMode))
763 {
764 match n.attribute(AId::WritingMode).unwrap_or("lr-tb") {
765 "tb" | "tb-rl" | "vertical-rl" | "vertical-lr" => WritingMode::TopToBottom,
766 _ => WritingMode::LeftToRight,
767 }
768 } else {
769 WritingMode::LeftToRight
770 }
771}
772
773fn path_length(path: &tiny_skia_path::Path) -> f64 {
774 let mut prev_mx = path.points()[0].x;
775 let mut prev_my = path.points()[0].y;
776 let mut prev_x = prev_mx;
777 let mut prev_y = prev_my;
778
779 fn create_curve_from_line(px: f32, py: f32, x: f32, y: f32) -> kurbo::CubicBez {
780 let line = kurbo::Line::new(
781 kurbo::Point::new(px as f64, py as f64),
782 kurbo::Point::new(x as f64, y as f64),
783 );
784 let p1 = line.eval(0.33);
785 let p2 = line.eval(0.66);
786 kurbo::CubicBez::new(line.p0, p1, p2, line.p1)
787 }
788
789 let mut length = 0.0;
790 for seg in path.segments() {
791 let curve = match seg {
792 tiny_skia_path::PathSegment::MoveTo(p) => {
793 prev_mx = p.x;
794 prev_my = p.y;
795 prev_x = p.x;
796 prev_y = p.y;
797 continue;
798 }
799 tiny_skia_path::PathSegment::LineTo(p) => {
800 create_curve_from_line(prev_x, prev_y, p.x, p.y)
801 }
802 tiny_skia_path::PathSegment::QuadTo(p1, p) => kurbo::QuadBez::new(
803 kurbo::Point::new(prev_x as f64, prev_y as f64),
804 kurbo::Point::new(p1.x as f64, p1.y as f64),
805 kurbo::Point::new(p.x as f64, p.y as f64),
806 )
807 .raise(),
808 tiny_skia_path::PathSegment::CubicTo(p1, p2, p) => kurbo::CubicBez::new(
809 kurbo::Point::new(prev_x as f64, prev_y as f64),
810 kurbo::Point::new(p1.x as f64, p1.y as f64),
811 kurbo::Point::new(p2.x as f64, p2.y as f64),
812 kurbo::Point::new(p.x as f64, p.y as f64),
813 ),
814 tiny_skia_path::PathSegment::Close => {
815 create_curve_from_line(prev_x, prev_y, prev_mx, prev_my)
816 }
817 };
818
819 length += curve.arclen(0.5);
820 prev_x = curve.p3.x as f32;
821 prev_y = curve.p3.y as f32;
822 }
823
824 length
825}