1use 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#[derive(Clone, Copy, Debug)]
90struct CharacterPosition {
91 x: Option<f32>,
93 y: Option<f32>,
95 dx: Option<f32>,
97 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 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 bounding_box: dummy,
137 abs_bounding_box: dummy,
138 stroke_bounding_box: dummy,
139 abs_stroke_bounding_box: dummy,
140 flattened: Box::new(Group::empty()),
141 layouted: vec![],
142 };
143
144 if text::convert(&mut text, &state.opt.font_resolver, &mut cache.fontdb).is_none() {
145 return;
146 }
147
148 parent.children.push(Node::Text(Box::new(text)));
149}
150
151struct IterState {
152 chars_count: usize,
153 chunk_bytes_count: usize,
154 split_chunk: bool,
155 text_flow: TextFlow,
156 chunks: Vec<TextChunk>,
157}
158
159fn collect_text_chunks(
160 text_node: SvgNode,
161 pos_list: &[CharacterPosition],
162 state: &converter::State,
163 cache: &mut converter::Cache,
164) -> Vec<TextChunk> {
165 let mut iter_state = IterState {
166 chars_count: 0,
167 chunk_bytes_count: 0,
168 split_chunk: false,
169 text_flow: TextFlow::Linear,
170 chunks: Vec::new(),
171 };
172
173 collect_text_chunks_impl(text_node, pos_list, state, cache, &mut iter_state);
174
175 iter_state.chunks
176}
177
178fn collect_text_chunks_impl(
179 parent: SvgNode,
180 pos_list: &[CharacterPosition],
181 state: &converter::State,
182 cache: &mut converter::Cache,
183 iter_state: &mut IterState,
184) {
185 for child in parent.children() {
186 if child.is_element() {
187 if child.tag_name() == Some(EId::TextPath) {
188 if parent.tag_name() != Some(EId::Text) {
189 iter_state.chars_count += count_chars(child);
191 continue;
192 }
193
194 match resolve_text_flow(child, state) {
195 Some(v) => {
196 iter_state.text_flow = v;
197 }
198 None => {
199 iter_state.chars_count += count_chars(child);
203 continue;
204 }
205 }
206
207 iter_state.split_chunk = true;
208 }
209
210 collect_text_chunks_impl(child, pos_list, state, cache, iter_state);
211
212 iter_state.text_flow = TextFlow::Linear;
213
214 if child.tag_name() == Some(EId::TextPath) {
216 iter_state.split_chunk = true;
217 }
218
219 continue;
220 }
221
222 if !parent.is_visible_element(state.opt) {
223 iter_state.chars_count += child.text().chars().count();
224 continue;
225 }
226
227 let anchor = parent.find_attribute(AId::TextAnchor).unwrap_or_default();
228
229 let font_size = super::units::resolve_font_size(parent, state);
231 let font_size = match NonZeroPositiveF32::new(font_size) {
232 Some(n) => n,
233 None => {
234 iter_state.chars_count += child.text().chars().count();
236 continue;
237 }
238 };
239
240 let font = convert_font(parent, state);
241
242 let raw_paint_order: svgtypes::PaintOrder =
243 parent.find_attribute(AId::PaintOrder).unwrap_or_default();
244 let paint_order = super::converter::svg_paint_order_to_usvg(raw_paint_order);
245
246 let mut dominant_baseline = parent
247 .find_attribute(AId::DominantBaseline)
248 .unwrap_or_default();
249
250 if dominant_baseline == DominantBaseline::NoChange {
252 dominant_baseline = parent
253 .parent_element()
254 .unwrap()
255 .find_attribute(AId::DominantBaseline)
256 .unwrap_or_default();
257 }
258
259 let mut apply_kerning = true;
260 #[allow(clippy::if_same_then_else)]
261 if parent.resolve_length(AId::Kerning, state, -1.0) == 0.0 {
262 apply_kerning = false;
263 } else if parent.find_attribute::<&str>(AId::FontKerning) == Some("none") {
264 apply_kerning = false;
265 }
266
267 let mut text_length =
268 parent.try_convert_length(AId::TextLength, Units::UserSpaceOnUse, state);
269 if let Some(n) = text_length {
271 if n < 0.0 {
272 text_length = None;
273 }
274 }
275
276 let visibility: Visibility = parent.find_attribute(AId::Visibility).unwrap_or_default();
277
278 let span = TextSpan {
279 start: 0,
280 end: 0,
281 fill: style::resolve_fill(parent, true, state, cache),
282 stroke: style::resolve_stroke(parent, true, state, cache),
283 paint_order,
284 font,
285 font_size,
286 small_caps: parent.find_attribute::<&str>(AId::FontVariant) == Some("small-caps"),
287 apply_kerning,
288 decoration: resolve_decoration(parent, state, cache),
289 visible: visibility == Visibility::Visible,
290 dominant_baseline,
291 alignment_baseline: parent
292 .find_attribute(AId::AlignmentBaseline)
293 .unwrap_or_default(),
294 baseline_shift: convert_baseline_shift(parent, state),
295 letter_spacing: parent.resolve_length(AId::LetterSpacing, state, 0.0),
296 word_spacing: parent.resolve_length(AId::WordSpacing, state, 0.0),
297 text_length,
298 length_adjust: parent.find_attribute(AId::LengthAdjust).unwrap_or_default(),
299 };
300
301 let mut is_new_span = true;
302 for c in child.text().chars() {
303 let char_len = c.len_utf8();
304
305 let is_new_chunk = pos_list[iter_state.chars_count].x.is_some()
311 || pos_list[iter_state.chars_count].y.is_some()
312 || iter_state.split_chunk
313 || iter_state.chunks.is_empty();
314
315 iter_state.split_chunk = false;
316
317 if is_new_chunk {
318 iter_state.chunk_bytes_count = 0;
319
320 let mut span2 = span.clone();
321 span2.start = 0;
322 span2.end = char_len;
323
324 iter_state.chunks.push(TextChunk {
325 x: pos_list[iter_state.chars_count].x,
326 y: pos_list[iter_state.chars_count].y,
327 anchor,
328 spans: vec![span2],
329 text_flow: iter_state.text_flow.clone(),
330 text: c.to_string(),
331 });
332 } else if is_new_span {
333 let mut span2 = span.clone();
335 span2.start = iter_state.chunk_bytes_count;
336 span2.end = iter_state.chunk_bytes_count + char_len;
337
338 if let Some(chunk) = iter_state.chunks.last_mut() {
339 chunk.text.push(c);
340 chunk.spans.push(span2);
341 }
342 } else {
343 if let Some(chunk) = iter_state.chunks.last_mut() {
345 chunk.text.push(c);
346 if let Some(span) = chunk.spans.last_mut() {
347 debug_assert_ne!(span.end, 0);
348 span.end += char_len;
349 }
350 }
351 }
352
353 is_new_span = false;
354 iter_state.chars_count += 1;
355 iter_state.chunk_bytes_count += char_len;
356 }
357 }
358}
359
360fn resolve_text_flow(node: SvgNode, state: &converter::State) -> Option<TextFlow> {
361 let linked_node = node.attribute::<SvgNode>(AId::Href)?;
362 let path = super::shapes::convert(linked_node, state)?;
363
364 let transform = linked_node.resolve_transform(AId::Transform, state);
366 let path = if !transform.is_identity() {
367 let mut path_copy = path.as_ref().clone();
368 path_copy = path_copy.transform(transform)?;
369 Arc::new(path_copy)
370 } else {
371 path
372 };
373
374 let start_offset: Length = node.attribute(AId::StartOffset).unwrap_or_default();
375 let start_offset = if start_offset.unit == LengthUnit::Percent {
376 let path_len = path_length(&path);
379 (path_len * (start_offset.number / 100.0)) as f32
380 } else {
381 node.resolve_length(AId::StartOffset, state, 0.0)
382 };
383
384 let id = NonEmptyString::new(linked_node.element_id().to_string())?;
385 Some(TextFlow::Path(Arc::new(TextPath {
386 id,
387 start_offset,
388 path,
389 })))
390}
391
392fn convert_font(node: SvgNode, state: &converter::State) -> Font {
393 let style: FontStyle = node.find_attribute(AId::FontStyle).unwrap_or_default();
394 let stretch = conv_font_stretch(node);
395 let weight = resolve_font_weight(node);
396
397 let font_families = if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::FontFamily))
398 {
399 n.attribute(AId::FontFamily).unwrap_or("")
400 } else {
401 ""
402 };
403
404 let mut families = parse_font_families(font_families)
405 .ok()
406 .log_none(|| {
407 log::warn!(
408 "Failed to parse {} value: '{}'. Falling back to {}.",
409 AId::FontFamily,
410 font_families,
411 state.opt.font_family
412 )
413 })
414 .unwrap_or_default();
415
416 if families.is_empty() {
417 families.push(FontFamily::Named(state.opt.font_family.clone()))
418 }
419
420 Font {
421 families,
422 style,
423 stretch,
424 weight,
425 }
426}
427
428fn conv_font_stretch(node: SvgNode) -> FontStretch {
430 if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::FontStretch)) {
431 match n.attribute(AId::FontStretch).unwrap_or("") {
432 "narrower" | "condensed" => FontStretch::Condensed,
433 "ultra-condensed" => FontStretch::UltraCondensed,
434 "extra-condensed" => FontStretch::ExtraCondensed,
435 "semi-condensed" => FontStretch::SemiCondensed,
436 "semi-expanded" => FontStretch::SemiExpanded,
437 "wider" | "expanded" => FontStretch::Expanded,
438 "extra-expanded" => FontStretch::ExtraExpanded,
439 "ultra-expanded" => FontStretch::UltraExpanded,
440 _ => FontStretch::Normal,
441 }
442 } else {
443 FontStretch::Normal
444 }
445}
446
447fn resolve_font_weight(node: SvgNode) -> u16 {
448 fn bound(min: usize, val: usize, max: usize) -> usize {
449 std::cmp::max(min, std::cmp::min(max, val))
450 }
451
452 let nodes: Vec<_> = node.ancestors().collect();
453 let mut weight = 400;
454 for n in nodes.iter().rev().skip(1) {
455 weight = match n.attribute(AId::FontWeight).unwrap_or("") {
457 "normal" => 400,
458 "bold" => 700,
459 "100" => 100,
460 "200" => 200,
461 "300" => 300,
462 "400" => 400,
463 "500" => 500,
464 "600" => 600,
465 "700" => 700,
466 "800" => 800,
467 "900" => 900,
468 "bolder" => {
469 let step = if weight == 400 { 300 } else { 100 };
475
476 bound(100, weight + step, 900)
477 }
478 "lighter" => {
479 let step = if weight == 400 { 200 } else { 100 };
485
486 bound(100, weight - step, 900)
487 }
488 _ => weight,
489 };
490 }
491
492 weight as u16
493}
494
495fn resolve_positions_list(text_node: SvgNode, state: &converter::State) -> Vec<CharacterPosition> {
555 let total_chars = count_chars(text_node);
557 let mut list = vec![
558 CharacterPosition {
559 x: None,
560 y: None,
561 dx: None,
562 dy: None,
563 };
564 total_chars
565 ];
566
567 let mut offset = 0;
568 for child in text_node.descendants() {
569 if child.is_element() {
570 if !matches!(child.tag_name(), Some(EId::Text) | Some(EId::Tspan)) {
572 continue;
573 }
574
575 let child_chars = count_chars(child);
576 macro_rules! push_list {
577 ($aid:expr, $field:ident) => {
578 if let Some(num_list) = super::units::convert_list(child, $aid, state) {
579 let len = std::cmp::min(num_list.len(), child_chars);
582 for i in 0..len {
583 list[offset + i].$field = Some(num_list[i]);
584 }
585 }
586 };
587 }
588
589 push_list!(AId::X, x);
590 push_list!(AId::Y, y);
591 push_list!(AId::Dx, dx);
592 push_list!(AId::Dy, dy);
593 } else if child.is_text() {
594 offset += child.text().chars().count();
596 }
597 }
598
599 list
600}
601
602fn resolve_rotate_list(text_node: SvgNode) -> Vec<f32> {
611 let mut list = vec![0.0; count_chars(text_node)];
613 let mut last = 0.0;
614 let mut offset = 0;
615 for child in text_node.descendants() {
616 if child.is_element() {
617 if let Some(rotate) = child.attribute::<Vec<f32>>(AId::Rotate) {
618 for i in 0..count_chars(child) {
619 if let Some(a) = rotate.get(i).cloned() {
620 list[offset + i] = a;
621 last = a;
622 } else {
623 list[offset + i] = last;
626 }
627 }
628 }
629 } else if child.is_text() {
630 offset += child.text().chars().count();
632 }
633 }
634
635 list
636}
637
638fn resolve_decoration(
640 tspan: SvgNode,
641 state: &converter::State,
642 cache: &mut converter::Cache,
643) -> TextDecoration {
644 fn find_decoration(node: SvgNode, value: &str) -> bool {
646 if let Some(str_value) = node.attribute::<&str>(AId::TextDecoration) {
647 str_value.split(' ').any(|v| v == value)
648 } else {
649 false
650 }
651 }
652
653 let mut gen_style = |text_decoration: &str| {
660 if !tspan
661 .ancestors()
662 .any(|n| find_decoration(n, text_decoration))
663 {
664 return None;
665 }
666
667 let mut fill_node = None;
668 let mut stroke_node = None;
669
670 for node in tspan.ancestors() {
671 if find_decoration(node, text_decoration) || node.tag_name() == Some(EId::Text) {
672 fill_node = fill_node.map_or(Some(node), Some);
673 stroke_node = stroke_node.map_or(Some(node), Some);
674 break;
675 }
676 }
677
678 Some(TextDecorationStyle {
679 fill: fill_node.and_then(|node| style::resolve_fill(node, true, state, cache)),
680 stroke: stroke_node.and_then(|node| style::resolve_stroke(node, true, state, cache)),
681 })
682 };
683
684 TextDecoration {
685 underline: gen_style("underline"),
686 overline: gen_style("overline"),
687 line_through: gen_style("line-through"),
688 }
689}
690
691fn convert_baseline_shift(node: SvgNode, state: &converter::State) -> Vec<BaselineShift> {
692 let mut shift = Vec::new();
693 let nodes: Vec<_> = node
694 .ancestors()
695 .take_while(|n| n.tag_name() != Some(EId::Text))
696 .collect();
697 for n in nodes {
698 if let Some(len) = n.try_attribute::<Length>(AId::BaselineShift) {
699 if len.unit == LengthUnit::Percent {
700 let n = super::units::resolve_font_size(n, state) * (len.number as f32 / 100.0);
701 shift.push(BaselineShift::Number(n));
702 } else {
703 let n = super::units::convert_length(
704 len,
705 n,
706 AId::BaselineShift,
707 Units::ObjectBoundingBox,
708 state,
709 );
710 shift.push(BaselineShift::Number(n));
711 }
712 } else if let Some(s) = n.attribute(AId::BaselineShift) {
713 match s {
714 "sub" => shift.push(BaselineShift::Subscript),
715 "super" => shift.push(BaselineShift::Superscript),
716 _ => shift.push(BaselineShift::Baseline),
717 }
718 }
719 }
720
721 if shift
722 .iter()
723 .all(|base| matches!(base, BaselineShift::Baseline))
724 {
725 shift.clear();
726 }
727
728 shift
729}
730
731fn count_chars(node: SvgNode) -> usize {
732 node.descendants()
733 .filter(|n| n.is_text())
734 .fold(0, |w, n| w + n.text().chars().count())
735}
736
737fn convert_writing_mode(text_node: SvgNode) -> WritingMode {
761 if let Some(n) = text_node
762 .ancestors()
763 .find(|n| n.has_attribute(AId::WritingMode))
764 {
765 match n.attribute(AId::WritingMode).unwrap_or("lr-tb") {
766 "tb" | "tb-rl" | "vertical-rl" | "vertical-lr" => WritingMode::TopToBottom,
767 _ => WritingMode::LeftToRight,
768 }
769 } else {
770 WritingMode::LeftToRight
771 }
772}
773
774fn path_length(path: &tiny_skia_path::Path) -> f64 {
775 let mut prev_mx = path.points()[0].x;
776 let mut prev_my = path.points()[0].y;
777 let mut prev_x = prev_mx;
778 let mut prev_y = prev_my;
779
780 fn create_curve_from_line(px: f32, py: f32, x: f32, y: f32) -> kurbo::CubicBez {
781 let line = kurbo::Line::new(
782 kurbo::Point::new(px as f64, py as f64),
783 kurbo::Point::new(x as f64, y as f64),
784 );
785 let p1 = line.eval(0.33);
786 let p2 = line.eval(0.66);
787 kurbo::CubicBez::new(line.p0, p1, p2, line.p1)
788 }
789
790 let mut length = 0.0;
791 for seg in path.segments() {
792 let curve = match seg {
793 tiny_skia_path::PathSegment::MoveTo(p) => {
794 prev_mx = p.x;
795 prev_my = p.y;
796 prev_x = p.x;
797 prev_y = p.y;
798 continue;
799 }
800 tiny_skia_path::PathSegment::LineTo(p) => {
801 create_curve_from_line(prev_x, prev_y, p.x, p.y)
802 }
803 tiny_skia_path::PathSegment::QuadTo(p1, p) => kurbo::QuadBez::new(
804 kurbo::Point::new(prev_x as f64, prev_y as f64),
805 kurbo::Point::new(p1.x as f64, p1.y as f64),
806 kurbo::Point::new(p.x as f64, p.y as f64),
807 )
808 .raise(),
809 tiny_skia_path::PathSegment::CubicTo(p1, p2, p) => kurbo::CubicBez::new(
810 kurbo::Point::new(prev_x as f64, prev_y as f64),
811 kurbo::Point::new(p1.x as f64, p1.y as f64),
812 kurbo::Point::new(p2.x as f64, p2.y as f64),
813 kurbo::Point::new(p.x as f64, p.y as f64),
814 ),
815 tiny_skia_path::PathSegment::Close => {
816 create_curve_from_line(prev_x, prev_y, prev_mx, prev_my)
817 }
818 };
819
820 length += curve.arclen(0.5);
821 prev_x = curve.p3.x as f32;
822 prev_y = curve.p3.y as f32;
823 }
824
825 length
826}