@@ -430,6 +430,9 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
430430
431431 let graphite_gradient_stops = extract_graphite_gradient_stops ( & svg) ;
432432
433+ // Pre-parse the raw SVG XML for <textPath> attributes that usvg doesn't expose
434+ let textpath_attrs = pre_parse_textpath_attrs ( & svg) ;
435+
433436 // Pass identity so each leaf layer receives only its SVG-native transform from `abs_transform`.
434437 // The placement offset is then applied once to the root group layer below.
435438 import_usvg_node (
@@ -439,6 +442,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
439442 parent,
440443 insert_index,
441444 & graphite_gradient_stops,
445+ & textpath_attrs,
442446 ) ;
443447
444448 // After import, `layer_node` is set to the root group. Apply the placement transform to it
@@ -538,6 +542,55 @@ fn parse_hex_stop_color(hex: &str, opacity: f32) -> Option<Color> {
538542 Some ( Color :: from_rgbaf32_unchecked ( r, g, b, opacity) )
539543}
540544
545+ #[ derive( Debug , Default , Clone ) ]
546+ struct TextPathAttrs {
547+ pub method : Option < String > ,
548+ pub spacing : Option < String > ,
549+ pub side : Option < String > ,
550+ pub text_length : Option < f64 > ,
551+ pub length_adjust : Option < String > ,
552+ }
553+
554+ fn pre_parse_textpath_attrs ( svg : & str ) -> std:: collections:: HashMap < String , TextPathAttrs > {
555+ fn get_attr < ' a > ( attrs_str : & ' a str , name : & str ) -> Option < & ' a str > {
556+ [ '"' , '\'' ] . into_iter ( ) . find_map ( |q| {
557+ let pattern = format ! ( "{name}={q}" ) ;
558+ let rest = & attrs_str[ attrs_str. find ( & pattern) ? + pattern. len ( ) ..] ;
559+ Some ( & rest[ ..rest. find ( q) ?] )
560+ } )
561+ }
562+
563+ let mut map = std:: collections:: HashMap :: new ( ) ;
564+ let mut search = svg;
565+
566+ while let Some ( text_start) = search. find ( "<text" ) {
567+ search = & search[ text_start..] ;
568+ let tag_end = search. find ( '>' ) . unwrap_or ( search. len ( ) ) ;
569+ let text_tag = & search[ ..=tag_end] ;
570+ let text_id = get_attr ( text_tag, "id" ) . unwrap_or ( "" ) . to_string ( ) ;
571+
572+ search = & search[ tag_end. saturating_add ( 1 ) . min ( search. len ( ) ) ..] ;
573+
574+ if let Some ( tp_start) = search. find ( "<textPath" ) {
575+ let tp_end = search[ tp_start..] . find ( '>' ) . map ( |e| tp_start + e) . unwrap_or ( search. len ( ) ) ;
576+ let tp_tag = & search[ tp_start..=tp_end] ;
577+
578+ map. insert (
579+ text_id,
580+ TextPathAttrs {
581+ method : get_attr ( tp_tag, "method" ) . map ( str:: to_string) ,
582+ spacing : get_attr ( tp_tag, "spacing" ) . map ( str:: to_string) ,
583+ side : get_attr ( tp_tag, "side" ) . map ( str:: to_string) ,
584+ text_length : get_attr ( tp_tag, "textLength" ) . and_then ( |v| v. parse ( ) . ok ( ) ) ,
585+ length_adjust : get_attr ( tp_tag, "lengthAdjust" ) . map ( str:: to_string) ,
586+ } ,
587+ ) ;
588+ }
589+ }
590+
591+ map
592+ }
593+
541594/// Import a usvg node as the root of an SVG import operation.
542595///
543596/// The root layer uses the full `move_layer_to_stack` (with push/collision logic) to correctly
@@ -551,6 +604,7 @@ fn import_usvg_node(
551604 parent : LayerNodeIdentifier ,
552605 insert_index : usize ,
553606 graphite_gradient_stops : & HashMap < String , GradientStops > ,
607+ textpath_attrs : & HashMap < String , TextPathAttrs > ,
554608) {
555609 let layer = modify_inputs. create_layer ( id) ;
556610
@@ -571,7 +625,7 @@ fn import_usvg_node(
571625 modify_inputs. import = true ;
572626
573627 for child in group. children ( ) {
574- let extent = import_usvg_node_inner ( modify_inputs, child, NodeId :: new ( ) , layer, 0 , graphite_gradient_stops, & mut group_extents_map) ;
628+ let extent = import_usvg_node_inner ( modify_inputs, child, NodeId :: new ( ) , layer, 0 , graphite_gradient_stops, & mut group_extents_map, textpath_attrs ) ;
575629 child_extents_svg_order. push ( extent) ;
576630 }
577631
@@ -596,7 +650,7 @@ fn import_usvg_node(
596650 warn ! ( "Skip image" ) ;
597651 }
598652 usvg:: Node :: Text ( text) => {
599- import_usvg_text ( modify_inputs, text, node. abs_transform ( ) , layer, parent, insert_index) ;
653+ import_usvg_text ( modify_inputs, text, node. abs_transform ( ) , layer, parent, insert_index, textpath_attrs ) ;
600654 }
601655 }
602656}
@@ -614,6 +668,7 @@ fn import_usvg_node_inner(
614668 insert_index : usize ,
615669 graphite_gradient_stops : & HashMap < String , GradientStops > ,
616670 group_extents_map : & mut HashMap < LayerNodeIdentifier , Vec < u32 > > ,
671+ textpath_attrs : & HashMap < String , TextPathAttrs > ,
617672) -> u32 {
618673 let layer = modify_inputs. create_layer ( id) ;
619674 modify_inputs. network_interface . move_layer_to_stack_for_import ( layer, parent, insert_index, & [ ] ) ;
@@ -623,7 +678,7 @@ fn import_usvg_node_inner(
623678 usvg:: Node :: Group ( group) => {
624679 let mut child_extents: Vec < u32 > = Vec :: new ( ) ;
625680 for child in group. children ( ) {
626- let extent = import_usvg_node_inner ( modify_inputs, child, NodeId :: new ( ) , layer, 0 , graphite_gradient_stops, group_extents_map) ;
681+ let extent = import_usvg_node_inner ( modify_inputs, child, NodeId :: new ( ) , layer, 0 , graphite_gradient_stops, group_extents_map, textpath_attrs ) ;
627682 child_extents. push ( extent) ;
628683 }
629684 modify_inputs. layer_node = Some ( layer) ;
@@ -642,7 +697,7 @@ fn import_usvg_node_inner(
642697 0
643698 }
644699 usvg:: Node :: Text ( text) => {
645- import_usvg_text ( modify_inputs, text, node. abs_transform ( ) , layer, parent, insert_index) ;
700+ import_usvg_text ( modify_inputs, text, node. abs_transform ( ) , layer, parent, insert_index, textpath_attrs ) ;
646701 0
647702 }
648703 usvg:: Node :: Path ( path) => {
@@ -675,8 +730,12 @@ fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
675730 }
676731}
677732
678- fn import_usvg_text ( modify_inputs : & mut ModifyInputsContext , text : & usvg:: Text , transform : usvg:: Transform , layer : LayerNodeIdentifier , parent : LayerNodeIdentifier , insert_index : usize ) {
679- use graphene_std:: text:: TextAnchor ;
733+ fn import_usvg_text ( modify_inputs : & mut ModifyInputsContext , text : & usvg:: Text , transform : usvg:: Transform , layer : LayerNodeIdentifier , parent : LayerNodeIdentifier , insert_index : usize , textpath_attrs : & HashMap < String , TextPathAttrs > ) {
734+ use graphene_std:: text:: { LengthAdjust , TextAnchor , TextPathMethod , TextPathSide , TextPathSpacing } ;
735+
736+ // Look up pre-parsed textPath attrs using the <text> element id (or "" for unidentified elements)
737+ let text_id = text. id ( ) ;
738+ let tp_attrs = textpath_attrs. get ( text_id) . cloned ( ) . unwrap_or_default ( ) ;
680739
681740 for ( i, chunk) in text. chunks ( ) . iter ( ) . enumerate ( ) {
682741 let current_layer = if i == 0 {
@@ -717,7 +776,12 @@ fn import_usvg_text(modify_inputs: &mut ModifyInputsContext, text: &usvg::Text,
717776 transform. tx as f64 ,
718777 transform. ty as f64 ,
719778 ] ) ;
720- modify_inputs. insert_text_on_path ( chunk. text ( ) . to_string ( ) , font, font_size, letter_spacing, path_subpaths, start_offset, anchor, affine, current_layer) ;
779+ let method = if tp_attrs. method . as_deref ( ) == Some ( "stretch" ) { TextPathMethod :: Stretch } else { TextPathMethod :: Align } ;
780+ let spacing = if tp_attrs. spacing . as_deref ( ) == Some ( "auto" ) { TextPathSpacing :: Auto } else { TextPathSpacing :: Exact } ;
781+ let side = if tp_attrs. side . as_deref ( ) == Some ( "right" ) { TextPathSide :: Right } else { TextPathSide :: Left } ;
782+ let length_adjust = if tp_attrs. length_adjust . as_deref ( ) == Some ( "spacingAndGlyphs" ) { LengthAdjust :: SpacingAndGlyphs } else { LengthAdjust :: Spacing } ;
783+
784+ modify_inputs. insert_text_on_path ( chunk. text ( ) . to_string ( ) , font, font_size, letter_spacing, path_subpaths, start_offset, anchor, side, method, spacing, tp_attrs. text_length , length_adjust, affine, current_layer) ;
721785 modify_inputs. fill_set ( Fill :: Solid ( Color :: BLACK ) ) ;
722786 } else {
723787 // Regular text fallback
0 commit comments