Skip to content
Closed
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
46 changes: 46 additions & 0 deletions crates/resvg/src/render/text.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// ...existing code...

fn render_text_path(
canvas: &mut Canvas,
tree: &usvg::Tree,
text_path: &usvg::TextPath,
chunks: &[TextChunk],
span_transform: Transform,
font_db: &fontdb::Database,
) -> Option<()> {
// The path is already stored in the TextPath object
let path = &text_path.path;

// Render the text along the path using the path directly
if path.is_empty() {
return None;
}

// Calculate the path length to position text
let path_len = path_length(path);
if path_len <= 0.0 {
return None;
}

// Position text at the start_offset along the path
let mut offset = text_path.start_offset;
if offset < 0.0 {
offset = 0.0;
} else if offset > path_len as f32 {
offset = path_len as f32;
}

// Use the path directly for layout
layout_text_on_path(
canvas,
tree,
path,
offset,
chunks,
span_transform,
font_db,
)
}

// ...existing code...

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions crates/resvg/tests/text/textPath/m-A-path.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions crates/usvg/src/layout/text/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// If chunks is empty then we have just a whitespace.
if chunks.is_empty() {
return None;
}

let base_transform = text.abs_transform();
let mut spans = Vec::new();

let mut iter = chunks.iter();
while let Some(chunk) = iter.next() {
// Skip empty text chunks
if chunk.text.is_empty() {
continue;
}

let mut chunk_spans = match &chunk.text_flow {
19 changes: 19 additions & 0 deletions crates/usvg/src/layout/text/path.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
if segments.is_empty() {
return None;
}

// Find a total offset along the path.
let path_len = path_length(path.as_ref()) as f32;
if path_len <= 0.0 {
return None; // Can't layout text on a zero-length path
}

let mut offset = text_path.start_offset;
// Ensure offset is within valid range
if offset < 0.0 {
offset = 0.0;
} else if offset > path_len {
offset = path_len;
}

let mut spans = Vec::new();
7 changes: 7 additions & 0 deletions crates/usvg/src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ mod style;
mod svgtree;
mod switch;
mod units;

// Helper function to generate unique IDs
pub(crate) fn gen_id() -> u64 {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(1);
COUNTER.fetch_add(1, Ordering::Relaxed)
}
mod use_node;

#[cfg(feature = "text")]
Expand Down
157 changes: 154 additions & 3 deletions crates/usvg/src/parser/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,23 @@ fn collect_text_chunks_impl(
}

fn resolve_text_flow(node: SvgNode, state: &converter::State) -> Option<TextFlow> {
// First try the path attribute (SVG 2.0 feature)
if let Some(path_data) = node.attribute::<&str>(AId::Path) {
// Parse the path data into segments
let mut path_segments = Vec::new();
for segment in svgtypes::SimplifyingPathParser::from(path_data) {
let segment = match segment {
Ok(v) => v,
Err(_) => return None, // Invalid path data
};
path_segments.push(segment);
}

// Use the helper function to create a TextFlow from the path data
return create_text_path_from_data(node, path_segments, state);
}

// Fallback to xlink:href attribute (SVG 1.1 feature)
let linked_node = node.attribute::<SvgNode>(AId::Href)?;
let path = super::shapes::convert(linked_node, state)?;

Expand All @@ -375,7 +392,7 @@ fn resolve_text_flow(node: SvgNode, state: &converter::State) -> Option<TextFlow
// 'If a percentage is given, then the `startOffset` represents
// a percentage distance along the entire path.'
let path_len = path_length(&path);
(path_len * (start_offset.number / 100.0)) as f32
(path_len * (start_offset.number as f32 / 100.0)) as f32
} else {
node.resolve_length(AId::StartOffset, state, 0.0)
};
Expand All @@ -393,8 +410,10 @@ fn convert_font(node: SvgNode, state: &converter::State) -> Font {
let stretch = conv_font_stretch(node);
let weight = resolve_font_weight(node);

let font_families = if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::FontFamily))
{
// Check the current node first, then fall back to ancestors
let font_families = if node.has_attribute(AId::FontFamily) {
node.attribute(AId::FontFamily).unwrap_or("")
} else if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::FontFamily)) {
n.attribute(AId::FontFamily).unwrap_or("")
} else {
""
Expand Down Expand Up @@ -770,6 +789,138 @@ fn convert_writing_mode(text_node: SvgNode) -> WritingMode {
}
}

// Define PathData type for working with path data
type PathData = Vec<svgtypes::SimplePathSegment>;

// Helper function to process path segments
fn apply_path_segment(
builder: &mut tiny_skia_path::PathBuilder,
segment: svgtypes::SimplePathSegment,
prev_mp: Option<(f32, f32)>
) -> Option<(f32, f32)> {
match segment {
svgtypes::SimplePathSegment::MoveTo { x, y } => {
builder.move_to(x as f32, y as f32);
Some((x as f32, y as f32))
}
svgtypes::SimplePathSegment::LineTo { x, y } => {
builder.line_to(x as f32, y as f32);
Some((x as f32, y as f32))
}
svgtypes::SimplePathSegment::Quadratic { x1, y1, x, y } => {
builder.quad_to(x1 as f32, y1 as f32, x as f32, y as f32);
Some((x as f32, y as f32))
}
svgtypes::SimplePathSegment::CurveTo { x1, y1, x2, y2, x, y } => {
builder.cubic_to(
x1 as f32, y1 as f32, x2 as f32, y2 as f32, x as f32, y as f32,
);
Some((x as f32, y as f32))
}
svgtypes::SimplePathSegment::ClosePath => {
builder.close();
prev_mp
}
}
}

// Helper function to create a TextFlow::Path from path data directly
fn create_text_path_from_data(node: SvgNode, path_data: PathData, state: &converter::State) -> Option<TextFlow> {
// Build the path
let mut builder = tiny_skia_path::PathBuilder::new();
let mut prev_mp = None;
for segment in path_data.iter() {
if let Some(mp) = apply_path_segment(&mut builder, segment, prev_mp) {
prev_mp = Some(mp);
}
}

// Some valid paths can still lead to an invalid `Path`.
let path = match builder.finish() {
Some(path) => path,
None => return Some(TextFlow::Linear), // If path creation fails, fall back to linear
};

// Don't proceed with empty paths
if path.is_empty() {
return Some(TextFlow::Linear);
}

let path = Arc::new(path);

// Generate a unique ID for the path
let path_id = NonEmptyString::new(format!("textPath{}", super::string_hash(node.element_id())))?;

// Ensure the path is valid before creating the TextPath
if path.is_empty() {
return Some(TextFlow::Linear); // Return linear flow if path is empty
}

// Create the TextPath with valid path and return
// Create and return the text path
Some(TextFlow::Path(Arc::new(TextPath {
id: path_id,
start_offset,
path,
})))
}

fn create_text_path_from_data(
node: SvgNode,
path_data: PathData,
state: &converter::State,
) -> Option<TextFlow> {
// Build the path
let mut builder = tiny_skia_path::PathBuilder::new();
let mut prev_mp = None;
for segment in path_data.iter() {
if let Some(mp) = converter::apply_path_segment(&mut builder, segment, prev_mp) {
prev_mp = Some(mp);
}
}

// Some valid paths can still lead to an invalid `Path`.
let path = match builder.finish() {
Some(path) => path,
None => return Some(TextFlow::Linear), // If path creation fails, fall back to linear
};

// Don't proceed with empty paths
if path.is_empty() {
return Some(TextFlow::Linear);
}


// Don't proceed with empty paths
if path.is_empty() {
return Some(TextFlow::Linear);
}

let path = Arc::new(path);

// Get start offset
let start_offset: Length = node
.attribute(AId::StartOffset)
.unwrap_or_else(|| Length::new(0.0, LengthUnit::Number));

// Convert percentage value into absolute.
let start_offset = if start_offset.unit == LengthUnit::Percent {
// 'If a percentage is given, then the startOffset represents
// a percentage distance along the entire path.'
let path_len = path_length(&path);
path_len as f32 * (start_offset.number as f32 / 100.0)
} else {
node.resolve_length(AId::StartOffset, state, 0.0)
};

// Create and return the text path
Some(TextFlow::Path(Arc::new(TextPath {
id: path_id,
start_offset,
path,
})))
}

fn path_length(path: &tiny_skia_path::Path) -> f64 {
let mut prev_mx = path.points()[0].x;
let mut prev_my = path.points()[0].y;
Expand Down
20 changes: 20 additions & 0 deletions crates/usvg/src/tree/text_path.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// ...existing code...

impl TextPath {
pub fn from_xml_node(node: &roxmltree::Node) -> Option<Self> {
// ...existing code...

// Fix: Support both xlink:href and href
let href = node.attribute((XLINK_NAMESPACE, "href"))
.or_else(|| node.attribute("href"))?;

// ...existing code...
Some(TextPath {
href: href.to_string(),
// ...existing fields...
})
}
}

// ...existing code...

Loading