Merge pull request #7343 from penpot/elenatorro-12118-support-large-svg-files

🐛 Fix parsing large paths with multiple subpaths
This commit is contained in:
Elena Torró
2025-09-26 13:35:17 +02:00
committed by GitHub
9 changed files with 801 additions and 221 deletions

View File

@@ -24,6 +24,7 @@ pub fn is_close_to(current: f32, value: f32) -> bool {
(current - value).abs() <= THRESHOLD
}
#[allow(dead_code)]
pub fn are_close_points(a: impl Into<(f32, f32)>, b: impl Into<(f32, f32)>) -> bool {
let (a_x, a_y) = a.into();
let (b_x, b_y) = b.into();

View File

@@ -1334,6 +1334,9 @@ impl RenderState {
// Nested shapes shadowing - apply black shadow to child shapes too
for shadow_shape_id in element.children.iter() {
let shadow_shape = tree.get(shadow_shape_id).unwrap();
if shadow_shape.hidden {
continue;
}
let clip_bounds = node_render_state.get_nested_shadow_clip_bounds(
element,
modifiers.get(&element.id),

View File

@@ -14,23 +14,7 @@ pub enum Segment {
Close,
}
impl Segment {
fn xy(&self) -> Option<Point> {
match self {
Segment::MoveTo(xy) => Some(*xy),
Segment::LineTo(xy) => Some(*xy),
Segment::CurveTo((_, _, xy)) => Some(*xy),
Segment::Close => None,
}
}
pub fn is_close_to(&self, other: &Segment) -> bool {
match (self.xy(), other.xy()) {
(Some(a), Some(b)) => math::are_close_points(a, b),
_ => false,
}
}
}
impl Segment {}
#[derive(Debug, Clone, PartialEq)]
pub struct Path {
@@ -92,8 +76,7 @@ impl Path {
}
}
// TODO: handle error
let open = subpaths::is_open_path(&segments).expect("Failed to determine if path is open");
let open = subpaths::is_open_path(&segments);
Self {
segments,

View File

@@ -1,11 +1,9 @@
use super::Segment;
use crate::math::are_close_points;
use crate::shapes::paths::Point;
use crate::shapes::paths::Segment;
type Result<T> = std::result::Result<T, String>;
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone)]
pub struct Subpath {
segments: Vec<Segment>,
pub segments: Vec<Segment>,
closed: Option<bool>,
}
@@ -17,230 +15,188 @@ impl Subpath {
}
}
pub fn starts_in(&self, other_segment: Option<&Segment>) -> bool {
if let (Some(start), Some(end)) = (self.start(), other_segment) {
start.is_close_to(end)
} else {
false
}
pub fn start(&self) -> Option<Point> {
self.segments.first().and_then(|s| match s {
Segment::MoveTo(p) | Segment::LineTo(p) => Some(*p),
_ => None,
})
}
pub fn ends_in(&self, other_segment: Option<&Segment>) -> bool {
if let (Some(end), Some(start)) = (self.end(), other_segment) {
end.is_close_to(start)
} else {
false
}
}
pub fn start(&self) -> Option<&Segment> {
self.segments.first()
}
pub fn end(&self) -> Option<&Segment> {
self.segments.last()
}
pub fn is_empty(&self) -> bool {
self.segments.is_empty()
pub fn end(&self) -> Option<Point> {
self.segments.iter().rev().find_map(|s| match s {
Segment::MoveTo(p) | Segment::LineTo(p) => Some(*p),
Segment::CurveTo((_, _, p)) => Some(*p),
_ => None,
})
}
pub fn is_closed(&self) -> bool {
self.closed.unwrap_or_else(|| self.calculate_closed())
}
pub fn add_segment(&mut self, segment: Segment) {
self.segments.push(segment);
self.closed = None;
}
pub fn reversed(&self) -> Self {
let mut reversed = self.clone();
reversed.segments.reverse();
reversed
let mut rev = self.clone();
rev.segments.reverse();
rev.closed = None;
rev
}
fn calculate_closed(&self) -> bool {
if self.segments.is_empty() {
return false;
return true;
}
// Check if the path ends with a Close segment
if let Some(Segment::Close) = self.segments.last() {
return true;
}
// Check if the first and last points are close to each other
if let (Some(first), Some(last)) = (self.segments.first(), self.segments.last()) {
let first_point = match first {
Segment::MoveTo(xy) => xy,
_ => return false,
};
let last_point = match last {
Segment::LineTo(xy) => xy,
Segment::CurveTo((_, _, xy)) => xy,
_ => return false,
};
return are_close_points(*first_point, *last_point);
if let (Some(first), Some(last)) = (self.start(), self.end()) {
return are_close_points(first, last);
}
false
}
}
impl Default for Subpath {
fn default() -> Self {
Self::new(vec![])
fn are_close_points(a: Point, b: Point) -> bool {
let tol = 1e-1;
(a.0 - b.0).abs() < tol && (a.1 - b.1).abs() < tol
}
#[derive(Debug, Clone)]
enum MergeMode {
EndStart,
StartEnd,
EndEnd,
StartStart,
}
impl TryFrom<(&Subpath, &Subpath)> for Subpath {
type Error = &'static str;
fn try_from((a, b): (&Subpath, &Subpath)) -> Result<Self, Self::Error> {
let mut segs = a.segments.clone();
segs.extend_from_slice(&b.segments);
Ok(Subpath::new(segs))
}
}
/// Joins two subpaths into a single subpath
impl TryFrom<(&Subpath, &Subpath)> for Subpath {
type Error = String;
pub fn closed_subpaths(subpaths: Vec<Subpath>) -> Vec<Subpath> {
let n = subpaths.len();
if n == 0 {
return vec![];
}
fn try_from((subpath, other): (&Subpath, &Subpath)) -> Result<Self> {
if subpath.is_empty() || other.is_empty() || subpath.end() != other.start() {
return Err("Subpaths cannot be joined".to_string());
let mut used = vec![false; n];
let mut result = Vec::with_capacity(n);
for i in 0..n {
if used[i] {
continue;
}
let mut segments = subpath.segments.clone();
segments.extend_from_slice(&other.segments);
Ok(Subpath::new(segments))
let mut current = subpaths[i].clone();
used[i] = true;
let mut merged_any = false;
loop {
if current.is_closed() {
break;
}
let mut did_merge = false;
for j in 0..n {
if used[j] || subpaths[j].is_closed() {
continue;
}
let candidate = &subpaths[j];
let maybe_merge = [
(current.end(), candidate.start(), MergeMode::EndStart),
(current.start(), candidate.end(), MergeMode::StartEnd),
(current.end(), candidate.end(), MergeMode::EndEnd),
(current.start(), candidate.start(), MergeMode::StartStart),
]
.iter()
.find_map(|(p1, p2, mode)| {
if let (Some(a), Some(b)) = (p1, p2) {
if are_close_points(*a, *b) {
Some(mode.clone())
} else {
None
}
} else {
None
}
});
if let Some(mode) = maybe_merge {
if let Some(new_current) = try_merge(&current, candidate, mode) {
used[j] = true;
current = new_current;
merged_any = true;
did_merge = true;
break;
}
}
}
if !did_merge {
break;
}
}
if !current.is_closed() && merged_any {
if let Some(start) = current.start() {
let mut segs = current.segments.clone();
segs.push(Segment::LineTo(start));
segs.push(Segment::Close);
current = Subpath::new(segs);
}
}
result.push(current);
}
result
}
fn try_merge(current: &Subpath, candidate: &Subpath, mode: MergeMode) -> Option<Subpath> {
match mode {
MergeMode::EndStart => Subpath::try_from((current, candidate)).ok(),
MergeMode::StartEnd => Subpath::try_from((candidate, current)).ok(),
MergeMode::EndEnd => Subpath::try_from((current, &candidate.reversed())).ok(),
MergeMode::StartStart => Subpath::try_from((&candidate.reversed(), current)).ok(),
}
}
/// Groups segments into subpaths based on MoveTo segments
fn get_subpaths(segments: &[Segment]) -> Vec<Subpath> {
let mut subpaths: Vec<Subpath> = vec![];
let mut current_subpath = Subpath::default();
pub fn split_into_subpaths(segments: &[Segment]) -> Vec<Subpath> {
let mut subpaths = Vec::new();
let mut current_segments = Vec::new();
for segment in segments {
match segment {
Segment::MoveTo(_) => {
if !current_subpath.is_empty() {
subpaths.push(current_subpath);
// Start new subpath unless current is empty
if !current_segments.is_empty() {
subpaths.push(Subpath::new(current_segments.clone()));
current_segments.clear();
}
current_subpath = Subpath::default();
// Add the MoveTo segment to the new subpath
current_subpath.add_segment(*segment);
}
_ => {
current_subpath.add_segment(*segment);
current_segments.push(*segment);
}
_ => current_segments.push(*segment),
}
}
if !current_subpath.is_empty() {
subpaths.push(current_subpath);
// Push last subpath if any
if !current_segments.is_empty() {
subpaths.push(Subpath::new(current_segments));
}
subpaths
}
/// Computes the merged candidate and the remaining, unmerged subpaths
fn merge_paths(candidate: Subpath, others: Vec<Subpath>) -> Result<(Subpath, Vec<Subpath>)> {
if candidate.is_closed() {
return Ok((candidate, others));
}
let mut merged = candidate.clone();
let mut other_without_merged = vec![];
let mut merged_any = false;
for subpath in others {
// Only merge if the candidate is not already closed and the subpath can be meaningfully connected
if !merged.is_closed() && !subpath.is_closed() {
if merged.ends_in(subpath.start()) {
if let Ok(new_merged) = Subpath::try_from((&merged, &subpath)) {
merged = new_merged;
merged_any = true;
} else {
other_without_merged.push(subpath);
}
} else if merged.starts_in(subpath.end()) {
if let Ok(new_merged) = Subpath::try_from((&subpath, &merged)) {
merged = new_merged;
merged_any = true;
} else {
other_without_merged.push(subpath);
}
} else if merged.ends_in(subpath.end()) {
if let Ok(new_merged) = Subpath::try_from((&merged, &subpath.reversed())) {
merged = new_merged;
merged_any = true;
} else {
other_without_merged.push(subpath);
}
} else if merged.starts_in(subpath.start()) {
if let Ok(new_merged) = Subpath::try_from((&subpath.reversed(), &merged)) {
merged = new_merged;
merged_any = true;
} else {
other_without_merged.push(subpath);
}
} else {
other_without_merged.push(subpath);
}
} else {
// If either subpath is closed, don't merge
other_without_merged.push(subpath);
}
}
// If we tried to merge but failed to close, force close the merged subpath
if !merged.is_closed() && merged_any {
let mut closed_segments = merged.segments.clone();
if let Some(Segment::MoveTo(start)) = closed_segments.first() {
closed_segments.push(Segment::LineTo(*start));
closed_segments.push(Segment::Close);
}
merged = Subpath::new(closed_segments);
}
Ok((merged, other_without_merged))
}
/// Searches a path for potential subpaths that can be closed and merges them
fn closed_subpaths(
current: &Subpath,
others: &[Subpath],
partial: &[Subpath],
) -> Result<Vec<Subpath>> {
let mut result = partial.to_vec();
let (new_current, new_others) = if current.is_closed() {
(current.clone(), others.to_vec())
} else {
merge_paths(current.clone(), others.to_vec())?
};
// we haven't found any matching subpaths -> advance
if new_current == *current {
result.push(current.clone());
if new_others.is_empty() {
return Ok(result);
}
closed_subpaths(&new_others[0], &new_others[1..], &result)
}
// if diffrent, we have to search again with the merged subpaths
else {
closed_subpaths(&new_current, &new_others, &result)
}
}
pub fn is_open_path(segments: &[Segment]) -> Result<bool> {
let subpaths = get_subpaths(segments);
let closed_subpaths = if subpaths.len() > 1 {
closed_subpaths(&subpaths[0], &subpaths[1..], &[])?
} else {
subpaths
};
// return true if any subpath is open
Ok(closed_subpaths.iter().any(|subpath| !subpath.is_closed()))
pub fn is_open_path(segments: &[Segment]) -> bool {
let subpaths = split_into_subpaths(segments);
let closed_subpaths = closed_subpaths(subpaths);
closed_subpaths.iter().any(|sp| !sp.is_closed())
}
#[cfg(test)]
@@ -266,8 +222,7 @@ mod tests {
Segment::Close,
];
let result =
subpaths::is_open_path(&segments).expect("Failed to determine if path is open");
let result = subpaths::is_open_path(&segments);
assert!(result, "Path should be open");
}
@@ -280,8 +235,7 @@ mod tests {
Segment::LineTo((223.0, 582.0)),
];
let result =
subpaths::is_open_path(&segments).expect("Failed to determine if path is open");
let result = subpaths::is_open_path(&segments);
assert!(!result, "Path should be closed");
}
@@ -331,16 +285,14 @@ mod tests {
Segment::LineTo((400.1158, 610.0)),
];
let result =
subpaths::is_open_path(&segments).expect("Failed to determine if path is open");
let result = subpaths::is_open_path(&segments);
assert!(result, "Path should be open");
}
#[test]
fn test_is_open_path_4() {
let segments = vec![];
let result =
subpaths::is_open_path(&segments).expect("Failed to determine if path is open");
let result = subpaths::is_open_path(&segments);
assert!(!result, "Path should be closed");
}
}

View File

@@ -2,6 +2,7 @@
use macros::ToJs;
use mem::SerializableResult;
use std::mem::size_of;
use std::sync::{Mutex, OnceLock};
use crate::shapes::{Path, Segment, ToPath};
use crate::{mem, with_current_shape, with_current_shape_mut, STATE};
@@ -151,17 +152,59 @@ impl From<Vec<RawSegmentData>> for Path {
}
}
static PATH_UPLOAD_BUFFER: OnceLock<Mutex<Vec<u8>>> = OnceLock::new();
fn get_path_upload_buffer() -> &'static Mutex<Vec<u8>> {
PATH_UPLOAD_BUFFER.get_or_init(|| Mutex::new(Vec::new()))
}
#[no_mangle]
pub extern "C" fn start_shape_path_buffer() {
let buffer = get_path_upload_buffer();
let mut buffer = buffer.lock().unwrap();
buffer.clear();
}
#[no_mangle]
pub extern "C" fn set_shape_path_chunk_buffer() {
let bytes = mem::bytes();
let buffer = get_path_upload_buffer();
let mut buffer = buffer.lock().unwrap();
buffer.extend_from_slice(&bytes);
mem::free_bytes();
}
#[no_mangle]
pub extern "C" fn set_shape_path_buffer() {
with_current_shape_mut!(state, |shape: &mut Shape| {
let buffer = get_path_upload_buffer();
let mut buffer = buffer.lock().unwrap();
let chunk_size = size_of::<RawSegmentData>();
if buffer.len() % chunk_size != 0 {
// FIXME
println!("Warning: buffer length is not a multiple of chunk size!");
}
let mut segments = Vec::new();
for (i, chunk) in buffer.chunks(chunk_size).enumerate() {
match RawSegmentData::try_from(chunk) {
Ok(seg) => segments.push(Segment::from(seg)),
Err(e) => println!("Error at segment {}: {}", i, e),
}
}
shape.set_path_segments(segments);
buffer.clear();
});
}
#[no_mangle]
pub extern "C" fn set_shape_path_content() {
with_current_shape_mut!(state, |shape: &mut Shape| {
let bytes = mem::bytes();
let segments = bytes
.chunks(size_of::<RawSegmentData>())
.map(|chunk| RawSegmentData::try_from(chunk).expect("Invalid path data"))
.map(Segment::from)
.collect();
shape.set_path_segments(segments);
});
}