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