mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 13:17:19 -06:00
.ai import: try a placed file at its box's corner when the layers differ
A placed (linked) file is put where its matrix takes the canvas point of art space's origin (the centre of %AI3_TemplateBox). In many files the matrix takes the corner of the box the file is written with instead, so the image landed far off its artboard, the layers differed from the page, and the file came in as its PDF part. The reader keeps that second placement for each placed file where the two differ. When the layers don't draw as the page, they are compared again with those files at their box's corner, and used if that draws as the page. Files whose layers already match keep their placement.
This commit is contained in:
@@ -106,6 +106,10 @@ pub struct Structure {
|
||||
/// Where the drawn looks lay that objects read with their art style took the place of (the
|
||||
/// document's coordinates): the page draws Illustrator's rendering of those styles there.
|
||||
pub restyled: Vec<Rect>,
|
||||
/// Placed files whose pixels the file puts at another point too: their image's id and the
|
||||
/// move, in its pixels, that places them there (the corner of the box they are written with,
|
||||
/// in place of the canvas point of art space's origin).
|
||||
pub placed_elsewhere: Vec<(NodeId, Affine)>,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -197,6 +201,8 @@ struct Reader<'a> {
|
||||
palette: super::palette::Palette,
|
||||
/// See [`Structure::restyled`].
|
||||
restyled: Vec<Rect>,
|
||||
/// See [`Structure::placed_elsewhere`].
|
||||
placed_elsewhere: Vec<(NodeId, Affine)>,
|
||||
/// CMYK samples converted, by value.
|
||||
cmyk_cache: HashMap<[u8; 4], [u8; 3]>,
|
||||
/// A `/ForeignObject`'s `/Data`: its PDF as ASCII85 in comment lines, until the object closes.
|
||||
@@ -286,6 +292,7 @@ impl<'a> Reader<'a> {
|
||||
styles: HashMap::new(),
|
||||
palette: super::palette::Palette::default(),
|
||||
restyled: vec![],
|
||||
placed_elsewhere: vec![],
|
||||
mesh: None,
|
||||
cmyk_cache: HashMap::new(),
|
||||
done: false,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use super::containers::close;
|
||||
use super::document::TEXT_SLOT;
|
||||
use super::types::{FLIP, IMAGE_TOO_LARGE, LINKED_PREVIEWS, MAX_PIXELS, MAX_POINTS, MAX_SIDE, NON_NATIVE_ART, NON_NATIVE_ART_UNREAD, Place};
|
||||
use super::types::{
|
||||
FLIP, IMAGE_TOO_LARGE, LINKED_PREVIEWS, MAX_PIXELS, MAX_PLACED_ELSEWHERE, MAX_POINTS, MAX_SIDE, NON_NATIVE_ART, NON_NATIVE_ART_UNREAD, Place,
|
||||
};
|
||||
use super::{
|
||||
Affine, Appearance, AppearanceItem, Arc, BezPath, CANVAS_CENTRE, FillLayer, ImageBlob, ImageObject, Inks, Instance, LinkInfo, Node, NodeKind,
|
||||
Obj, OpacityMask, Paint, PathData, Point, Reader, Rect, StrokeLayer, V, ps,
|
||||
Affine, Appearance, AppearanceItem, Arc, BezPath, CANVAS_CENTRE, FillLayer, ImageBlob, ImageObject, Inks, Instance, LinkInfo, Node, NodeId,
|
||||
NodeKind, Obj, OpacityMask, Paint, PathData, Point, Reader, Rect, StrokeLayer, V, ps,
|
||||
};
|
||||
|
||||
impl<'a> Reader<'a> {
|
||||
@@ -464,7 +466,15 @@ impl<'a> Reader<'a> {
|
||||
// corner when the file doesn't say where the canvas is); the preview's are spread over them.
|
||||
let centre = self.template.map(|t| ((t[0] + t[2]) / 2.0, (t[1] + t[3]) / 2.0));
|
||||
let origin = centre.map_or(b.origin(), |(x, y)| Point::new(CANVAS_CENTRE - x, CANVAS_CENTRE + y));
|
||||
let over_file = Affine::translate(origin.to_vec2()) * Affine::scale_non_uniform(b.width() / f64::from(w), b.height() / f64::from(h));
|
||||
let (sx, sy) = (b.width() / f64::from(w), b.height() / f64::from(h));
|
||||
let over_file = Affine::translate(origin.to_vec2()) * Affine::scale_non_uniform(sx, sy);
|
||||
// Many files put the pixels at the box's own corner instead: that placement is kept for
|
||||
// when the layers don't draw as the page does with this one (a move in the preview's
|
||||
// pixels, which later moves of the image keep).
|
||||
let to_corner = b.origin() - origin;
|
||||
let elsewhere = (to_corner.hypot() > 0.01 && sx.abs() > 1e-12 && sy.abs() > 1e-12)
|
||||
.then(|| Affine::translate((to_corner.x / sx, to_corner.y / sy)))
|
||||
.filter(|a| a.as_coeffs().iter().all(|v| v.is_finite()));
|
||||
let xf = self.space() * m * over_file;
|
||||
if !xf.as_coeffs().iter().all(|v| v.is_finite()) || xf.determinant().abs() < 1e-12 {
|
||||
self.unreadable("placed files");
|
||||
@@ -476,6 +486,11 @@ impl<'a> Reader<'a> {
|
||||
let key = blob.content_key();
|
||||
self.doc.images.entry(key.clone()).or_insert(blob);
|
||||
self.warn(LINKED_PREVIEWS);
|
||||
if let Some(shift) = elsewhere
|
||||
&& self.placed_elsewhere.len() < MAX_PLACED_ELSEWHERE
|
||||
{
|
||||
self.placed_elsewhere.push((NodeId(self.doc.peek_next_id()), shift));
|
||||
}
|
||||
self.add_image(
|
||||
ImageObject { key, width: w, height: h, xf, link: Some(LinkInfo::new(path)), placement: Default::default(), pixels: None },
|
||||
hidden,
|
||||
|
||||
@@ -454,6 +454,7 @@ impl<'a> Reader<'a> {
|
||||
hidden_unread: self.hidden_unread,
|
||||
slot_names: self.slot_names,
|
||||
restyled: self.restyled,
|
||||
placed_elsewhere: self.placed_elsewhere,
|
||||
warnings,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ pub(super) const MAX_POINTS: usize = 4_000_000;
|
||||
|
||||
/// Largest image read (pixels).
|
||||
pub(super) const MAX_PIXELS: u64 = 1 << 25;
|
||||
/// Most placed files whose other placement ([`super::Structure::placed_elsewhere`]) is kept.
|
||||
pub(super) const MAX_PLACED_ELSEWHERE: usize = 4096;
|
||||
|
||||
/// The note for an image past [`MAX_PIXELS`]: the file then opens from its PDF part, which reads
|
||||
/// larger images (#1479).
|
||||
|
||||
@@ -57,7 +57,7 @@ pub(super) fn build_from(
|
||||
trust: bool,
|
||||
unit: f64,
|
||||
) -> Result<(Document, Vec<String>), String> {
|
||||
let Structure { mut doc, to_doc, bbox, artboard, template, hidden_unread, slot_names, restyled, warnings } = structure;
|
||||
let Structure { mut doc, to_doc, bbox, artboard, template, hidden_unread, slot_names, restyled, placed_elsewhere, warnings } = structure;
|
||||
// Editing data in the units of the page's user space ([`in_units`]) is compared with the page
|
||||
// scaled down to those units, and the document read from it is scaled up by `unit`.
|
||||
let board = artboard.map(|a| to_doc.transform_rect_bbox(a));
|
||||
@@ -225,8 +225,15 @@ pub(super) fn build_from(
|
||||
}
|
||||
match compare(&frame, &mut view, &doc) {
|
||||
Ok(note) => notes.extend(note),
|
||||
Err(why) if trust => notes.push(format!("{why}: they come from its editing data all the same")),
|
||||
Err(why) => return Err(why),
|
||||
// Placed files at the other point their data gives, where that draws as the page.
|
||||
Err(why) => match elsewhere(&doc, &placed_elsewhere).map(|other| (compare(&frame, &mut view, &other), other)) {
|
||||
Some((Ok(note), other)) => {
|
||||
doc = other;
|
||||
notes.extend(note);
|
||||
}
|
||||
_ if trust => notes.push(format!("{why}: they come from its editing data all the same")),
|
||||
_ => return Err(why),
|
||||
},
|
||||
}
|
||||
}
|
||||
None => unmark(&mut doc.layers, true, true, &mut vec![]),
|
||||
@@ -238,6 +245,22 @@ pub(super) fn build_from(
|
||||
Ok((doc, notes))
|
||||
}
|
||||
|
||||
/// `doc` with the placed files `placed` lists at their other placement (see
|
||||
/// [`ai::Structure::placed_elsewhere`]): each image's transform followed by its move. `None` when
|
||||
/// none is listed.
|
||||
fn elsewhere(doc: &Document, placed: &[(NodeId, Affine)]) -> Option<Document> {
|
||||
if placed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut doc = doc.clone();
|
||||
for (id, shift) in placed {
|
||||
if let Some(NodeKind::Image(im)) = doc.node_mut(*id).map(|n| &mut n.kind) {
|
||||
im.xf *= *shift;
|
||||
}
|
||||
}
|
||||
Some(doc)
|
||||
}
|
||||
|
||||
/// Does the editing data's first artboard `board` (in the document read from it: art space through
|
||||
/// `to_doc`) have the size of `page`'s first artboard in units of `unit` points, `unit` being above
|
||||
/// 1, and not its size in points? `page` is a PDF part read in points from pages of `unit` points a
|
||||
|
||||
@@ -1570,3 +1570,37 @@ fn point_type_without_a_matrix_is_placed_by_its_layout() {
|
||||
};
|
||||
assert_eq!(place(&without), place(&with));
|
||||
}
|
||||
|
||||
/// A placed file is put where its matrix takes the canvas point of art space's origin (the
|
||||
/// centre of `%AI3_TemplateBox` on the canvas); many files put it at the corner of the box it is
|
||||
/// written with instead. When the layers don't draw as the page with the first, the second is
|
||||
/// tried.
|
||||
#[test]
|
||||
fn a_placed_file_at_its_boxs_corner_matches_the_page() {
|
||||
// A 2 × 2 preview of half-transparent black (premultiplied ARGB), of a file whose box is
|
||||
// 8000 8000 8040 8040 on the canvas: 40 × 40 units, which the matrix takes to (30, 70)–(70,
|
||||
// 30) in art space, a square at (30, 30)–(70, 70) on the 100 × 100 page. The template box's
|
||||
// canvas point (8091.5, 8241.5) would put it off the page.
|
||||
let pixel = "\x7f\0\0\0";
|
||||
let place = format!(
|
||||
"%AI5_BeginPlace\n%%FileType: 1\n[1 0 0 -1 -7970 8070] 8000 8000 8040 8040 4 4 0\n(photo.png)`\n\
|
||||
%AI26_BeginPlacedObjectPreview\n2 2 8 32 17 {}\n%AI26_EndPlacedObjectPreview\n~\n%AI5_EndPlace\n",
|
||||
pixel.repeat(4)
|
||||
);
|
||||
let data = native(&layer("Jig", true, &place)).replace("%AI5_NumLayers", "%AI3_TemplateBox: 100 50 100 50\n%AI5_NumLayers");
|
||||
// The page: the square in 50% grey, as the PDF part draws the image.
|
||||
let mut page = Document::new(100.0, 100.0);
|
||||
let id = page.alloc_id();
|
||||
let path = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(30.0, 30.0, 70.0, 70.0));
|
||||
let grey = vectorcraft_doc::FillLayer::new(vectorcraft_color::Paint::solid(vectorcraft_color::Color::gray(0.5)));
|
||||
let n = Node::path(id, path, vectorcraft_doc::Appearance { items: vec![vectorcraft_doc::AppearanceItem::Fill(grey)], ..Default::default() });
|
||||
let l = page.layers[0].id;
|
||||
page.insert(Some(l), 0, n).unwrap();
|
||||
let (d, w, read) = crate::layered_ai(&private(&data), page, vec![], false, false, 1.0);
|
||||
assert!(read, "{w:?}");
|
||||
assert_eq!(names(&d), ["Jig"]);
|
||||
let image = &d.layers[0].children().unwrap()[0];
|
||||
assert!(matches!(image.kind, NodeKind::Image(_)), "{:?}", image.kind);
|
||||
let b = image.geometric_bounds().unwrap();
|
||||
assert!([b.x0 - 30.0, b.y0 - 30.0, b.x1 - 70.0, b.y1 - 70.0].iter().all(|v| v.abs() < 0.01), "on the page's square: {b:?}");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (SVG export: simple shapes as SVG basic shapes) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (`.ai` import: placed files at their box's corner where that draws as the page) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
Every format Illustrator opens, places, saves or exports, against VectorCraft's format table (`FORMATS`,
|
||||
`OPEN_EXTS`, `PLACE_EXTS` in `crates/engine/src/cmd/fileio/formats.rs`). Illustrator's list comes from its public
|
||||
@@ -132,6 +132,7 @@ the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPri
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | `.ai` import: a placed (linked) file is put where its matrix takes the canvas point of art space's origin, and, when the layers then don't draw as the page, at the corner of the box it is written with, which many files use; six files of a user's set whose photos landed off their artboards now open from their layers. Own-fixture test in `import::tests_native`; scores unchanged |
|
||||
| 2026-10-30 | minor | SVG export: axis-aligned rectangles are written as `<rect>`, circles and ellipses drawn with four standard arcs as `<circle>`/`<ellipse>`, straight lines as `<line>` and other straight-segment paths as `<polygon>`/`<polyline>`, at the written precision; a rectangle or ellipse with a dashed stroke, turned ellipses, curves and compound paths stay `<path>` (#4652); scores unchanged |
|
||||
| 2026-10-30 | minor | `.ai` import: a placed (linked) file written as an object's copy on `%_` lines is read: its `%AI5_BeginPlace` and `%AI5_EndPlace` markers there were dropped, so the `~` that ends it was an unknown operator and the whole file came in as its PDF part; its preview's header, on a `%_` line too, is read. Own-fixture test in `import::ai::tests`; scores unchanged |
|
||||
| 2026-10-30 | minor | PDF export: an opaque gradient mesh is written as one PDF Type 6 (Coons patch) shading (ISO 32000-1, 8.7.4.5.7) filling its outline, so viewers blend its colours smoothly instead of showing the bands and seams of the 8 × 8 flat quads a patch it was cut into; a file of 65,558 small meshes exports in 14 s instead of 56 s, at half the size. A mesh with see-through points is still written as quads. Own-fixture tests in `tests::mesh_export`; scores unchanged |
|
||||
|
||||
Reference in New Issue
Block a user