Merge pull request #4660 from mockydevs/fix/ai-tilde-operator

.ai import: a placed file on %_ lines no longer drops the layers
This commit is contained in:
BFlat
2026-10-30 09:18:04 +07:00
committed by GitHub
4 changed files with 47 additions and 2 deletions
+6
View File
@@ -40,6 +40,8 @@ pub(super) struct Token<'a> {
/// A placed file's preview (binary): (begin, end) markers.
const PREVIEW: (&[u8], &[u8]) = (b"%AI26_BeginPlacedObjectPreview", b"%AI26_EndPlacedObjectPreview");
/// A placed file's markers, which a hidden one has on `%_` lines.
const PLACE: [&[u8]; 2] = [b"%AI5_BeginPlace", b"%AI5_EndPlace"];
/// A binary section skipped whole: (begin, end) markers.
const DOCUMENT: (&[u8], &[u8]) = (b"%%BeginDocument", b"%%EndDocument");
@@ -142,6 +144,10 @@ impl<'a> Lexer<'a> {
if start {
return Some(Token { tok: Tok::Comment(line.get(1..).unwrap_or_default()), hidden: false });
}
// So does a hidden placed file: its `~` ends what `%_%AI5_BeginPlace` begins.
if prefixed && PLACE.iter().any(|m| line.starts_with(m)) {
return Some(Token { tok: Tok::Comment(line.get(1..).unwrap_or_default()), hidden: true });
}
// A comment later in a line.
continue;
}
+3 -1
View File
@@ -504,7 +504,9 @@ pub(super) fn elements<'o>(o: &'o Obj, name: &'o str) -> impl Iterator<Item = &'
/// a space), then `h` rows of `rowBytes` bytes, premultiplied ARGB. `None` when it isn't that, is
/// larger than [`MAX_PIXELS`] or is cut short.
pub(super) fn preview_png(data: &[u8]) -> Option<(Vec<u8>, u32, u32)> {
let mut rest = data.trim_ascii_start();
// A placed file's copy on `%_` lines has its header on one too: `%_512 256 2048 32 17 `.
let data = data.trim_ascii_start();
let mut rest = data.strip_prefix(b"%_").unwrap_or(data);
let mut head = [0usize; 5];
for v in &mut head {
let n = rest.iter().position(|b| *b == b' ')?;
+36
View File
@@ -833,6 +833,42 @@ fn placed(visible: bool, matrix: &str, path: &str, preview: Option<&[u8]>, after
s
}
/// [`placed`]'s file with its placed file written as the copy of an object on `%_` lines (the
/// editable object whose drawn look comes before it): the lines from `%AI5_BeginPlace` to
/// `%AI5_EndPlace` start with `%_`, the versioned content's markers aside. The preview's header
/// line does too; its samples follow it raw.
fn hide_placed(data: &[u8]) -> Vec<u8> {
let find = |w: &[u8]| data.windows(w.len()).position(|x| x == w).unwrap();
let (from, to) = (find(b"%AI5_BeginPlace"), find(b"%AI5_EndPlace\r") + 14);
let mut out = data[..from].to_vec();
for line in data[from..to].split(|b| *b == b'\r') {
if !line.is_empty() && !line.starts_with(b"%AI17_") {
out.extend_from_slice(b"%_");
}
out.extend_from_slice(line);
out.push(b'\r');
}
out.pop();
out.extend_from_slice(&data[to..]);
out
}
#[test]
fn a_placed_file_on_commented_lines_is_read() {
// Its `%_%AI5_BeginPlace` was dropped as a comment after `%_`, so the `~` that ends it was an
// operator the reader didn't know, and the whole file came in as its PDF part; and its
// preview, whose header starts with `%_`, wasn't read.
let preview = [&b"2 1 12 32 17 "[..], &[255, 255, 0, 0, 128, 0, 64, 0, 9, 9, 9, 9]].concat();
let data = hide_placed(&placed(true, "[5 0 0 -5 0 200]", "/pics/photo.png", Some(&preview), ""));
assert!(data.windows(18).any(|w| w == b"%_%AI5_BeginPlace\r"), "{}", String::from_utf8_lossy(&data));
let r = read(&data).unwrap();
let a = art(&r.doc);
let NodeKind::Image(im) = &a[0].kind else { panic!("{:?}", a[0].kind) };
assert_eq!((im.link.as_ref().map(|l| l.path.as_str()), im.width, im.height), (Some("/pics/photo.png"), 2, 1));
let (img, _) = pixels(&r.doc);
assert_eq!((img.get_pixel(0, 0).0, img.get_pixel(1, 0).0), ([255, 0, 0, 255], [0, 127, 0, 128]), "its preview");
}
#[test]
fn placed_files_are_linked_images_showing_their_preview() {
// A 2×1 preview (premultiplied ARGB, a padded row) of a file of 4×2 pixels.
+2 -1
View File
@@ -1,6 +1,6 @@
# File format parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (PDF export: opaque gradient meshes as smooth Type 6 patch shadings) · **Target:** Adobe Illustrator 2026 (30.x)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (`.ai` import: placed files on `%_` lines no longer send the file to its PDF part) · **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 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 |
| 2026-10-30 | minor | `.ai` import: a file whose PDF part is an empty page opens from its editing data where that reads, instead of opening blank; scores unchanged |
| 2026-10-30 | minor | `.ai` import: a file whose PDF part's pages are cropped to its art rather than its artboards no longer comes in as that PDF part (each page a copy of all the art) but from its layers, with its artboards, where they draw as the page does; own-fixture test in `tests_aiimport`; scores unchanged |