mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 05:55:32 +00:00
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:
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user