@@ -394,8 +394,11 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
394394 center,
395395 } => {
396396 let mut options = usvg:: Options :: default ( ) ;
397- options. fontdb_mut ( ) . load_font_data ( include_bytes ! ( "../overlays/source-sans-pro-regular.ttf" ) . to_vec ( ) ) ;
398397 options. font_family = "Source Sans Pro" . to_string ( ) ;
398+ let fontdb = options. fontdb_mut ( ) ;
399+ fontdb. load_font_data ( include_bytes ! ( "../overlays/source-sans-pro-regular.ttf" ) . to_vec ( ) ) ;
400+ fontdb. set_serif_family ( "Source Sans Pro" ) ;
401+ fontdb. set_sans_serif_family ( "Source Sans Pro" ) ;
399402
400403 let svg = svg. replace ( "font-family=\" sans-serif\" " , "font-family=\" Source Sans Pro\" " ) ;
401404 let svg = svg. replace ( "font-family='sans-serif'" , "font-family='Source Sans Pro'" ) ;
@@ -473,6 +476,7 @@ fn usvg_transform(c: usvg::Transform) -> DAffine2 {
473476}
474477
475478const GRAPHITE_NAMESPACE : & str = "https://graphite.art" ;
479+ const XLINK_NAMESPACE : & str = "http://www.w3.org/1999/xlink" ;
476480
477481/// Pre-parses the raw SVG XML to extract gradient stops that have `graphite:midpoint` attributes.
478482/// Graphite exports gradients with midpoint curve data by writing interpolated approximation stops
@@ -559,8 +563,10 @@ fn pre_parse_textpath_attrs(svg: &str) -> std::collections::HashMap<String, Vec<
559563 } ;
560564 for node in doc. descendants ( ) {
561565 if node. tag_name ( ) . name ( ) == "textPath" {
562- let id = node. attribute ( "id" ) . unwrap_or ( "" ) . to_string ( ) ;
563- map. entry ( id) . or_default ( ) . push ( TextPathAttrs {
566+ let Some ( path_id) = textpath_href_id ( node) else {
567+ continue ;
568+ } ;
569+ map. entry ( path_id) . or_default ( ) . push ( TextPathAttrs {
564570 method : node. attribute ( "method" ) . map ( str:: to_string) ,
565571 spacing : node. attribute ( "spacing" ) . map ( str:: to_string) ,
566572 side : node. attribute ( "side" ) . map ( str:: to_string) ,
@@ -572,6 +578,13 @@ fn pre_parse_textpath_attrs(svg: &str) -> std::collections::HashMap<String, Vec<
572578 map
573579}
574580
581+ fn textpath_href_id ( node : usvg:: roxmltree:: Node ) -> Option < String > {
582+ node. attribute ( ( XLINK_NAMESPACE , "href" ) )
583+ . or_else ( || node. attribute ( "href" ) )
584+ . and_then ( |href| href. strip_prefix ( '#' ) )
585+ . map ( str:: to_string)
586+ }
587+
575588/// Import a usvg node as the root of an SVG import operation.
576589///
577590/// The root layer uses the full `move_layer_to_stack` (with push/collision logic) to correctly
@@ -625,12 +638,14 @@ fn import_usvg_node(
625638 modify_inputs. network_interface . unload_all_nodes_bounding_box ( & [ ] ) ;
626639 }
627640 usvg:: Node :: Path ( path) => {
641+ log:: info!( "Importing node as Path: id={}" , node. id( ) ) ;
628642 import_usvg_path ( modify_inputs, node, path, layer, graphite_gradient_stops) ;
629643 }
630644 usvg:: Node :: Image ( _image) => {
631645 warn ! ( "Skip image" ) ;
632646 }
633647 usvg:: Node :: Text ( text) => {
648+ log:: info!( "Importing node as Text: id={}" , node. id( ) ) ;
634649 import_usvg_text ( modify_inputs, text, node. abs_transform ( ) , layer, parent, insert_index, textpath_attrs) ;
635650 }
636651 }
@@ -711,8 +726,16 @@ fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node,
711726 }
712727}
713728
714- fn import_usvg_text ( modify_inputs : & mut ModifyInputsContext , text : & usvg:: Text , transform : usvg:: Transform , layer : LayerNodeIdentifier , parent : LayerNodeIdentifier , insert_index : usize , textpath_attrs : & mut HashMap < String , Vec < TextPathAttrs > > ) {
715- use graphene_std:: text:: { LengthAdjust , TextAnchor , TextPathMethod , TextPathSide , TextPathSpacing } ;
729+ fn import_usvg_text (
730+ modify_inputs : & mut ModifyInputsContext ,
731+ text : & usvg:: Text ,
732+ transform : usvg:: Transform ,
733+ layer : LayerNodeIdentifier ,
734+ parent : LayerNodeIdentifier ,
735+ insert_index : usize ,
736+ textpath_attrs : & mut HashMap < String , Vec < TextPathAttrs > > ,
737+ ) {
738+ log:: info!( "Importing usvg text node with {} chunks" , text. chunks( ) . len( ) ) ;
716739
717740 for ( i, chunk) in text. chunks ( ) . iter ( ) . enumerate ( ) {
718741 let current_layer = if i == 0 {
@@ -738,38 +761,79 @@ fn import_usvg_text(modify_inputs: &mut ModifyInputsContext, text: &usvg::Text,
738761
739762 if let usvg:: TextFlow :: Path ( text_path) = chunk. text_flow ( ) {
740763 let tp_id = text_path. id ( ) ;
741- let tp_attrs = textpath_attrs . get_mut ( tp_id ) . and_then ( |vec| if !vec . is_empty ( ) { Some ( vec . remove ( 0 ) ) } else { None } ) . unwrap_or_default ( ) ;
764+ let tp_attrs = take_textpath_attrs ( textpath_attrs , tp_id ) ;
742765 let path_subpaths = convert_tiny_skia_path ( text_path. path ( ) ) ;
743766 let start_offset = text_path. start_offset ( ) as f64 ;
744- let anchor = match chunk. anchor ( ) {
745- usvg:: TextAnchor :: Start => TextAnchor :: Start ,
746- usvg:: TextAnchor :: Middle => TextAnchor :: Middle ,
747- usvg:: TextAnchor :: End => TextAnchor :: End ,
748- } ;
749767
750- let affine = DAffine2 :: from_cols_array ( & [
751- transform. sx as f64 ,
752- transform. ky as f64 ,
753- transform. kx as f64 ,
754- transform. sy as f64 ,
755- transform. tx as f64 ,
756- transform. ty as f64 ,
757- ] ) ;
758- let method = if tp_attrs. method . as_deref ( ) == Some ( "stretch" ) { TextPathMethod :: Stretch } else { TextPathMethod :: Align } ;
759- let spacing = if tp_attrs. spacing . as_deref ( ) == Some ( "auto" ) { TextPathSpacing :: Auto } else { TextPathSpacing :: Exact } ;
760- let side = if tp_attrs. side . as_deref ( ) == Some ( "right" ) { TextPathSide :: Right } else { TextPathSide :: Left } ;
761- let length_adjust = if tp_attrs. length_adjust . as_deref ( ) == Some ( "spacingAndGlyphs" ) { LengthAdjust :: SpacingAndGlyphs } else { LengthAdjust :: Spacing } ;
762-
763- 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) ;
764- modify_inputs. fill_set ( Fill :: Solid ( Color :: BLACK ) ) ;
768+ modify_inputs. insert_text_on_path (
769+ chunk. text ( ) . to_string ( ) ,
770+ font,
771+ font_size,
772+ letter_spacing,
773+ path_subpaths,
774+ start_offset,
775+ text_anchor ( chunk. anchor ( ) ) ,
776+ text_path_side ( & tp_attrs) ,
777+ text_path_method ( & tp_attrs) ,
778+ text_path_spacing ( & tp_attrs) ,
779+ tp_attrs. text_length ,
780+ text_length_adjust ( & tp_attrs) ,
781+ usvg_transform ( transform) ,
782+ current_layer,
783+ ) ;
784+ if let Some ( fill) = chunk. spans ( ) . first ( ) . and_then ( |span| span. fill ( ) ) {
785+ apply_usvg_fill ( fill, modify_inputs, DAffine2 :: IDENTITY , & HashMap :: new ( ) ) ;
786+ }
765787 } else {
766788 // Regular text fallback
767789 modify_inputs. insert_text ( chunk. text ( ) . to_string ( ) , font, TypesettingConfig { font_size, ..Default :: default ( ) } , current_layer) ;
768- modify_inputs. fill_set ( Fill :: Solid ( Color :: BLACK ) ) ;
790+ if let Some ( fill) = chunk. spans ( ) . first ( ) . and_then ( |span| span. fill ( ) ) {
791+ apply_usvg_fill ( fill, modify_inputs, DAffine2 :: IDENTITY , & HashMap :: new ( ) ) ;
792+ }
769793 }
770794 }
771795}
772796
797+ fn take_textpath_attrs ( textpath_attrs : & mut HashMap < String , Vec < TextPathAttrs > > , path_id : & str ) -> TextPathAttrs {
798+ textpath_attrs. get_mut ( path_id) . and_then ( |attrs| ( !attrs. is_empty ( ) ) . then ( || attrs. remove ( 0 ) ) ) . unwrap_or_default ( )
799+ }
800+
801+ fn text_anchor ( anchor : usvg:: TextAnchor ) -> graphene_std:: text:: TextAnchor {
802+ match anchor {
803+ usvg:: TextAnchor :: Start => graphene_std:: text:: TextAnchor :: Start ,
804+ usvg:: TextAnchor :: Middle => graphene_std:: text:: TextAnchor :: Middle ,
805+ usvg:: TextAnchor :: End => graphene_std:: text:: TextAnchor :: End ,
806+ }
807+ }
808+
809+ fn text_path_side ( attrs : & TextPathAttrs ) -> graphene_std:: text:: TextPathSide {
810+ match attrs. side . as_deref ( ) {
811+ Some ( "right" ) => graphene_std:: text:: TextPathSide :: Right ,
812+ _ => graphene_std:: text:: TextPathSide :: Left ,
813+ }
814+ }
815+
816+ fn text_path_method ( attrs : & TextPathAttrs ) -> graphene_std:: text:: TextPathMethod {
817+ match attrs. method . as_deref ( ) {
818+ Some ( "stretch" ) => graphene_std:: text:: TextPathMethod :: Stretch ,
819+ _ => graphene_std:: text:: TextPathMethod :: Align ,
820+ }
821+ }
822+
823+ fn text_path_spacing ( attrs : & TextPathAttrs ) -> graphene_std:: text:: TextPathSpacing {
824+ match attrs. spacing . as_deref ( ) {
825+ Some ( "auto" ) => graphene_std:: text:: TextPathSpacing :: Auto ,
826+ _ => graphene_std:: text:: TextPathSpacing :: Exact ,
827+ }
828+ }
829+
830+ fn text_length_adjust ( attrs : & TextPathAttrs ) -> graphene_std:: text:: LengthAdjust {
831+ match attrs. length_adjust . as_deref ( ) {
832+ Some ( "spacingAndGlyphs" ) => graphene_std:: text:: LengthAdjust :: SpacingAndGlyphs ,
833+ _ => graphene_std:: text:: LengthAdjust :: Spacing ,
834+ }
835+ }
836+
773837/// Set correct positions for all imported layers in a single top-down O(n) pass.
774838///
775839/// For each group's child stack:
0 commit comments