.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 2026-10-13 07:52:20 +09:00 committed by bflatastic
parent 9dfa7bf931
commit ba68fcfbfc
3 changed files with 46 additions and 2 deletions

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),

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"));

File diff suppressed because one or more lines are too long