Import PDF: linked pages on their trim boxes keep the bleed between artboards

Linked pages on their trim boxes were spaced by the 36 pt gap alone, so
with a large bleed one page's bleed box reached into the next artboard
and a page export could show part of its neighbor. The artboards are
now laid out once the document's bleed is known, 36 pt apart plus the
bleed on both sides: the document's bleed, or a page's own bleed box
where it reaches farther.
This commit is contained in:
dotCooCoo
2026-11-14 17:56:30 -08:00
parent e6bfe40d30
commit 3b788515d1
4 changed files with 68 additions and 14 deletions
File diff suppressed because one or more lines are too long
+26 -12
View File
@@ -221,7 +221,7 @@ fn page_options(page: usize, o: &LoadOptions) -> ImportOptions {
/// a PDF-compatible `.ai` asked for its crop box gets its trim box, as it opens), in a row from the
/// left with the gap a PDF's artboards open with, and a layer named after the page. On its trim box,
/// a page shows its bleed box round the artboard, and the document's bleed is the pages' bleed (the
/// widest on each side). The document opens
/// widest on each side); the artboards are then the gap apart plus the bleed on both sides. The document opens
/// in CMYK when most of the pages read in CMYK, unless `o.color_mode` names a mode. Refused without a
/// path, for any other file, with a password (a linked page is read again without one), with
/// `textAs: outlines` (a linked page shows the file's own type) and for a file over
@@ -262,8 +262,11 @@ pub(super) fn linked_pages(name: &str, bytes: &[u8], path: Option<&str>, o: &Loa
let shared = Arc::new(bytes.to_vec());
// The file is hashed once for all its pages.
let hash = link.hash.clone().unwrap_or_else(|| vectorcraft_doc::links::hash_bytes(bytes));
let (mut x, mut warnings, mut cmyk_pages) = (0.0, Vec::<String>::new(), 0);
for (i, &index) in picked.iter().enumerate() {
let (mut warnings, mut cmyk_pages) = (Vec::<String>::new(), 0);
// Each page as read, with its bleed (top, bottom, left, right). The pages are laid out once
// the document's bleed is known.
let mut read_pages = Vec::with_capacity(picked.len());
for &index in &picked {
let page = index as u32 + 1;
let file_once = document::SharedFile { bytes: &shared, hash: &hash, opened: &opened };
// On its trim box, a page shows its bleed box, the artboard on the trim box inside it, so
@@ -272,7 +275,7 @@ pub(super) fn linked_pages(name: &str, bytes: &[u8], path: Option<&str>, o: &Loa
let page_box = if bleed.is_some() { vectorcraft_doc::PageBox::Bleed } else { document::page_box(crop) };
let read = |bounding| document::shared_source(name, file_once, Some(path), page, bounding, page_box, C);
// A blank page has no art's bounds: it keeps its crop box, as opening it as art does.
let (s, bounding) = match read(bounding) {
let (mut s, bounding) = match read(bounding) {
Err(e) if bounding && e.to_string().contains("has no art to place") => (read(false)?, false),
s => (s?, bounding),
};
@@ -283,10 +286,26 @@ pub(super) fn linked_pages(name: &str, bytes: &[u8], path: Option<&str>, o: &Loa
}
cmyk_pages += usize::from(s.color_mode == ColorMode::Cmyk);
// The document's bleed is the widest of the pages' on each side.
let [top, bottom, left, right] = bleed.unwrap_or_default();
for (side, b) in doc.setup.bleed.iter_mut().zip([top, bottom, left, right]) {
let sides = bleed.unwrap_or_default();
for (side, b) in doc.setup.bleed.iter_mut().zip(sides) {
*side = side.max(b.min(vectorcraft_doc::setup::MAX_BLEED));
}
for w in std::mem::take(&mut s.warnings) {
if !warnings.contains(&w) {
warnings.push(w);
}
}
read_pages.push((page, s, bounding, sides));
}
// Each artboard is the gap from the one before plus the bleed on both sides of it: the
// document's bleed, which export adds round every artboard, or a page's own bleed box where it
// reaches farther. Neither page then shows or exports the other's art.
let [_, _, doc_left, doc_right] = doc.setup.bleed;
let mut x = 0.0;
for (i, (page, s, bounding, [top, bottom, left, right])) in read_pages.into_iter().enumerate() {
if i > 0 {
x += doc_left.max(left);
}
let mut node = document::node(&mut doc, &s, LinkInfo { page: Some(page), ..link.clone() }, bounding);
if let NodeKind::PlacedDocument(p) = &mut node.kind {
p.xf = Affine::translate((x - left, -top));
@@ -299,12 +318,7 @@ pub(super) fn linked_pages(name: &str, bytes: &[u8], path: Option<&str>, o: &Loa
doc.layers.push(Arc::new(layer));
let rect = Rect::new(x, 0.0, x + s.frame.width() - left - right, s.frame.height() - top - bottom);
doc.artboards.push(Artboard { id: i as u32 + 1, name: format!("Artboard {}", i + 1), rect, ..board.clone() });
x = rect.x1 + gap;
for w in s.warnings {
if !warnings.contains(&w) {
warnings.push(w);
}
}
x = rect.x1 + doc_right.max(right) + gap;
}
// Pages painted mostly in CMYK read as CMYK documents, as opening them as art does: the
// document opens in CMYK (with the CMYK default swatches) when most of its pages do.
@@ -1205,6 +1205,46 @@ fn link_pages_open_an_ai_file_on_its_trim_box_with_its_bleed() {
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn link_pages_on_their_trim_boxes_keep_the_bleed_between_the_artboards() {
// Two 100 pt trim boxes: the first with 60 pt of bleed on each side, the second with 80 pt on
// its left, more than the largest document bleed (72 pt).
let entries = format!("/PieceInfo << /Illustrator << /Private << /AIPrivateData1 {} 0 R /NumBlock 1 >> >> >>", first_extra(2));
let first = PdfPage {
bleed: Some([0.0, 0.0, 220.0, 220.0]),
trim: Some([60.0, 60.0, 160.0, 160.0]),
entries,
..PdfPage::new(220.0, 220.0, "0 g 0 0 220 220 re f")
};
let second = PdfPage {
bleed: Some([0.0, 0.0, 240.0, 220.0]),
trim: Some([80.0, 60.0, 180.0, 160.0]),
..PdfPage::new(240.0, 220.0, "0 g 0 0 240 220 re f")
};
let d = std::env::temp_dir().join(format!("vc-ailinkpagesgap-{}", std::process::id()));
std::fs::create_dir_all(&d).unwrap();
let path = d.join("pages.ai");
std::fs::write(&path, pdf_with(&[first, second], &["<< /Length 4 >>\nstream\ndata\nendstream"], None)).unwrap();
let path = path.to_string_lossy().to_string();
let mut s = Session::new();
s.execute("document.open", &json!({"path": path, "linkPages": true})).unwrap();
let doc = &s.doc().unwrap().doc;
assert_eq!(doc.setup.bleed, [60.0, 60.0, 72.0, 60.0], "top, bottom, left, right");
// The first page's bleed ends at 160; the second's starts 36 pt later, 80 pt left of its trim box.
let boards: Vec<(f64, f64)> = doc.artboards.iter().map(|a| (a.rect.x0, a.rect.x1)).collect();
assert_eq!(boards, [(0.0, 100.0), (276.0, 376.0)]);
let shown: Vec<f64> = doc
.layers
.iter()
.map(|l| match &l.children().unwrap()[0].kind {
NodeKind::PlacedDocument(p) => p.xf.translation().x,
k => panic!("{k:?}"),
})
.collect();
assert_eq!(shown, [-60.0, 196.0], "each page's bleed box round its artboard");
let _ = std::fs::remove_dir_all(&d);
}
/// A page of text alone, as text-only art has in its PDF part: no path is painted.
const TEXT_ART: &str = "BT /F1 36 Tf 10 40 Td (Text only) Tj ET";
+1 -1
View File
@@ -1282,7 +1282,7 @@ placed document linked to that page of the file, as `file.place` with `link` pla
Each page gets an artboard of its `cropTo` box (`bounding`: its art's bounds; a blank page gets its crop box), in a row from the left 36 pt apart, and
a layer named after the page (`Page 2`). A PDF-compatible `.ai` with `cropTo: "crop"` gets its trim boxes, as it does
when it opens as art. A page on its trim box shows its bleed box round the artboard, and the pages give the document
their bleed. The document opens in CMYK when most of those pages are painted mostly in
their bleed; the artboards are then 36 pt apart plus the bleed on both sides. The document opens in CMYK when most of those pages are painted mostly in
CMYK, unless `colorMode` names a mode. All the pages' objects share one copy of the file. The Links panel lists each
page, and `links.embed` makes a page editable art.
`document.open` refuses `linkPages` for a file that is not a PDF or a PDF-compatible `.ai`, for a file given as