Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions crates/resvg/tests/integration/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1603,6 +1603,13 @@ use crate::render;
#[test] fn text_textPath_dy_with_tiny_coordinates() { assert_eq!(render("tests/text/textPath/dy-with-tiny-coordinates"), 0); }
#[test] fn text_textPath_invalid_link() { assert_eq!(render("tests/text/textPath/invalid-link"), 0); }
#[test] fn text_textPath_invalid_textPath_in_the_middle() { assert_eq!(render("tests/text/textPath/invalid-textPath-in-the-middle"), 0); }
#[test] fn text_textPath_link_to_circle_with_href() { assert_eq!(render("tests/text/textPath/link-to-circle-with-href"), 0); }
#[test] fn text_textPath_link_to_ellipse_with_href() { assert_eq!(render("tests/text/textPath/link-to-ellipse-with-href"), 0); }
#[test] fn text_textPath_link_to_line_with_href() { assert_eq!(render("tests/text/textPath/link-to-line-with-href"), 0); }
#[test] fn text_textPath_link_to_path_with_href() { assert_eq!(render("tests/text/textPath/link-to-path-with-href"), 0); }
#[test] fn text_textPath_link_to_polygon_with_href() { assert_eq!(render("tests/text/textPath/link-to-polygon-with-href"), 0); }
#[test] fn text_textPath_link_to_polyline_with_href() { assert_eq!(render("tests/text/textPath/link-to-polyline-with-href"), 0); }
#[test] fn text_textPath_link_to_rect_with_href() { assert_eq!(render("tests/text/textPath/link-to-rect-with-href"), 0); }
#[test] fn text_textPath_link_to_rect() { assert_eq!(render("tests/text/textPath/link-to-rect"), 0); }
#[test] fn text_textPath_m_A_path() { assert_eq!(render("tests/text/textPath/m-A-path"), 0); }
#[test] fn text_textPath_m_L_Z_path() { assert_eq!(render("tests/text/textPath/m-L-Z-path"), 0); }
Expand Down Expand Up @@ -1631,10 +1638,13 @@ use crate::render;
#[test] fn text_textPath_with_big_letter_spacing() { assert_eq!(render("tests/text/textPath/with-big-letter-spacing"), 0); }
#[test] fn text_textPath_with_coordinates_on_text() { assert_eq!(render("tests/text/textPath/with-coordinates-on-text"), 0); }
#[test] fn text_textPath_with_coordinates_on_textPath() { assert_eq!(render("tests/text/textPath/with-coordinates-on-textPath"), 0); }
#[test] fn text_textPath_with_empty_path_fallback() { assert_eq!(render("tests/text/textPath/with-empty-path-fallback"), 0); }
#[test] fn text_textPath_with_filter() { assert_eq!(render("tests/text/textPath/with-filter"), 0); }
#[test] fn text_textPath_with_href_and_xlink_href() { assert_eq!(render("tests/text/textPath/with-href-and-xlink-href"), 0); }
#[test] fn text_textPath_with_invalid_path_and_xlink_href() { assert_eq!(render("tests/text/textPath/with-invalid-path-and-xlink-href"), 0); }
#[test] fn text_textPath_with_letter_spacing() { assert_eq!(render("tests/text/textPath/with-letter-spacing"), 0); }
#[test] fn text_textPath_with_path_and_xlink_href() { assert_eq!(render("tests/text/textPath/with-path-and-xlink-href"), 0); }
#[test] fn text_textPath_with_error_path() { assert_eq!(render("tests/text/textPath/with-error-path"), 0); }
#[test] fn text_textPath_with_path() { assert_eq!(render("tests/text/textPath/with-path"), 0); }
#[test] fn text_textPath_with_rotate() { assert_eq!(render("tests/text/textPath/with-rotate"), 0); }
#[test] fn text_textPath_with_text_anchor() { assert_eq!(render("tests/text/textPath/with-text-anchor"), 0); }
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions crates/resvg/tests/tests/text/textPath/link-to-line-with-href.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions crates/resvg/tests/tests/text/textPath/link-to-path-with-href.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions crates/resvg/tests/tests/text/textPath/link-to-rect-with-href.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions crates/resvg/tests/tests/text/textPath/with-error-path.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This oen looks wrong?

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified crates/resvg/tests/tests/text/textPath/with-path.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 18 additions & 15 deletions crates/usvg/src/parser/converter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ pub struct Cache {
radial_gradient_index: usize,
pattern_index: usize,
clip_path_index: usize,
text_path_index: usize,
mask_index: usize,
filter_index: usize,
image_index: usize,
Expand Down Expand Up @@ -120,6 +121,7 @@ impl Cache {
radial_gradient_index: 0,
pattern_index: 0,
clip_path_index: 0,
text_path_index: 0,
mask_index: 0,
filter_index: 0,
image_index: 0,
Expand Down Expand Up @@ -171,6 +173,17 @@ impl Cache {
}
}

pub(crate) fn gen_text_path_id(&mut self) -> NonEmptyString {
loop {
self.text_path_index += 1;
let new_id = format!("textPathPath{}", self.text_path_index);
let new_hash = string_hash(&new_id);
if !self.all_ids.contains(&new_hash) {
return NonEmptyString::new(new_id).unwrap();
}
}
}

pub(crate) fn gen_mask_id(&mut self) -> NonEmptyString {
loop {
self.mask_index += 1;
Expand Down Expand Up @@ -396,22 +409,12 @@ pub(crate) fn convert_doc(svg_doc: &svgtree::Document, opt: &Options) -> Result<
opt.fontdb.clone(),
);

// ID generators use this set for collision checks.
// For synthesized text-path IDs we need to avoid conflicts with *any* existing SVG element ID,
// so we collect all non-empty IDs, not only clip/filter/paint/image resources.
for node in svg_doc.descendants() {
if let Some(tag) = node.tag_name() {
if matches!(
tag,
EId::ClipPath
| EId::Filter
| EId::LinearGradient
| EId::Mask
| EId::Pattern
| EId::RadialGradient
| EId::Image
) {
if !node.element_id().is_empty() {
cache.all_ids.insert(string_hash(node.element_id()));
}
}
if !node.element_id().is_empty() {
cache.all_ids.insert(string_hash(node.element_id()));
}
}

Expand Down
4 changes: 4 additions & 0 deletions crates/usvg/src/parser/shapes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ pub(crate) fn convert(node: SvgNode, state: &converter::State) -> Option<Arc<Pat

pub(crate) fn convert_path(node: SvgNode) -> Option<Arc<Path>> {
let value: &str = node.attribute(AId::D)?;
convert_path_data(value)
}

pub(crate) fn convert_path_data(value: &str) -> Option<Arc<Path>> {
let mut builder = tiny_skia_path::PathBuilder::new();
for segment in svgtypes::SimplifyingPathParser::from(value) {
let segment = match segment {
Expand Down
64 changes: 49 additions & 15 deletions crates/usvg/src/parser/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ fn collect_text_chunks_impl(
continue;
}

match resolve_text_flow(child, state) {
match resolve_text_flow(child, state, cache) {
Some(v) => {
iter_state.text_flow = v;
}
Expand Down Expand Up @@ -362,19 +362,12 @@ fn collect_text_chunks_impl(
}
}

fn resolve_text_flow(node: SvgNode, state: &converter::State) -> Option<TextFlow> {
let linked_node = node.attribute::<SvgNode>(AId::Href)?;
let path = super::shapes::convert(linked_node, state)?;

// The reference path's transform needs to be applied
let transform = linked_node.resolve_transform(AId::Transform, state);
let path = if !transform.is_identity() {
let mut path_copy = path.as_ref().clone();
path_copy = path_copy.transform(transform)?;
Arc::new(path_copy)
} else {
path
};
fn resolve_text_flow(
node: SvgNode,
state: &converter::State,
cache: &mut converter::Cache,
) -> Option<TextFlow> {
let (id, path) = resolve_text_flow_path(node, state, cache)?;

let start_offset: Length = node.attribute(AId::StartOffset).unwrap_or_default();
let start_offset = if start_offset.unit == LengthUnit::Percent {
Expand All @@ -386,14 +379,55 @@ fn resolve_text_flow(node: SvgNode, state: &converter::State) -> Option<TextFlow
node.resolve_length(AId::StartOffset, state, 0.0)
};

let id = NonEmptyString::new(linked_node.element_id().to_string())?;
Some(TextFlow::Path(Arc::new(TextPath {
id,
start_offset,
path,
})))
}

fn resolve_text_flow_path(
node: SvgNode,
state: &converter::State,
cache: &mut converter::Cache,
) -> Option<(NonEmptyString, Arc<tiny_skia_path::Path>)> {
// SVG 2: when both `path` and `href` are present, inline `path` has precedence.
// If `path` is missing or invalid, fallback to `href`.
if let Some(path_data) = node.attribute::<&str>(AId::Path) {
if let Some(path) = super::shapes::convert_path_data(path_data) {
// usvg serializes text-on-path via a referenced `<path id="...">` in `<defs>`.
// So an inline `textPath path="..."` without its own ID still needs
// an internal stable ID for `<textPath xlink:href="#...">` on write.
let id = NonEmptyString::new(node.element_id().to_string())
.unwrap_or_else(|| cache.gen_text_path_id());
return Some((id, path));
}
}

resolve_text_flow_href(node, state)
}

fn resolve_text_flow_href(
node: SvgNode,
state: &converter::State,
) -> Option<(NonEmptyString, Arc<tiny_skia_path::Path>)> {
let linked_node = node.attribute::<SvgNode>(AId::Href)?;
let path = super::shapes::convert(linked_node, state)?;

// The reference path's transform needs to be applied
let transform = linked_node.resolve_transform(AId::Transform, state);
let path = if !transform.is_identity() {
let mut path_copy = path.as_ref().clone();
path_copy = path_copy.transform(transform)?;
Arc::new(path_copy)
} else {
path
};

let id = NonEmptyString::new(linked_node.element_id().to_string())?;
Some((id, path))
}

fn convert_font(node: SvgNode, state: &converter::State) -> Font {
let style: FontStyle = node.find_attribute(AId::FontStyle).unwrap_or_default();
let stretch = conv_font_stretch(node);
Expand Down
Loading