mirror of
https://github.com/penpot/penpot.git
synced 2025-12-12 06:24:17 +01:00
Merge pull request #7343 from penpot/elenatorro-12118-support-large-svg-files
🐛 Fix parsing large paths with multiple subpaths
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(¤t, 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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user