Import PDF: linked pages read the file once and keep blank pages

linkPages read and checked the whole PDF again for every page it
linked. The pages are now read from the file opened once. With
cropTo: bounding, a blank page stopped the open with "has no art to
place"; it now gets its crop box, as opening the page as art does.
This commit is contained in:
dotCooCoo
2026-11-14 16:42:05 -08:00
parent c730a1c776
commit cc7d081198
7 changed files with 85 additions and 24 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
+12 -4
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,7 +217,7 @@ 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
/// gets an artboard of its `o.crop` box (its art's bounds for `bounding`, a blank page's crop box), 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
/// 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
@@ -239,7 +239,9 @@ 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}")))?,
@@ -258,7 +260,13 @@ pub(super) fn linked_pages(name: &str, bytes: &[u8], path: Option<&str>, o: &Loa
let (mut x, mut warnings, mut cmyk_pages) = (0.0, Vec::<String>::new(), 0);
for (i, &index) in picked.iter().enumerate() {
let page = index as u32 + 1;
let s = document::shared_source(name, (&shared, &hash), Some(path), page, bounding, document::page_box(o.crop), C)?;
let file_once = document::SharedFile { bytes: &shared, hash: &hash, opened: &opened };
let read = |bounding| document::shared_source(name, file_once, Some(path), page, bounding, document::page_box(o.crop), 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) {
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 {
+11 -5
View File
@@ -254,7 +254,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 });
}
@@ -305,10 +305,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,
@@ -318,11 +320,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);
+39 -13
View File
@@ -117,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);
@@ -128,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
@@ -237,35 +252,46 @@ 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, with its hash ([`hash_bytes`])
/// worked out once for them all: `file` is the bytes and that hash.
/// 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,
file: (&Arc<Vec<u8>>, &str),
file: SharedFile<'_>,
path: Option<&str>,
page: u32,
bounding: bool,
page_box: PageBox,
cmd: &str,
) -> Result<Source> {
read_source(name, file.0, Some(file), path, (page, bounding, page_box), cmd)
read_source(name, file.bytes, Some(file), path, (page, bounding, page_box), cmd)
}
/// [`source`], the blob keeping `shared`'s bytes when given (else a copy of `bytes`), and its hash
/// used for the key (else `bytes` hashed); `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>>, &str)>,
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), |(_, h)| h.to_string());
let shared = shared.map(|(b, _)| b);
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, page, bounding, page_box);
// Resolving its links depends on where the file is and on what the caller may read, so a hit
@@ -278,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 {
@@ -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");
+1 -1
View File
@@ -1279,7 +1279,7 @@ 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
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`). 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.