1use 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#[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, 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 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 font_optical_sizing = match parent.find_attribute::<&str>(AId::FontOpticalSizing) {
267 Some("none") => crate::FontOpticalSizing::None,
268 _ => crate::FontOpticalSizing::Auto, };
270
271 let mut text_length =
272 parent.try_convert_length(AId::TextLength, Units::UserSpaceOnUse, state);
273 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 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 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 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 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 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 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 if !has_wght && weight != 400 {
412 variations.push(FontVariation::new(*b"wght", weight as f32));
413 }
414
415 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 if !has_ital && style == FontStyle::Italic {
436 variations.push(FontVariation::new(*b"ital", 1.0));
437 }
438
439 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
477fn 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 if value.eq_ignore_ascii_case("normal") || value.is_empty() {
495 return Vec::new();
496 }
497
498 let mut variations = Vec::new();
499
500 for part in value.split(',') {
502 let part = part.trim();
503 if part.is_empty() {
504 continue;
505 }
506
507 let mut chars = part.chars().peekable();
510
511 while chars.peek().map_or(false, |c| c.is_whitespace()) {
513 chars.next();
514 }
515
516 let quote = match chars.next() {
518 Some('"') => '"',
519 Some('\'') => '\'',
520 _ => continue, };
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 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 while chars.peek().map_or(false, |c| c.is_whitespace()) {
542 chars.next();
543 }
544
545 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
566fn 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 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 let step = if weight == 400 { 300 } else { 100 };
613
614 bound(100, weight + step, 900)
615 }
616 "lighter" => {
617 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
633fn resolve_positions_list(text_node: SvgNode, state: &converter::State) -> Vec<CharacterPosition> {
693 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 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 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 offset += child.text().chars().count();
734 }
735 }
736
737 list
738}
739
740fn resolve_rotate_list(text_node: SvgNode) -> Vec<f32> {
749 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 list[offset + i] = last;
764 }
765 }
766 }
767 } else if child.is_text() {
768 offset += child.text().chars().count();
770 }
771 }
772
773 list
774}
775
776fn resolve_decoration(
778 tspan: SvgNode,
779 state: &converter::State,
780 cache: &mut converter::Cache,
781) -> TextDecoration {
782 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 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
875fn 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}