Skip to content

Commit 2c13faa

Browse files
committed
Implement raw XML pre-parsing for SVG textPath import
1 parent c0cd93f commit 2c13faa

2 files changed

Lines changed: 88 additions & 16 deletions

File tree

editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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

editor/src/messages/portfolio/document/graph_operation/utility_types.rs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -299,28 +299,36 @@ impl<'a> ModifyInputsContext<'a> {
299299
path_subpaths: Vec<Subpath<PointId>>,
300300
start_offset: f64,
301301
text_anchor: TextAnchor,
302+
side: graphene_std::text::TextPathSide,
303+
method: graphene_std::text::TextPathMethod,
304+
spacing: graphene_std::text::TextPathSpacing,
305+
text_length: Option<f64>,
306+
length_adjust: graphene_std::text::LengthAdjust,
302307
transform: DAffine2,
303308
layer: LayerNodeIdentifier,
304309
) {
305-
use graphene_std::text::TextPathSide;
306-
307-
// Create the path vector object
308310
let path_vector = Table::new_from_element(Vector::from_subpaths(path_subpaths, true));
309-
310-
// Create the Text On Path node directly using the vector as a value input
311311
let text_on_path_node = resolve_proto_node_type(graphene_std::text::text_on_path::IDENTIFIER)
312312
.expect("Text On Path node does not exist")
313313
.node_template_input_override([
314314
Some(NodeInput::scope("editor-api")),
315315
Some(NodeInput::value(TaggedValue::String(text), false)),
316-
Some(NodeInput::value(TaggedValue::Vector(path_vector), false)), // path input passed as vector value directly
316+
Some(NodeInput::value(TaggedValue::Vector(path_vector), false)),
317317
Some(NodeInput::value(TaggedValue::Font(font), false)),
318318
Some(NodeInput::value(TaggedValue::F64(font_size), false)),
319319
Some(NodeInput::value(TaggedValue::F64(character_spacing), false)),
320-
Some(NodeInput::value(TaggedValue::F64(start_offset), false)), // start_offset
321-
Some(NodeInput::value(TaggedValue::Bool(false), false)), // start_offset_percent
322-
Some(NodeInput::value(TaggedValue::TextPathSide(TextPathSide::Left), false)),
320+
Some(NodeInput::value(TaggedValue::F64(start_offset), false)),
321+
Some(NodeInput::value(TaggedValue::Bool(false), false)),
322+
Some(NodeInput::value(TaggedValue::TextPathSide(side), false)),
323323
Some(NodeInput::value(TaggedValue::TextAnchor(text_anchor), false)),
324+
Some(NodeInput::value(TaggedValue::TextPathMethod(method), false)),
325+
Some(NodeInput::value(TaggedValue::TextPathSpacing(spacing), false)),
326+
Some(NodeInput::value(TaggedValue::Bool(text_length.is_some()), false)),
327+
Some(NodeInput::value(TaggedValue::F64(text_length.unwrap_or(0.0)), false)),
328+
Some(NodeInput::value(TaggedValue::LengthAdjust(length_adjust), false)),
329+
Some(NodeInput::value(TaggedValue::Bool(false), false)),
330+
Some(NodeInput::value(TaggedValue::F64(0.0), false)),
331+
Some(NodeInput::value(TaggedValue::Bool(false), false)),
324332
]);
325333

326334
let text_on_path_id = NodeId::new();

0 commit comments

Comments
 (0)