@@ -12,11 +12,10 @@ use graph_craft::document::value::TaggedValue;
1212use graph_craft:: document:: { NodeId , NodeInput } ;
1313use graphene_std:: Color ;
1414use graphene_std:: renderer:: Quad ;
15- use graphene_std:: renderer:: convert_usvg_path:: convert_usvg_path;
15+ use graphene_std:: renderer:: convert_usvg_path:: { convert_tiny_skia_path , convert_usvg_path} ;
1616use graphene_std:: table:: Table ;
1717use graphene_std:: text:: { Font , TypesettingConfig } ;
1818use graphene_std:: vector:: style:: { Fill , Gradient , GradientStop , GradientStops , GradientType , PaintOrder , Stroke , StrokeAlign , StrokeCap , StrokeJoin } ;
19-
2019#[ derive( ExtractField ) ]
2120pub struct GraphOperationMessageContext < ' a > {
2221 pub network_interface : & ' a mut NodeNetworkInterface ,
@@ -394,7 +393,14 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
394393 insert_index,
395394 center,
396395 } => {
397- let tree = match usvg:: Tree :: from_str ( & svg, & usvg:: Options :: default ( ) ) {
396+ let mut options = usvg:: Options :: default ( ) ;
397+ options. fontdb_mut ( ) . load_font_data ( include_bytes ! ( "../overlays/source-sans-pro-regular.ttf" ) . to_vec ( ) ) ;
398+ options. font_family = "Source Sans Pro" . to_string ( ) ;
399+
400+ let svg = svg. replace ( "font-family=\" sans-serif\" " , "font-family=\" Source Sans Pro\" " ) ;
401+ let svg = svg. replace ( "font-family='sans-serif'" , "font-family='Source Sans Pro'" ) ;
402+
403+ let tree = match usvg:: Tree :: from_str ( & svg, & options) {
398404 Ok ( t) => t,
399405 Err ( e) => {
400406 responses. add ( DialogMessage :: DisplayDialogError {
@@ -424,6 +430,9 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
424430
425431 let graphite_gradient_stops = extract_graphite_gradient_stops ( & svg) ;
426432
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+
427436 // Pass identity so each leaf layer receives only its SVG-native transform from `abs_transform`.
428437 // The placement offset is then applied once to the root group layer below.
429438 import_usvg_node (
@@ -433,6 +442,7 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
433442 parent,
434443 insert_index,
435444 & graphite_gradient_stops,
445+ & textpath_attrs,
436446 ) ;
437447
438448 // After import, `layer_node` is set to the root group. Apply the placement transform to it
@@ -532,6 +542,55 @@ fn parse_hex_stop_color(hex: &str, opacity: f32) -> Option<Color> {
532542 Some ( Color :: from_rgbaf32_unchecked ( r, g, b, opacity) )
533543}
534544
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+
535594/// Import a usvg node as the root of an SVG import operation.
536595///
537596/// The root layer uses the full `move_layer_to_stack` (with push/collision logic) to correctly
@@ -545,6 +604,7 @@ fn import_usvg_node(
545604 parent : LayerNodeIdentifier ,
546605 insert_index : usize ,
547606 graphite_gradient_stops : & HashMap < String , GradientStops > ,
607+ textpath_attrs : & HashMap < String , TextPathAttrs > ,
548608) {
549609 let layer = modify_inputs. create_layer ( id) ;
550610
@@ -565,7 +625,7 @@ fn import_usvg_node(
565625 modify_inputs. import = true ;
566626
567627 for child in group. children ( ) {
568- 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 ) ;
569629 child_extents_svg_order. push ( extent) ;
570630 }
571631
@@ -590,9 +650,7 @@ fn import_usvg_node(
590650 warn ! ( "Skip image" ) ;
591651 }
592652 usvg:: Node :: Text ( text) => {
593- let font = Font :: new ( graphene_std:: consts:: DEFAULT_FONT_FAMILY . to_string ( ) , graphene_std:: consts:: DEFAULT_FONT_STYLE . to_string ( ) ) ;
594- modify_inputs. insert_text ( text. chunks ( ) . iter ( ) . map ( |chunk| chunk. text ( ) ) . collect ( ) , font, TypesettingConfig :: default ( ) , layer) ;
595- modify_inputs. fill_set ( Fill :: Solid ( Color :: BLACK ) ) ;
653+ import_usvg_text ( modify_inputs, text, node. abs_transform ( ) , layer, parent, insert_index, textpath_attrs) ;
596654 }
597655 }
598656}
@@ -610,6 +668,7 @@ fn import_usvg_node_inner(
610668 insert_index : usize ,
611669 graphite_gradient_stops : & HashMap < String , GradientStops > ,
612670 group_extents_map : & mut HashMap < LayerNodeIdentifier , Vec < u32 > > ,
671+ textpath_attrs : & HashMap < String , TextPathAttrs > ,
613672) -> u32 {
614673 let layer = modify_inputs. create_layer ( id) ;
615674 modify_inputs. network_interface . move_layer_to_stack_for_import ( layer, parent, insert_index, & [ ] ) ;
@@ -619,7 +678,7 @@ fn import_usvg_node_inner(
619678 usvg:: Node :: Group ( group) => {
620679 let mut child_extents: Vec < u32 > = Vec :: new ( ) ;
621680 for child in group. children ( ) {
622- 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 ) ;
623682 child_extents. push ( extent) ;
624683 }
625684 modify_inputs. layer_node = Some ( layer) ;
@@ -633,24 +692,21 @@ fn import_usvg_node_inner(
633692 group_extents_map. insert ( layer, child_extents) ;
634693 total_extent
635694 }
636- usvg:: Node :: Path ( path) => {
637- import_usvg_path ( modify_inputs, node, path, layer, graphite_gradient_stops) ;
638- 0
639- }
640695 usvg:: Node :: Image ( _image) => {
641696 warn ! ( "Skip image" ) ;
642697 0
643698 }
644699 usvg:: Node :: Text ( text) => {
645- let font = Font :: new ( graphene_std:: consts:: DEFAULT_FONT_FAMILY . to_string ( ) , graphene_std:: consts:: DEFAULT_FONT_STYLE . to_string ( ) ) ;
646- modify_inputs. insert_text ( text. chunks ( ) . iter ( ) . map ( |chunk| chunk. text ( ) ) . collect ( ) , font, TypesettingConfig :: default ( ) , layer) ;
647- modify_inputs. fill_set ( Fill :: Solid ( Color :: BLACK ) ) ;
700+ import_usvg_text ( modify_inputs, text, node. abs_transform ( ) , layer, parent, insert_index, textpath_attrs) ;
701+ 0
702+ }
703+ usvg:: Node :: Path ( path) => {
704+ import_usvg_path ( modify_inputs, node, path, layer, graphite_gradient_stops) ;
648705 0
649706 }
650707 }
651708}
652709
653- /// Helper to apply path data (vector geometry, fill, stroke, transform) to a layer.
654710fn import_usvg_path ( modify_inputs : & mut ModifyInputsContext , node : & usvg:: Node , path : & usvg:: Path , layer : LayerNodeIdentifier , graphite_gradient_stops : & HashMap < String , GradientStops > ) {
655711 let subpaths = convert_usvg_path ( path) ;
656712 let bounds = subpaths. iter ( ) . filter_map ( |subpath| subpath. bounding_box ( ) ) . reduce ( Quad :: combine_bounds) . unwrap_or_default ( ) ;
@@ -674,6 +730,67 @@ fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
674730 }
675731}
676732
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 ( ) ;
739+
740+ for ( i, chunk) in text. chunks ( ) . iter ( ) . enumerate ( ) {
741+ let current_layer = if i == 0 {
742+ layer
743+ } else {
744+ let new_id = NodeId :: new ( ) ;
745+ let new_layer = modify_inputs. create_layer ( new_id) ;
746+ modify_inputs. network_interface . move_layer_to_stack_for_import ( new_layer, parent, insert_index, & [ ] ) ;
747+ new_layer
748+ } ;
749+ modify_inputs. layer_node = Some ( current_layer) ;
750+
751+ let font_family = chunk
752+ . spans ( )
753+ . first ( )
754+ . and_then ( |span| span. font ( ) . families ( ) . first ( ) . map ( |f| f. to_string ( ) ) )
755+ . unwrap_or_else ( || graphene_std:: consts:: DEFAULT_FONT_FAMILY . to_string ( ) ) ;
756+ let font_style = graphene_std:: consts:: DEFAULT_FONT_STYLE . to_string ( ) ;
757+ let font = Font :: new ( font_family, font_style) ;
758+
759+ let font_size = chunk. spans ( ) . first ( ) . map ( |s| s. font_size ( ) . get ( ) ) . unwrap_or ( 24.0 ) as f64 ;
760+ let letter_spacing = chunk. spans ( ) . first ( ) . map ( |s| s. letter_spacing ( ) ) . unwrap_or ( 0.0 ) as f64 ;
761+
762+ if let usvg:: TextFlow :: Path ( text_path) = chunk. text_flow ( ) {
763+ let path_subpaths = convert_tiny_skia_path ( text_path. path ( ) ) ;
764+ let start_offset = text_path. start_offset ( ) as f64 ;
765+ let anchor = match chunk. anchor ( ) {
766+ usvg:: TextAnchor :: Start => TextAnchor :: Start ,
767+ usvg:: TextAnchor :: Middle => TextAnchor :: Middle ,
768+ usvg:: TextAnchor :: End => TextAnchor :: End ,
769+ } ;
770+
771+ let affine = DAffine2 :: from_cols_array ( & [
772+ transform. sx as f64 ,
773+ transform. ky as f64 ,
774+ transform. kx as f64 ,
775+ transform. sy as f64 ,
776+ transform. tx as f64 ,
777+ transform. ty as f64 ,
778+ ] ) ;
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) ;
785+ modify_inputs. fill_set ( Fill :: Solid ( Color :: BLACK ) ) ;
786+ } else {
787+ // Regular text fallback
788+ modify_inputs. insert_text ( chunk. text ( ) . to_string ( ) , font, TypesettingConfig { font_size, ..Default :: default ( ) } , current_layer) ;
789+ modify_inputs. fill_set ( Fill :: Solid ( Color :: BLACK ) ) ;
790+ }
791+ }
792+ }
793+
677794/// Set correct positions for all imported layers in a single top-down O(n) pass.
678795///
679796/// For each group's child stack:
0 commit comments