mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-13 22:12:12 +00:00
Fix shared file import edge cases
This commit is contained in:
5 files changed
+67
-8
No files matched your search
@@ -47,12 +47,16 @@ pub fn import(bytes: &[u8], limits: Limits) -> Result<Imported, Error> {
|
||||
}
|
||||
let source = vectorcraft_eps::import(bytes).map_err(Error::Parse)?;
|
||||
let page_bounds = vectorcraft_eps::page(bytes).map_err(Error::Parse)?;
|
||||
// Illustrator's editable copy is expressed in its art-box document coordinates. The page
|
||||
// reader, in contrast, returns a page at the origin. Normalize extracted paths to match it.
|
||||
let origin = source.document.artboards.first().map(|board| board.rect.origin()).unwrap_or_default();
|
||||
let to_page = kurbo::Affine::translate((-origin.x, -origin.y));
|
||||
let mut paths = Vec::new();
|
||||
let mut total_elements = 0usize;
|
||||
let mut clipped = false;
|
||||
let mut omitted = false;
|
||||
for layer in &source.document.layers {
|
||||
collect(layer, &mut paths, &mut total_elements, limits, &mut clipped, &mut omitted)?;
|
||||
collect(layer, &mut paths, &mut total_elements, limits, to_page, &mut clipped, &mut omitted)?;
|
||||
}
|
||||
let mut warnings = source.warnings;
|
||||
if clipped {
|
||||
@@ -69,6 +73,7 @@ fn collect(
|
||||
paths: &mut Vec<BezPath>,
|
||||
elements: &mut usize,
|
||||
limits: Limits,
|
||||
to_page: kurbo::Affine,
|
||||
clipped: &mut bool,
|
||||
omitted: &mut bool,
|
||||
) -> Result<(), Error> {
|
||||
@@ -85,7 +90,7 @@ fn collect(
|
||||
if paths.len() >= limits.max_paths || *elements > limits.max_path_elements {
|
||||
return Err(Error::OutputLimit);
|
||||
}
|
||||
paths.push(path);
|
||||
paths.push(to_page * path);
|
||||
}
|
||||
} else if let Some(children) = node.children() {
|
||||
let is_clip = matches!(node.kind, vectorcraft_doc::NodeKind::Group { clip: true, .. } | vectorcraft_doc::NodeKind::Layer { clip: true, .. });
|
||||
@@ -94,7 +99,7 @@ fn collect(
|
||||
if is_clip && index == 0 {
|
||||
continue;
|
||||
}
|
||||
collect(child, paths, elements, limits, clipped, omitted)?;
|
||||
collect(child, paths, elements, limits, to_page, clipped, omitted)?;
|
||||
}
|
||||
} else {
|
||||
*omitted = true;
|
||||
|
||||
@@ -195,6 +195,7 @@ pub fn format_for(name: &str, bytes: &[u8]) -> Option<ImportFormat> {
|
||||
F::Svgz => Some(ImportFormat::Svgz),
|
||||
F::Dxf => Some(ImportFormat::Dxf),
|
||||
F::Pdf => Some(ImportFormat::Pdf),
|
||||
F::Illustrator if bytes.starts_with(b"%PDF-") => Some(ImportFormat::Pdf),
|
||||
F::Eps | F::PostScript | F::Illustrator => Some(ImportFormat::Eps),
|
||||
F::Png | F::Jpeg | F::Bmp | F::Gif | F::Tiff | F::WebP => Some(ImportFormat::Raster),
|
||||
_ => None,
|
||||
@@ -391,3 +392,14 @@ fn path_is_finite(path: &BezPath) -> bool {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod format_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pdf_based_illustrator_is_routed_to_pdf_reader() {
|
||||
let bytes = b"%PDF-1.7\n/AIPrivateData";
|
||||
assert_eq!(format_for("illustration.ai", bytes), Some(ImportFormat::Pdf));
|
||||
}
|
||||
}
|
||||
@@ -166,10 +166,13 @@ fn write_svg(scene: &OutputScene) -> Result<String, Error> {
|
||||
|
||||
fn write_pdf(scene: &OutputScene) -> Result<Vec<u8>, Error> {
|
||||
validate_scene(scene)?;
|
||||
// Match SVG's painter order: layers in order, then paths in source order within each layer.
|
||||
let paths = scene
|
||||
.paths
|
||||
.layers
|
||||
.iter()
|
||||
.filter(|path| scene.layers.get(path.layer).is_some_and(|layer| layer.visible))
|
||||
.enumerate()
|
||||
.filter(|(_, layer)| layer.visible)
|
||||
.flat_map(|(layer_index, _)| scene.paths.iter().filter(move |path| path.layer == layer_index))
|
||||
.map(|path| PdfPath { path: path.path.clone(), color_rgb: path.color_rgb, stroke_width_mm: Some(path.stroke_width_mm) })
|
||||
.collect();
|
||||
write_pdf_pages(&[PdfPage { width_mm: scene.width_mm, height_mm: scene.height_mm, view_box: scene.view_box, paths }])
|
||||
|
||||
+14
-2
@@ -145,6 +145,7 @@ fn import_selected(bytes: &[u8], limits: Limits, selected: Option<usize>) -> Res
|
||||
#[derive(Default)]
|
||||
struct Collector {
|
||||
paths: Vec<BezPath>,
|
||||
last_fill_path: Option<BezPath>,
|
||||
omitted_text: usize,
|
||||
omitted_images: usize,
|
||||
clips_seen: usize,
|
||||
@@ -157,26 +158,37 @@ struct Collector {
|
||||
impl Device<'_> for Collector {
|
||||
fn set_soft_mask(&mut self, _: Option<SoftMask<'_>>) {}
|
||||
fn set_blend_mode(&mut self, _: BlendMode) {}
|
||||
fn draw_path(&mut self, path: &BezPath, transform: Affine, _: &Paint<'_>, _: &PathDrawMode) {
|
||||
fn draw_path(&mut self, path: &BezPath, transform: Affine, _: &Paint<'_>, mode: &PathDrawMode) {
|
||||
if path.is_empty() {
|
||||
return;
|
||||
}
|
||||
let path = transform * path.clone();
|
||||
match mode {
|
||||
PathDrawMode::Fill(_) => self.last_fill_path = Some(path.clone()),
|
||||
PathDrawMode::Stroke(_) => {
|
||||
if self.last_fill_path.take().as_ref() == Some(&path) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
let count = path.elements().len();
|
||||
if self.paths.len() >= self.max_paths || self.elements.saturating_add(count) > self.max_elements {
|
||||
self.exceeded = true;
|
||||
return;
|
||||
}
|
||||
self.elements = self.elements.saturating_add(count);
|
||||
self.paths.push(transform * path.clone());
|
||||
self.paths.push(path);
|
||||
}
|
||||
fn push_clip_path(&mut self, _: &ClipPath) {
|
||||
self.clips_seen = self.clips_seen.saturating_add(1);
|
||||
}
|
||||
fn push_transparency_group(&mut self, _: f32, _: Option<SoftMask<'_>>, _: BlendMode) {}
|
||||
fn draw_glyph(&mut self, _: &Glyph<'_>, _: Affine, _: Affine, _: &Paint<'_>, _: &GlyphDrawMode) {
|
||||
self.last_fill_path = None;
|
||||
self.omitted_text = self.omitted_text.saturating_add(1);
|
||||
}
|
||||
fn draw_image(&mut self, _: Image<'_, '_>, _: Affine) {
|
||||
self.last_fill_path = None;
|
||||
self.omitted_images = self.omitted_images.saturating_add(1);
|
||||
}
|
||||
fn pop_clip_path(&mut self) {}
|
||||
|
||||
+28
-1
@@ -178,7 +178,9 @@ fn clip_path_to_region(
|
||||
.map_err(|e| e.to_string())?;
|
||||
out.extend(kept.to_bezpath());
|
||||
}
|
||||
if has_stroke {
|
||||
// Some callers deliberately retain geometry even when the source has no paint. Keep that
|
||||
// geometry clip-able as linework instead of silently deleting it.
|
||||
if has_stroke || (!has_fill && !has_stroke) {
|
||||
let polygons: Vec<Vec<Point>> =
|
||||
flatten_contours(region)?.into_iter().filter(|(points, closed)| *closed && points.len() >= 3).map(|(points, _)| points).collect();
|
||||
for (mut points, closed) in flatten_contours(path)? {
|
||||
@@ -330,6 +332,25 @@ fn inside_even_odd(polygons: &[Vec<Point>], point: Point, work_left: &mut usize)
|
||||
let (Some(a), Some(b)) = (polygon.get(index), polygon.get((index + 1) % polygon.len())) else {
|
||||
continue;
|
||||
};
|
||||
let edge = *b - *a;
|
||||
let relative = point - *a;
|
||||
let length_squared = edge.dot(edge);
|
||||
let edge_length = length_squared.sqrt();
|
||||
if edge_length <= f64::EPSILON {
|
||||
if relative.x.hypot(relative.y) <= 1e-9 {
|
||||
return Ok(true);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let tolerance = 1e-9 * edge_length.max(1.0);
|
||||
let projection = relative.dot(edge);
|
||||
let projection_tolerance = tolerance * edge_length;
|
||||
if edge.cross(relative).abs() <= projection_tolerance
|
||||
&& projection >= -projection_tolerance
|
||||
&& projection <= length_squared + projection_tolerance
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
if (a.y > point.y) != (b.y > point.y) && point.x < a.x + (point.y - a.y) / (b.y - a.y) * (b.x - a.x) {
|
||||
odd = !odd;
|
||||
}
|
||||
@@ -2238,6 +2259,8 @@ mod tests {
|
||||
<rect id="evenodd-ring" width="20" height="20" clip-path="url(#ring)"/>
|
||||
<g clip-path="url(#left)"><line id="nested-open" x1="0" y1="5" x2="20" y2="5" stroke="black" clip-path="url(#right)"/></g>
|
||||
<line id="open" x1="0" y1="5" x2="20" y2="5" stroke="black" clip-path="url(#left)"/>
|
||||
<line id="boundary" x1="15" y1="2" x2="15" y2="8" stroke="black" clip-path="url(#pieces)"/>
|
||||
<path id="unpainted" d="M0 5H20" data-kind="engrave" fill="none" stroke="none" clip-path="url(#left)"/>
|
||||
<rect id="empty-result" width="20" height="10" clip-path="url(#empty)"/>
|
||||
</svg>"##,
|
||||
);
|
||||
@@ -2258,6 +2281,10 @@ mod tests {
|
||||
assert!(near(nested.bounding_box().x0, 10.0) && near(nested.bounding_box().x1, 15.0));
|
||||
let open = clipped(r#"<line id="open">"#);
|
||||
assert!(near(open.bounding_box().x0, 0.0) && near(open.bounding_box().x1, 15.0));
|
||||
let boundary = clipped(r#"<line id="boundary">"#);
|
||||
assert!(near(boundary.bounding_box().x0, 15.0) && near(boundary.bounding_box().x1, 15.0));
|
||||
let unpainted = clipped(r#"<path id="unpainted">"#);
|
||||
assert!(near(unpainted.bounding_box().x0, 0.0) && near(unpainted.bounding_box().x1, 15.0));
|
||||
assert!(clipped(r#"<rect id="empty-result">"#).elements().is_empty());
|
||||
|
||||
let mut invalid = got.clone();
|
||||
|
||||
Reference in new issue
Block a user