Merge pull request #7723 from dotCooCoo/fix/pdf-page-links-review

Import PDF: linked pages refuse placeholder .ai files, read the file once and keep blank pages
This commit is contained in:
BFlat
2026-11-15 11:37:26 +07:00
committed by GitHub
9 changed files with 240 additions and 37 deletions
+1
View File
@@ -91,6 +91,7 @@ pub(crate) use mesh3d::{mesh_file_names, mesh_files};
pub use naming::{NameParts as FileNameParts, parse_date as parse_file_date, render as render_file_name};
pub(crate) use native::preview_png;
pub use native::with_compression_pref;
pub(crate) use pdfimport::opened_page_document;
pub use pdfimport::{LoadOptions, page_document};
pub(crate) use save::job_for;
pub(crate) use save::save as save_document;
File diff suppressed because one or more lines are too long
+58 -15
View File
@@ -205,7 +205,7 @@ pub fn page_document(bytes: &[u8], page: usize, o: &LoadOptions) -> Result<(Docu
}
/// [`page_document`] of `opened`, the PDF `bytes` already read.
fn opened_page_document(bytes: &[u8], opened: &vectorcraft_pdf::Opened, page: usize, o: &LoadOptions) -> Result<(Document, Vec<String>)> {
pub(crate) fn opened_page_document(bytes: &[u8], opened: &vectorcraft_pdf::Opened, page: usize, o: &LoadOptions) -> Result<(Document, Vec<String>)> {
vectorcraft_pdf::import_opened(bytes, opened, &page_options(page, o)).map(|r| (r.document, r.warnings)).map_err(err)
}
@@ -217,8 +217,11 @@ fn page_options(page: usize, o: &LoadOptions) -> ImportOptions {
/// `document.open` with `linkPages`: the pages `o` picks of the PDF (or PDF-compatible `.ai` or
/// `.ait`) `name`, read from `path`, as a document holding one placed document per page, linked to
/// that page of the file as File › Place with Link places one ([`document::source`]). Each page
/// gets an artboard of its `o.crop` box (its art's bounds for `bounding`), in a row from the left
/// with the gap a PDF's artboards open with, and a layer named after the page. The document opens
/// gets an artboard of its `o.crop` box (its art's bounds for `bounding`, a blank page's crop box;
/// 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 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
@@ -239,12 +242,16 @@ pub(super) fn linked_pages(name: &str, bytes: &[u8], path: Option<&str>, o: &Loa
if bytes.len() > document::MAX_FILE {
return Err(bad(C, format!("linkPages: `{file}` is over {} MB", document::MAX_FILE >> 20)));
}
let count = vectorcraft_pdf::open_pdf(bytes, None).map_err(err)?.page_count();
// The file is read once: every page is read from it.
let opened = vectorcraft_pdf::open_pdf(bytes, None).map_err(err)?;
let count = opened.page_count();
let picked = match &o.pages {
None => (0..count).collect(),
Some(range) => parse_range(range, count).map_err(|e| err(format!("pages: {e}")))?,
};
let bounding = o.crop == CropTo::Bounding;
// A PDF-compatible `.ai` asked for its crop box opens on its trim boxes ([`LoadOptions::import_options`]).
let crop = if o.crop == CropTo::Crop && opened.has_illustrator_data() { CropTo::Trim } else { o.crop };
let mut doc = Document::new(1.0, 1.0);
let board = doc.artboards.first().cloned().ok_or_else(|| err("a new document has no artboard"))?;
doc.artboards.clear();
@@ -253,14 +260,55 @@ pub(super) fn linked_pages(name: &str, bytes: &[u8], path: Option<&str>, o: &Loa
let gap = ImportOptions::default().artboard_gap;
// Every page's object keeps the same one copy of the file.
let shared = Arc::new(bytes.to_vec());
let (mut x, mut warnings, mut cmyk_pages) = (0.0, Vec::<String>::new(), 0);
for (i, &index) in picked.iter().enumerate() {
// 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 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 s = document::shared_source(name, &shared, Some(path), page, bounding, document::page_box(o.crop), C)?;
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
// the art in the bleed shows and prints as bleed.
let bleed = if crop == CropTo::Trim { opened.page_bleed(index) } else { None };
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 (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),
};
// A page that is only a placeholder (a `.ai` saved without PDF compatibility) shows the
// file's editing data, not a page of the file.
if s.editing_data {
return Err(bad(C, format!("linkPages: `{file}` isn't a PDF or a PDF-compatible .ai file")));
}
cmyk_pages += usize::from(s.color_mode == ColorMode::Cmyk);
// The document's bleed is the widest of the pages' on each side.
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, 0.0));
p.xf = Affine::translate((x - left, -top));
}
node.name = Some(file.clone());
let mut layer = Node::layer(doc.alloc_id(), &format!("Page {page}"), LayerColor::Preset((i % 27) as u8));
@@ -268,14 +316,9 @@ pub(super) fn linked_pages(name: &str, bytes: &[u8], path: Option<&str>, o: &Loa
children.push(Arc::new(node));
}
doc.layers.push(Arc::new(layer));
let rect = Rect::new(x, 0.0, x + s.frame.width(), s.frame.height());
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.
@@ -1160,6 +1160,91 @@ fn an_ai_file_saved_without_its_pdf_part_places_from_its_editing_data() {
assert!(e.contains("placeholder"), "{e}");
}
/// `document.open` with `linkPages` refuses a `.ai` saved without PDF compatibility, whose page is
/// only a placeholder, and opens a PDF-compatible one.
#[test]
fn link_pages_refuses_an_ai_file_saved_without_its_pdf_part() {
let d = std::env::temp_dir().join(format!("vc-ailinkpages-{}", std::process::id()));
std::fs::create_dir_all(&d).unwrap();
let file = |name: &str, page: &str| {
let path = d.join(name);
std::fs::write(&path, ai::ai(&compressed(&ai::sample_data()), page)).unwrap();
path.to_string_lossy().to_string()
};
let (plain, compatible) = (file("plain.ai", PLACEHOLDER), file("compatible.ai", "0 g 10 10 40 40 re f"));
let mut s = Session::new();
let e = s.execute("document.open", &json!({"path": plain, "linkPages": true})).unwrap_err().to_string();
assert!(e.contains("isn't a PDF or a PDF-compatible .ai file"), "{e}");
assert!(s.documents().is_empty(), "nothing opened");
s.execute("document.open", &json!({"path": compatible, "linkPages": true})).unwrap();
assert!(s.doc().unwrap().doc.has_placed());
let _ = std::fs::remove_dir_all(&d);
}
/// `document.open` with `linkPages` opens a PDF-compatible `.ai` asked for its crop box on its trim
/// box, as it opens as art, showing its bleed box round the artboard, which is the document's bleed.
#[test]
fn link_pages_open_an_ai_file_on_its_trim_box_with_its_bleed() {
let d = std::env::temp_dir().join(format!("vc-ailinkpagestrim-{}", std::process::id()));
std::fs::create_dir_all(&d).unwrap();
let path = d.join("trimmed.ai");
let boxes = "/TrimBox [10 10 190 90] /BleedBox [4 2 196 98]";
std::fs::write(&path, ai::ai_with(&compressed(&ai::sample_data()), "0 g 10 10 40 40 re f", boxes)).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;
let board = doc.artboards[0].rect;
assert_eq!((board.width(), board.height()), (180.0, 80.0), "the trim box");
assert_eq!(doc.setup.bleed, [8.0, 8.0, 6.0, 6.0], "top, bottom, left, right");
// The page shows its bleed box, so the art in the bleed is kept.
let placed = doc.layers[0].children().unwrap()[0].clone();
let NodeKind::PlacedDocument(p) = &placed.kind else { panic!("{:?}", placed.kind) };
assert_eq!((p.width, p.height, p.page_box), (192.0, 96.0, vectorcraft_doc::PageBox::Bleed));
assert_eq!(p.xf.translation(), vectorcraft_geom::Vec2::new(-6.0, -8.0), "the trim box on the artboard");
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";
+12 -6
View File
@@ -257,7 +257,7 @@ fn load(p: &Value, cmd: &str, board: Option<Rect>, link_pdf: bool) -> Result<Loa
let (natural, warnings) = (s.frame, s.warnings.clone());
return Ok(Loaded { name, format, art: Art::Document(s, link.clone(), !crop), natural, warnings, link: Some(link), board: None });
}
let a = pdf_art(src.name, &src.bytes, src.path, page, opts, !crop, cmd)?;
let a = pdf_art(src.name, (&src.bytes, None), src.path, page, opts, !crop, cmd)?;
let art = Art::Vector { src: Box::new(a.doc), nodes: a.nodes, clip: a.clip };
return Ok(Loaded { name, format, art, natural: a.frame, warnings: a.warnings, link: None, board: None });
}
@@ -308,10 +308,12 @@ fn load(p: &Value, cmd: &str, board: Option<Rect>, link_pdf: bool) -> Result<Loa
/// art; read with `opts.password`. Only the page placed is read. A .ai saved without PDF
/// compatibility is read from its editing data, as it opens, and placed as an EPS is: the art on
/// artboard `page`, its linked images found from its folder. So is page 1 of a .ai or PDF whose
/// PDF part can't be read.
/// PDF part can't be read. `opened`, when given, is the PDF `bytes` already read
/// ([`vectorcraft_pdf::open_pdf`] with `opts`' password): its pages are read without reading the
/// file again.
pub(crate) fn pdf_art(
src: &str,
bytes: &[u8],
(bytes, opened): (&[u8], Option<&vectorcraft_pdf::Opened>),
path: Option<&str>,
page: usize,
opts: fileio::LoadOptions,
@@ -321,11 +323,15 @@ pub(crate) fn pdf_art(
let name = fileio::file_name(src);
let index = page.checked_sub(1).ok_or_else(|| bad(cmd, "page must be a whole number from 1"))?;
let o = fileio::LoadOptions { crop: if bounding { CropTo::Crop } else { opts.crop }, ..opts };
let mut read = fileio::page_document(bytes, index, &o);
let page_document = |i: usize| match opened {
Some(opened) => fileio::opened_page_document(bytes, opened, i, &o),
None => fileio::page_document(bytes, i, &o),
};
let mut read = page_document(index);
// Such a file can have more artboards than placeholder pages.
if page > 1
&& read.is_err()
&& let Err(e) = fileio::page_document(bytes, 0, &o)
&& let Err(e) = page_document(0)
&& fileio::is_placeholder(&e)
{
read = Err(e);
@@ -351,7 +357,7 @@ pub(crate) fn pdf_art(
let clip = (!bounding).then_some(board);
let frame = clip.or_else(|| shown_bounds(&symbols, &nodes));
let frame = frame.filter(|r| r.width() > 0.0 || r.height() > 0.0).ok_or_else(|| bad(cmd, format!("`{name}` has no art to place")))?;
Ok(document::DocumentArt { doc, nodes, clip, frame, warnings })
Ok(document::DocumentArt { doc, nodes, clip, frame, warnings, editing_data: alone })
}
/// The bounds of what `nodes` draw, as an export measures art
+48 -13
View File
@@ -100,6 +100,9 @@ pub(crate) struct DocumentArt {
/// What is shown: the artboard, or the art's bounds.
pub frame: Rect,
pub warnings: Vec<String>,
/// Whether a PDF or `.ai` page was read from the file's editing data (its page only a
/// placeholder, or its PDF part unreadable) instead of from the page.
pub editing_data: bool,
}
/// Artboard `page` (1-based) of the VectorCraft document `name` (`bytes`, read from `path` when
@@ -114,6 +117,21 @@ pub(crate) fn document_art(
bounding: bool,
page_box: PageBox,
cmd: &str,
) -> Result<DocumentArt> {
opened_document_art(name, (bytes, None), path, page, bounding, page_box, cmd)
}
/// [`document_art`] of `bytes` and, for a PDF already read, `opened`: the PDF `bytes` as
/// [`vectorcraft_pdf::open_pdf`] read it without a password, so its page is read without reading
/// the file again.
fn opened_document_art(
name: &str,
(bytes, opened): (&[u8], Option<&vectorcraft_pdf::Opened>),
path: Option<&str>,
page: u32,
bounding: bool,
page_box: PageBox,
cmd: &str,
) -> Result<DocumentArt> {
let file = fileio::file_name(name);
let pdf = is_pdf(name, bytes);
@@ -125,7 +143,7 @@ pub(crate) fn document_art(
}
if pdf {
let o = fileio::LoadOptions { crop: crop_to(page_box), ..Default::default() };
return super::pdf_art(name, bytes, path, page as usize, o, bounding, cmd);
return super::pdf_art(name, (bytes, opened), path, page as usize, o, bounding, cmd);
}
let mut l = fileio::load(name, bytes)?;
// Its linked images show their files; its placed documents stay as they are, unless their
@@ -145,7 +163,7 @@ pub(crate) fn document_art(
let nodes = super::on_artboard(&symbols, &layers, board);
let frame = if bounding { super::shown_bounds(&symbols, &nodes) } else { Some(board) };
let frame = frame.filter(|r| r.width() > 0.0 || r.height() > 0.0).ok_or_else(|| bad(cmd, format!("`{file}` has no art to place")))?;
Ok(DocumentArt { doc: l.doc, nodes, clip: (!bounding).then_some(board), frame, warnings: l.warnings })
Ok(DocumentArt { doc: l.doc, nodes, clip: (!bounding).then_some(board), frame, warnings: l.warnings, editing_data: false })
}
/// The most placed documents read inside one another while their links are resolved.
@@ -223,6 +241,8 @@ pub(crate) struct Source {
pub warnings: Vec<String>,
/// The colour mode the file's art was read in (a PDF page painted mostly in CMYK reads in CMYK).
pub color_mode: ColorMode,
/// [`DocumentArt::editing_data`].
pub editing_data: bool,
}
/// The VectorCraft document `name` (`bytes`, from `path` when there is one) read for showing
@@ -232,33 +252,48 @@ pub(crate) fn source(name: &str, bytes: &[u8], path: Option<&str>, page: u32, bo
read_source(name, bytes, None, path, (page, bounding, page_box), cmd)
}
/// [`source`] of the file whose bytes `bytes` holds, kept by the blob without a copy of its own:
/// several pages of one file read this way share one copy of it.
/// A PDF (or PDF-compatible `.ai`) whose pages are read for placing linked, read once for them all.
#[derive(Clone, Copy)]
pub(crate) struct SharedFile<'a> {
/// The file's bytes, which every page's blob keeps without a copy of its own.
pub bytes: &'a Arc<Vec<u8>>,
/// The bytes' hash ([`hash_bytes`]).
pub hash: &'a str,
/// The bytes as [`vectorcraft_pdf::open_pdf`] read them, without a password.
pub opened: &'a vectorcraft_pdf::Opened,
}
/// [`source`] of page `page` of `file`: several pages of one file read this way share one copy of
/// it, and the file is hashed and read once for them all.
pub(crate) fn shared_source(
name: &str,
bytes: &Arc<Vec<u8>>,
file: SharedFile<'_>,
path: Option<&str>,
page: u32,
bounding: bool,
page_box: PageBox,
cmd: &str,
) -> Result<Source> {
read_source(name, bytes, Some(bytes), path, (page, bounding, page_box), cmd)
read_source(name, file.bytes, Some(file), path, (page, bounding, page_box), cmd)
}
/// [`source`], the blob keeping `shared` when given (else a copy of `bytes`); `shown` is the page
/// (or artboard), whether the art's bounds are shown, and the page's box.
/// [`source`], the blob keeping `shared`'s bytes when given (else a copy of `bytes`), its hash
/// used for the key (else `bytes` hashed) and its pages read from what was read of it; `shown` is
/// the page (or artboard), whether the art's bounds are shown, and the page's box.
fn read_source(
name: &str,
bytes: &[u8],
shared: Option<&Arc<Vec<u8>>>,
shared: Option<SharedFile<'_>>,
path: Option<&str>,
shown: (u32, bool, PageBox),
cmd: &str,
) -> Result<Source> {
let (page, bounding, page_box) = shown;
let hash = shared.map_or_else(|| hash_bytes(bytes), |f| f.hash.to_string());
let opened = shared.map(|f| f.opened);
let shared = shared.map(|f| f.bytes);
// The same document placed the same way is read once (placing it again is free).
let key = key_for(&hash_bytes(bytes), page, bounding, page_box);
let key = key_for(&hash, page, bounding, page_box);
// Resolving its links depends on where the file is and on what the caller may read, so a hit
// needs the same path, and nothing is shared with or from a confined (automation) caller.
let confined = crate::file_access::current().is_some();
@@ -269,7 +304,7 @@ fn read_source(
}
return Ok(s);
}
let a = document_art(name, bytes, path, page, bounding, page_box, cmd)?;
let a = opened_document_art(name, (bytes, opened), path, page, bounding, page_box, cmd)?;
let bytes = shared.cloned().unwrap_or_else(|| Arc::new(bytes.to_vec()));
let mut blob = ImageBlob { mime: mime(name, &bytes).into(), bytes, proxy: None };
let shown = PlacedDocument {
@@ -284,10 +319,10 @@ fn read_source(
};
let art = group(a.nodes, a.clip);
blob.proxy = preview(&a.doc, &art, a.frame).map(Arc::new);
let color_mode = a.doc.color_mode;
let (color_mode, editing_data) = (a.doc.color_mode, a.editing_data);
// Drawing it needn't read it again.
vectorcraft_doc::placed_document::prime(&shown, blob.bytes.clone(), (a.doc, art, a.frame));
let s = Source { blob, key, page_box, frame: a.frame, warnings: a.warnings, color_mode };
let s = Source { blob, key, page_box, frame: a.frame, warnings: a.warnings, color_mode, editing_data };
if !confined {
let mut m = lock_memo();
if m.len() >= MEMO_SIZE {
@@ -666,6 +666,26 @@ fn linked_pages_share_one_copy_of_the_file_and_open_in_the_pages_color_mode() {
assert_eq!(s.doc().unwrap().doc.color_mode, vectorcraft_doc::ColorMode::Rgb, "colorMode wins");
}
#[test]
fn linked_pages_read_the_file_once_and_a_blank_page_keeps_its_crop_box() {
use vectorcraft_testkit::pdf::{PdfPage, pdf};
let dir = Folder::new("open-pdf-link-pages-blank");
let src = dir.file("blank.pdf");
let art = || PdfPage::new(100.0, 50.0, "1 0 0 rg 10 10 20 20 re f");
write(&src, &pdf(&[art(), PdfPage::new(80.0, 40.0, ""), art()], None));
let mut s = Session::new();
s.execute("document.open", &json!({"path": src, "linkPages": true, "cropTo": "bounding"})).unwrap();
let d = &s.doc().unwrap().doc;
let boards: Vec<Rect> = d.artboards.iter().map(|a| a.rect).collect();
assert_eq!(boards.len(), 3, "{boards:?}");
assert_eq!((boards[0].width(), boards[0].height()), (20.0, 20.0), "the art's bounds");
assert_eq!((boards[1].width(), boards[1].height()), (80.0, 40.0), "the blank page's crop box");
// The pages are read from the one copy every page's blob keeps: a page that read the file
// again would count here.
let blob = d.images.values().next().map(|b| b.bytes.clone()).unwrap();
assert_eq!(vectorcraft_pdf::reads_of(&blob), 0, "the pages are read from the file read once");
}
#[test]
fn link_pages_is_refused_without_a_path_for_other_files_and_with_a_password_or_outlines() {
let dir = Folder::new("open-pdf-link-pages-refused");
+11
View File
@@ -297,6 +297,17 @@ impl Opened {
bleed,
}
}
/// How far page `index`'s (0-based) bleed box reaches past its trim box, `[top, bottom, left,
/// right]` in points, as the page shows (rotated); `None` past the last page, and when the bleed
/// box doesn't hold the trim box.
pub fn page_bleed(&self, index: usize) -> Option<[f64; 4]> {
let page = self.pdf.pages().get(index)?;
let init = Affine::new(page.initial_transform(true).as_coeffs());
let (trim, bleed) = (init.transform_rect_bbox(page_box(page, CropTo::Trim)), init.transform_rect_bbox(page_box(page, CropTo::Bleed)));
let sides = [trim.y0 - bleed.y0, bleed.y1 - trim.y1, trim.x0 - bleed.x0, bleed.x1 - trim.x1];
sides.iter().all(|b| b.is_finite() && *b >= 0.0).then_some(sides)
}
}
/// A PDF's pages as an import on their trim boxes sizes them ([`Opened::trim_pages`]).
+4 -2
View File
@@ -1298,8 +1298,10 @@ opens at its size in points, the file's numbers times the `UserUnit` (one above
such a page opens from that data at the page's size.
`linkPages: true` (File › Open's Import PDF dialog: Import PDF pages as links) opens each page `pages` picks as a
placed document linked to that page of the file, as `file.place` with `link` places one, instead of as editable art.
Each page gets an artboard of its `cropTo` box (`bounding`: its art's bounds), in a row from the left 36 pt apart, and
a layer named after the page (`Page 2`). The document opens in CMYK when most of those pages are painted mostly in
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 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