.ai import: a page's art past its artboard no longer lies over the page before it

A PDF-compatible .ai keeps its art past the artboard edge unclipped (#646),
and its pages are laid out in a row 36 pt apart. Art that a page draws far
to the left of its artboard (notes beside a design, say) then lay over the
page before it. There it was taken for that page's art: the check against
the editing data found art its layers don't have and opened the file as
its PDF part, without the art outside its artboards, and place_pages left
that art behind when it moved the pages.

Such a page now moves right until its art clears the art before it.
Plain PDFs (clipped to the page) keep the 36 pt gap.
This commit is contained in:
fortesnow authored and bflatastic committed 2026-10-13 06:56:37 +07:00
1 parent 9dfa7bf931
commit ba68fcfbfc
3 files changed
+46 -2

No files matched your search

+22
View File
@@ -197,6 +197,8 @@ pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportRe
b.colors.uncalibrated_rgb = opts.uncalibrated_rgb && picked.iter().filter_map(|&n| pages.get(n)).any(crate::pages::has_private_data);
let cache = InterpreterCache::new();
let mut x = 0.0;
// Where the next page's art may begin (a `.ai` page's art reaches past its artboard).
let mut clear = f64::NEG_INFINITY;
// Every page only text over private data: a file saved without its PDF part shows a notice
// there, not its art. A page that states where its art is isn't that notice.
let (mut only_text, mut art_boxes) = (true, false);
@@ -267,6 +269,25 @@ pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportRe
if clipped {
b.warn(PAST_PAGE_NOTE);
}
// A `.ai` page keeps its art past the artboard edge, unclipped (#646), so a page whose art
// reaches far to the left would lay it over the page before it in the row: there it would
// be taken for that page's art (by the check against the editing data, which then finds
// art its layers don't have there, and by `place_pages`). Such a page moves right until its
// art clears the art before it.
let mut art_right = f64::NEG_INFINITY;
if ai && let Some(art) = parts.iter().flat_map(|(_, v)| v.iter()).filter_map(|n| n.visual_bounds()).reduce(|a, b| a.union(b)) {
let dx = (clear - art.x0).max(0.0);
if dx > 0.0 {
let shift = Affine::translate((dx, 0.0));
for (_, v) in &mut parts {
for n in v.iter_mut() {
Arc::make_mut(n).transform(shift, false);
}
}
ab = Rect::new(ab.x0 + dx, ab.y0, ab.x1 + dx, ab.y1);
}
art_right = art.x1 + dx;
}
let children: Vec<Arc<Node>> = parts.iter().flat_map(|(_, v)| v.iter().cloned()).collect();
only_text &= ai && is_only_text(&children);
art_boxes |= crate::pages::states_art(page);
@@ -278,6 +299,7 @@ pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportRe
ab = art;
}
x = right + opts.artboard_gap;
clear = right.max(art_right) + opts.artboard_gap;
doc.artboards.push(Artboard {
id: i as u32 + 1,
name: format!("Artboard {}", i + 1),
+21
View File
@@ -158,6 +158,27 @@ fn art_of_neighbouring_artboards_off_a_page_is_left_to_its_own_page() {
assert_eq!(d.layers[0].children().map_or(0, |c| c.len()), 2);
}
/// A `.ai` keeps its art past the artboard edge (#646): a note mostly left of artboard 2 (as notes
/// for a design are put beside it) doesn't lie over artboard 1 in the row, where it would be taken
/// for that page's art: page 2 moves right until its art clears page 1's.
#[test]
fn art_of_an_ai_page_reaching_left_does_not_lie_over_the_page_before() {
let ai = "/PieceInfo << /Illustrator << /Private << /AIPrivateData1 7 /NumBlock 1 >> >> >>";
let page = |content: &str| PdfPage { entries: ai.into(), ..PdfPage::new(100.0, 100.0, content) };
// Page 2's note runs from 70 points left of its box to 10 points into it.
let pages = [page("0 g 10 10 20 20 re f"), page("1 1 1 rg 1 0 0 RG -70 10 80 60 re B")];
let d = open(&pdf(&pages, None), ImportOptions::default());
let (a, b) = (d.artboards[0].rect, d.artboards[1].rect);
let note = d.layers[1].children().unwrap()[0].visual_bounds().unwrap();
assert!(note.x0 >= a.x1 + 36.0, "the note clears artboard 1: {note:?} {a:?}");
assert!((b.x0 - note.x0 - 70.0).abs() < 1.0, "and keeps its place by artboard 2: {note:?} {b:?}");
assert_eq!((b.width(), b.height()), (100.0, 100.0));
// A plain PDF clips its art to the page: its pages stay 36 points apart.
let plain = [PdfPage::new(100.0, 100.0, "0 g 10 10 20 20 re f"), PdfPage::new(100.0, 100.0, "1 0 0 RG -70 10 80 60 re S")];
let d = open(&pdf(&plain, None), ImportOptions::default());
assert_eq!(d.artboards[1].rect.x0, d.artboards[0].rect.x1 + 36.0);
}
#[test]
fn an_encrypted_pdf_opens_with_its_password_only() {
let bytes = pdf(&[PdfPage::new(200.0, 100.0, "1 0 0 rg 10 10 50 50 re f")], Some("secret"));
+3 -2
View File
@@ -1,6 +1,6 @@
# File format parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (grayscale TIFFs with an alpha channel or extra channels place and open, #1466 in part; a faded or masked fill keeps its stroke apart on PDF and `.ai` import, #1434; SVG knockout groups without masks, #1435; Affinity import: a text frame too short for its first line is made taller, #1423; `.ai` editing data: a compound path takes its last path's paint and keeps paths grouped inside it, #1410; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; `.ai` art styles, clipping fills and compound looks from the editing data; `editingData: "always"` opens a `.ai` from its editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; `.ai` art styles, clipping fills and compound looks from the editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; SVG knockout groups of opaque art without masks; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` keeps its RGB numbers; PDF import keeps a stroke apart from a fill masked or faded by its group) · **Target:** Adobe Illustrator 2026 (30.x)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (a `.ai` page's art past its artboard no longer lies over the page before it when read as its PDF part; grayscale TIFFs with an alpha channel or extra channels place and open, #1466 in part; a faded or masked fill keeps its stroke apart on PDF and `.ai` import, #1434; SVG knockout groups without masks, #1435; Affinity import: a text frame too short for its first line is made taller, #1423; `.ai` editing data: a compound path takes its last path's paint and keeps paths grouped inside it, #1410; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; `.ai` art styles, clipping fills and compound looks from the editing data; `editingData: "always"` opens a `.ai` from its editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; `.ai` art styles, clipping fills and compound looks from the editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` gradient matrices, meshes and artboard names from the editing data; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; SVG knockout groups of opaque art without masks; a `.ai` whose PDF part is only text opens from it, #1387; PDF and `.ai` import keep overprint, #1388; `.ai` editing data: gradients with middle stops, #1390; type of objects with an appearance, hidden layers and later artboards, #1391; area type in a frame too short for the fallback font, #1392; `.ai` editing data: placed (linked) files read, #1264; gradient-filled shapes import as paths, #1304; white constant alpha soft masks, #1286 in part; DXF control-point SPLINE closure read, #1299 follow-up; `.ai` and PDF trim and bleed boxes as artboard and bleed, #1266; `.ai` keeps its RGB numbers; PDF import keeps a stroke apart from a fill masked or faded by its group) · **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/mod.rs`). Illustrator's list comes from its public
@@ -50,7 +50,7 @@ private editing data. VectorCraft:
### Illustrator's private editing data
the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPrivateData` streams, a PDF's `AIPDFPrivateData` streams when it was saved with the editing data (#868), Zstandard or zlib, the EPS's in ASCII85) is not documented as a whole. VectorCraft reads its structure: layers and sublayers (names, order, visibility, lock, printing, preview, dimming, colour), groups as they were nested, compound paths (painted by their last path, or the last painted one, with the paths of groups inside them kept as their paths: #1410), clipping groups and layer clipping, object names, hidden and locked objects, fills and strokes (grey, CMYK, RGB, named and spot colours, spot colours defined in Lab with their Lab values (#1032), global process colours as global swatches at their tint), linear and radial gradients, pattern fills with their patterns as pattern swatches (#1025), opacity, blend modes, isolation and knockout, embedded images with their alpha channel, placed (linked) files as linked images in their place, each showing the low-resolution preview the file keeps of it until its file is found or relinked in the Links panel (#1264), guides, and every artboard where it is. A gradient with stops in the middle runs its whole ramp across its shape, and a gradient object does not pass its fill to the next path (#1390). An object whose art style is one fill and one stroke at most (solid colours, its own gradient, a variable-width profile) with an Outer Glow or Drop Shadow comes in as itself with them; one with more, other effects, a brush or dashes aligned to corners comes in as its drawn look (a group named after it). Gradients take their matrix (`Xm`, radials and skewed ones too), focal point and the order of stops sharing a ramp point; gradient meshes, a clipping path's own fill and the artboards' names are read too. `editingData: "always"` (`convert --editing-data`) opens a `.ai` from its editing data even where it draws differently from its page, which can be older, with a note. Type is made from the file's text document, whole and editable, shown or not: point type, area type (a story in several frames as threaded type) and type on a path, with its characters, fonts, size, leading, tracking, scaling, baseline shift, fill and stroke (grey, RGB or CMYK), alignment, indents and paragraph spacing; the page's drawing of it (often in pieces) is left out. Where the text document can't say (a frame of another kind, or a document that doesn't agree with the page), type that shows comes from the page into its text object's place (no group around it), and hidden type is left out with a note. Kerning, underline and the other character and paragraph options aren't read yet. A `.ai` saved without PDF compatibility (a placeholder page) opens from its editing data alone, its point type, area type and type on a path made from the text document. A `.ai` whose PDF part shows only text isn't taken for that placeholder when its page states an art, trim or bleed box: it opens from its editing data alone where that reads, else from its PDF part (#1387). Text objects with an appearance keep their own text and frame, type of hidden layers stays hidden, and type on later artboards lands on its text objects (#1391); area type whose frame is too short for a line of the fallback font shows its first line (the frame is made taller, with a warning in the open result) (#1392). Symbols, pattern fills whose pattern it can't read, placed files the data keeps no preview of and anything else it doesn't read make the file come in as its page (on a layer that doesn't show, they are left out of that layer), with a warning saying what. The layers' art is also compared with the page (by lightness, so colours written as their CMYK equivalents compare alike, and by whether a pixel is inked at all); the page is used when they differ by more than 5%, where one draws an object the other doesn't (away from type, whose lines may be laid out afresh), and when an EPS whose page is its art's box has art printed outside it (#735). An EPS whose page is its art's box keeps that page when it comes in through its layers, not the editing data's artboard, and a text object counts as drawn by the page by its colours, not only its lightness, so a red stroke stacked over a dark red one stays (#1102). `textAs: "outlines"` opens a `.ai` whose type shows from its PDF part, which has the outlines; `editingData: false` opens any of them as its page or PDF part only, for print pipelines (#735). A file whose import left something out (hidden text, art or layers) is never written over by an export or Save As to its own path unless `acknowledgeLoss: true` (`convert --replace-lossy`), so the original keeps what the document lacks. Where a `.ai` can't be read that way (or its layers are turned off in the import options), we read its PDF-compatible part, so Illustrator-only live objects arrive as appearance, and plain groups arrive ungrouped: that part draws each object on itLine truncated
the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPrivateData` streams, a PDF's `AIPDFPrivateData` streams when it was saved with the editing data (#868), Zstandard or zlib, the EPS's in ASCII85) is not documented as a whole. VectorCraft reads its structure: layers and sublayers (names, order, visibility, lock, printing, preview, dimming, colour), groups as they were nested, compound paths (painted by their last path, or the last painted one, with the paths of groups inside them kept as their paths: #1410), clipping groups and layer clipping, object names, hidden and locked objects, fills and strokes (grey, CMYK, RGB, named and spot colours, spot colours defined in Lab with their Lab values (#1032), global process colours as global swatches at their tint), linear and radial gradients, pattern fills with their patterns as pattern swatches (#1025), opacity, blend modes, isolation and knockout, embedded images with their alpha channel, placed (linked) files as linked images in their place, each showing the low-resolution preview the file keeps of it until its file is found or relinked in the Links panel (#1264), guides, and every artboard where it is. A gradient with stops in the middle runs its whole ramp across its shape, and a gradient object does not pass its fill to the next path (#1390). An object whose art style is one fill and one stroke at most (solid colours, its own gradient, a variable-width profile) with an Outer Glow or Drop Shadow comes in as itself with them; one with more, other effects, a brush or dashes aligned to corners comes in as its drawn look (a group named after it). Gradients take their matrix (`Xm`, radials and skewed ones too), focal point and the order of stops sharing a ramp point; gradient meshes, a clipping path's own fill and the artboards' names are read too. `editingData: "always"` (`convert --editing-data`) opens a `.ai` from its editing data even where it draws differently from its page, which can be older, with a note. Type is made from the file's text document, whole and editable, shown or not: point type, area type (a story in several frames as threaded type) and type on a path, with its characters, fonts, size, leading, tracking, scaling, baseline shift, fill and stroke (grey, RGB or CMYK), alignment, indents and paragraph spacing; the page's drawing of it (often in pieces) is left out. Where the text document can't say (a frame of another kind, or a document that doesn't agree with the page), type that shows comes from the page into its text object's place (no group around it), and hidden type is left out with a note. Kerning, underline and the other character and paragraph options aren't read yet. A `.ai` saved without PDF compatibility (a placeholder page) opens from its editing data alone, its point type, area type and type on a path made from the text document. A `.ai` whose PDF part shows only text isn't taken for that placeholder when its page states an art, trim or bleed box: it opens from its editing data alone where that reads, else from its PDF part (#1387). Text objects with an appearance keep their own text and frame, type of hidden layers stays hidden, and type on later artboards lands on its text objects (#1391); area type whose frame is too short for a line of the fallback font shows its first line (the frame is made taller, with a warning in the open result) (#1392). Symbols, pattern fills whose pattern it can't read, placed files the data keeps no preview of and anything else it doesn't read make the file come in as its page (on a layer that doesn't show, they are left out of that layer), with a warning saying what. The layers' art is also compared with the page (by lightness, so colours written as their CMYK equivalents compare alike, and by whether a pixel is inked at all); the page is used when they differ by more than 5%, where one draws an object the other doesn't (away from type, whose lines may be laid out afresh), and when an EPS whose page is its art's box has art printed outside it (#735). An EPS whose page is its art's box keeps that page when it comes in through its layers, not the editing data's artboard, and a text object counts as drawn by the page by its colours, not only its lightness, so a red stroke stacked over a dark red one stays (#1102). `textAs: "outlines"` opens a `.ai` whose type shows from its PDF part, which has the outlines; `editingData: false` opens any of them as its page or PDF part only, for print pipelines (#735). A file whose import left something out (hidden text, art or layers) is never written over by an export or Save As to its own path unless `acknowledgeLoss: true` (`convert --replace-lossy`), so the original keeps what the document lacks. Where a `.ai` can't be read that way (or its layers are turned off in the import options), we read its PDF-compatible part, so Illustrator-only live objects arrive as appearance, and plain groups arrive ungrouped: that part draws each object on itLine truncated
## Format by format
@@ -86,6 +86,7 @@ the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPri
| Date | Change | Summary |
|---|---|---|
| 2026-10-13 | minor | `.ai` read as its PDF part: a page whose art reaches past its artboard to the left (notes beside a design) moves right in the row until that art clears the page before it, so it is no longer taken for that page's art; a multi-artboard `.ai` with such notes opened as its PDF part ("the art on them differs from the page's"), without the art outside its artboards, and now opens from its editing data |
| 2026-10-13 | minor | TIFF: grayscale files with an alpha channel or other extra channels place, relink and open, read as CMYK files with alpha already were (8 and 16 bits, either polarity; #1489, part of #1466); the rows for #1434 (strokes kept apart from a faded or masked fill, a `.ai`'s RGB numbers) and #1435 (SVG knockout groups without masks, `--decimals`) were written by those PRs; scores unchanged (estimated) |
| 2026-10-12 | minor | Affinity import: a text frame too short for its first line (a 64 pt title in a 54 pt frame) is made taller from its top, with a warning in the open result, instead of showing nothing (#1423), as `.ai` area type is (#1392); the spacing Affinity itself fits the line with isn't read; scores unchanged |
| 2026-10-12 | minor | `.ai` editing data: a compound path takes the paint, opacity, blend mode and fill rule of its last path (the last painted one when that has none), so gradient-filled compound paths no longer come in black, and paths grouped inside a compound path stay its paths (#1426, fixes #1410); hand-written editing data test; scores unchanged |