PDF/.ai import: the file is read and checked once when it opens

Opening a .ai or a PDF read and checked the whole file up to five
times: for the document it carries, for whether it holds the editor's
private data, for the import, for the editor's copy and for its File
Info. Each pass parses the file and walks every object, about 190 ms on
a 15 MB .ai.

- vectorcraft_pdf::open_pdf reads and checks the file once, on the
  import's deep-stack thread, and import_opened imports pages from that
  reading. document.open takes the carried document, the page count,
  the editor's copy and the File Info from it. The editor's copy is read
  after the page import and only when the file is not restored from a
  carried document; so is the File Info.
- A .ai saved without PDF compatibility opened with pages that name
  every page reads its editor's copy from the same reading.
- document.pdfInfo reads the file once for its pages and the thumbnail.
- The Open and Place dialogs read the file once to decide whether to
  ask for pages, instead of twice.

A file whose PDF part fails the check still reads its editing data
separately, as before.
This commit is contained in:
dotCooCoo
2026-11-04 15:41:11 +07:00
committed by bflatastic
parent a8c6d078e5
commit a176ffae46
10 changed files with 237 additions and 73 deletions
+2
View File
@@ -61,6 +61,8 @@ vectorcraft-testkit = { workspace = true }
proptest = { workspace = true }
# A variable font built in code (no font file is committed).
vectorcraft-text = { workspace = true, features = ["test-fonts"] }
# How many times an open reads a PDF.
vectorcraft-pdf = { workspace = true, features = ["read-count"] }
wat = { workspace = true }
# Zstandard streams for the Illustrator editing data fuzz tests.
ruzstd = { workspace = true }
+68 -33
View File
@@ -165,14 +165,18 @@ pub fn load(name: &str, bytes: &[u8]) -> Result<Loaded> {
/// of its art, which it carries: its layers and hidden objects as they were, and the art outside
/// its artboard (its PDF part has only what is on the artboards), as an EPS of the editor is read.
/// Anything else as it was. `unit`: the size in points of a unit of the PDF part's user space (its
/// first page's `UserUnit`) → (document, warnings, whether its layers were read).
fn through_editing_data(bytes: &[u8], opts: &LoadOptions, doc: Document, warnings: Vec<String>, unit: f64) -> (Document, Vec<String>, bool) {
if opts.pages.is_some() || !opts.layers || !opts.editing_data {
return (doc, warnings, false);
}
let Some(private) = vectorcraft_pdf::illustrator_data(bytes, opts.password.as_deref()) else { return (doc, warnings, false) };
/// first page's `UserUnit`) → (document, warnings, whether its layers were read). `private`: the
/// editor's copy ([`editing_copy`]).
fn through_editing_data(
private: Option<&[u8]>,
opts: &LoadOptions,
doc: Document,
warnings: Vec<String>,
unit: f64,
) -> (Document, Vec<String>, bool) {
let Some(private) = private else { return (doc, warnings, false) };
let outlined = opts.text_as == vectorcraft_pdf::TextAs::Outlines;
let (doc, mut warnings, read) = vectorcraft_eps::layered_ai(&private, doc, warnings, outlined, opts.trust_editing_data, unit);
let (doc, mut warnings, read) = vectorcraft_eps::layered_ai(private, doc, warnings, outlined, opts.trust_editing_data, unit);
// The layers weren't read (or couldn't be): the notes of reading the PDF part still hold.
if !read {
return (doc, warnings, false);
@@ -189,18 +193,33 @@ fn through_editing_data(bytes: &[u8], opts: &LoadOptions, doc: Document, warning
(doc, warnings, true)
}
/// A `.ai` saved without PDF compatibility (its PDF part is a placeholder page) from the editor's
/// own copy of its art alone: `None` when the file has none (or pages are picked, or layers or the
/// editing data are off), an error when it can't be read.
fn from_editing_data_alone(bytes: &[u8], opts: &LoadOptions) -> Option<Result<(Document, Vec<String>)>> {
if opts.pages.is_some() || !opts.layers || !opts.editing_data {
/// A `.ai` saved without PDF compatibility (its PDF part is a placeholder page), `opened`, from the
/// editor's own copy of its art alone: `None` when the file has none (or the pages picked don't
/// name every page, or layers or the editing data are off), an error when it can't be read.
fn from_editing_data_alone(opened: &vectorcraft_pdf::Opened, opts: &LoadOptions) -> Option<Result<(Document, Vec<String>)>> {
if !every_page(opened.page_count(), opts) || !opts.layers || !opts.editing_data {
return None;
}
let private = vectorcraft_pdf::illustrator_data(bytes, opts.password.as_deref())?;
Some(
vectorcraft_eps::ai_alone(&private)
.map_err(|why| err(format!("this file was saved without PDF compatibility, and its editing data can't be read ({why})"))),
)
let private = opened.illustrator_data()?;
Some(editing_copy_alone(&private))
}
/// [`from_editing_data_alone`] of `private`, the editor's copy already read.
fn editing_copy_alone(private: &[u8]) -> Result<(Document, Vec<String>)> {
vectorcraft_eps::ai_alone(private)
.map_err(|why| err(format!("this file was saved without PDF compatibility, and its editing data can't be read ({why})")))
}
/// Does a file opened with `opts` read the editor's own copy of its art: no pages picked, and
/// layers and the editing data on?
fn reads_editing_copy(opts: &LoadOptions) -> bool {
opts.pages.is_none() && opts.layers && opts.editing_data
}
/// The editor's own copy of its art that `opened` carries (as [`vectorcraft_pdf::illustrator_data`]
/// reads it), when `opts` read it ([`reads_editing_copy`]).
fn editing_copy(opened: &vectorcraft_pdf::Opened, opts: &LoadOptions) -> Option<Vec<u8>> {
reads_editing_copy(opts).then(|| opened.illustrator_data()).flatten()
}
/// A `.ai`, or a PDF saved with the editing data, whose PDF part can't be read (`e`, the import's
@@ -266,10 +285,10 @@ pub(crate) fn is_placeholder(e: &EngineError) -> bool {
e.to_string() == vectorcraft_pdf::PdfError::PlaceholderOnly.to_string()
}
/// Do `opts` read every page of the PDF `bytes`: no `pages`, or a range that names each one?
fn every_page(bytes: &[u8], opts: &LoadOptions) -> bool {
/// Do `opts` read every page of a PDF of `count` pages: no `pages`, or a range that names each one?
fn every_page(count: usize, opts: &LoadOptions) -> bool {
let Some(range) = &opts.pages else { return true };
vectorcraft_pdf::info(bytes, opts.password.as_deref()).is_ok_and(|i| parse_range(range, i.pages.len()).is_ok_and(|p| p.len() == i.pages.len()))
parse_range(range, count).is_ok_and(|p| p.len() == count)
}
/// Page `page_index` (0-based) read from a PDF or `.ai` (`read`, as [`super::page_document`] reads
@@ -306,6 +325,8 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
None => err(format!("can't open `{name}`: not a format VectorCraft reads (see document.formats)")),
})?;
let title = file_name(name);
// The File Info a PDF, `.ai` or `.ait` gives.
let mut file_info = None;
let mut converted = false;
let mut preview_only = false;
let mut editing_data = false;
@@ -332,13 +353,22 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
restore_or_import(editing, || import_svg(&text, folder.as_deref()))?
}
"pdf" | "ai" | "ait" => {
// The file is read and checked once, for the document it carries, its pages, the
// editor's copy and its File Info.
let opened = vectorcraft_pdf::open_pdf(bytes, opts.password.as_deref());
// Saved with Preserve Editing: the document it carries, unless some pages are picked.
let editing = (opts.pages.is_none() && opts.native)
.then(|| vectorcraft_pdf::editing_with(bytes, opts.password.as_deref()))
.then(|| opened.as_ref().ok().and_then(|o| o.editing()))
.flatten()
.map(|e| (e.intact, move || Some(e.data)));
restore_or_import(editing, || match super::pdfimport::import(bytes, opts, format.id != "pdf") {
let imported = || match &opened {
Ok(o) => super::pdfimport::import(bytes, o, opts, format.id != "pdf"),
Err(e) => Err(err(e)),
};
let read = restore_or_import(editing, || match imported() {
Ok(r) => {
// The editor's copy, read once the import is done with the pages.
let private = opened.as_ref().ok().and_then(|o| editing_copy(o, opts));
// Pages of text alone: the editing copy alone where it reads, as for a file
// saved without its PDF part (#1387: else the page, which is the art). So too
// pages that draw nothing at all: there is no page art to check the layers
@@ -347,30 +377,35 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
// decoded draws nothing either): its notes are at most the page's own.
let empty = draws_nothing(&r.document) && r.warnings.iter().all(|w| is_page_note(w));
let alone = (r.only_text || empty)
.then(|| from_editing_data_alone(bytes, opts))
.then(|| private.as_deref().map(editing_copy_alone))
.flatten()
.and_then(|read| read.ok())
.map(|(doc, warnings)| outlined_if_asked(doc, warnings, opts));
let (doc, warnings, read) = match alone {
Some((doc, warnings)) => (doc, warnings, true),
None => through_editing_data(bytes, opts, r.document, r.warnings, r.user_unit),
None => through_editing_data(private.as_deref(), opts, r.document, r.warnings, r.user_unit),
};
editing_data = read;
Ok((doc, warnings))
}
// Saved without its PDF part: the editing copy is all the file has (`pages` that
// name every page read it as the whole file).
Err(e) if is_placeholder(&e) => {
let whole = LoadOptions { pages: None, ..opts.clone() };
from_editing_data_alone(bytes, if every_page(bytes, opts) { &whole } else { opts })
.unwrap_or(Err(e))
.map(|(doc, warnings)| outlined_if_asked(doc, warnings, opts))
.inspect(|_| editing_data = true)
}
Err(e) if is_placeholder(&e) => opened
.as_ref()
.ok()
.and_then(|o| from_editing_data_alone(o, opts))
.unwrap_or(Err(e))
.map(|(doc, warnings)| outlined_if_asked(doc, warnings, opts))
.inspect(|_| editing_data = true),
// A PDF part that can't be read (a stream the import refuses): the editing copy
// alone where it reads, else the error.
Err(e) => without_pdf_part(bytes, opts, &e).inspect(|_| editing_data = true).ok_or(e),
})?
})?;
// The File Info of a file read from its pages or its editing copy.
if !read.2 {
file_info = opened.as_ref().ok().map(|o| o.file_info());
}
read
}
"dxf" => {
let (doc, warnings) = super::dxfimport::import(bytes, &opts.dxf)?;
@@ -392,7 +427,7 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
// it carries, opens with the File Info it gives (a PostScript `.ai` gives none).
if matches!(format.id, "pdf" | "ai" | "ait")
&& !restored
&& let Some(info) = vectorcraft_pdf::file_info(bytes, opts.password.as_deref())
&& let Some(info) = file_info
{
super::super::fileinfo::read_pdf(&mut doc.metadata, info);
}
+29 -21
View File
@@ -108,15 +108,12 @@ impl LoadOptions {
/// The PDF import options: `pages` resolved against the file's page count. A PDF-compatible
/// `.ai`, or a PDF saved with its editing data, asked for its crop box opens on its trim boxes,
/// its editor's artboards, with the bleed round them (its crop box takes the bleed in).
fn import_options(&self, bytes: &[u8], editor_document: bool) -> Result<ImportOptions> {
fn import_options(&self, opened: &vectorcraft_pdf::Opened, editor_document: bool) -> Result<ImportOptions> {
let pages = match &self.pages {
None => None,
Some(range) => {
let count = vectorcraft_pdf::info(bytes, self.password.as_deref()).map_err(err)?.pages.len();
Some(parse_range(range, count).map_err(|e| err(format!("pages: {e}")))?)
}
Some(range) => Some(parse_range(range, opened.page_count()).map_err(|e| err(format!("pages: {e}")))?),
};
let is_ai = self.crop == CropTo::Crop && vectorcraft_pdf::has_illustrator_data(bytes, self.password.as_deref());
let is_ai = self.crop == CropTo::Crop && opened.has_illustrator_data();
Ok(ImportOptions {
pages,
crop: if is_ai { CropTo::Trim } else { self.crop },
@@ -140,24 +137,33 @@ fn choice<T: Choice + Default>(cmd: &str, key: &str, id: Option<&str>) -> Result
.ok_or_else(|| bad(cmd, format!("{key} must be one of {}", T::IDS.join(", "))))
}
/// Import the pages `o` pick → the document, import notes and what else the import tells.
/// Import the pages `o` pick of `opened`, the PDF `bytes` already read
/// ([`vectorcraft_pdf::open_pdf`]) → the document, import notes and what else the import tells.
/// `editor_document`: a `.ai` file.
pub(super) fn import(bytes: &[u8], o: &LoadOptions, editor_document: bool) -> Result<vectorcraft_pdf::ImportReport> {
vectorcraft_pdf::import_with_report(bytes, &o.import_options(bytes, editor_document)?).map_err(err)
pub(super) fn import(
bytes: &[u8],
opened: &vectorcraft_pdf::Opened,
o: &LoadOptions,
editor_document: bool,
) -> Result<vectorcraft_pdf::ImportReport> {
let options = o.import_options(opened, editor_document)?;
vectorcraft_pdf::import_opened(bytes, opened, &options).map_err(err)
}
/// Page `page` (0-based) of a PDF as a document of one artboard (its `o.crop` box) and one
/// layer of what it shows, with the same text choice as Open, and the import notes.
pub fn page_document(bytes: &[u8], page: usize, o: &LoadOptions) -> Result<(Document, Vec<String>)> {
let opts = ImportOptions {
pages: Some(vec![page]),
crop: o.crop,
password: o.password.clone(),
text_as: o.text_as,
layers: false,
..Default::default()
};
vectorcraft_pdf::import_with_report(bytes, &opts).map(|r| (r.document, r.warnings)).map_err(err)
vectorcraft_pdf::import_with_report(bytes, &page_options(page, o)).map(|r| (r.document, r.warnings)).map_err(err)
}
/// [`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>)> {
vectorcraft_pdf::import_opened(bytes, opened, &page_options(page, o)).map(|r| (r.document, r.warnings)).map_err(err)
}
/// The import options of [`page_document`].
fn page_options(page: usize, o: &LoadOptions) -> ImportOptions {
ImportOptions { pages: Some(vec![page]), crop: o.crop, password: o.password.clone(), text_as: o.text_as, layers: false, ..Default::default() }
}
const INFO: &str = "document.pdfInfo";
@@ -170,13 +176,15 @@ pub(super) fn pdf_info(_: &mut Session, p: &Value) -> Result<Value> {
if bytes.starts_with(b"%!") || vectorcraft_pdf::is_postscript(&bytes) {
return postscript_info(&bytes, p);
}
let info = match vectorcraft_pdf::info(&bytes, o.password.as_deref()) {
Ok(i) => i,
// The file is read once, for its pages and the thumbnail.
let opened = match vectorcraft_pdf::open_pdf(&bytes, o.password.as_deref()) {
Ok(opened) => opened,
Err(e @ (PdfError::NeedsPassword | PdfError::WrongPassword)) => {
return Ok(json!({"pages": 0, "needsPassword": true, "wrongPassword": e == PdfError::WrongPassword, "pageInfo": []}));
}
Err(e) => return Err(err(e)),
};
let info = opened.info();
let r4 = |r: vectorcraft_geom::Rect| json!([r.x0, r.y0, r.x1, r.y1]);
let pages: Vec<Value> = info
.pages
@@ -188,7 +196,7 @@ pub(super) fn pdf_info(_: &mut Session, p: &Value) -> Result<Value> {
.collect();
let mut out = json!({"pages": pages.len(), "needsPassword": false, "pageInfo": pages});
if let Some(page) = thumbnail_page(p, info.pages.len(), "PDF")? {
let (doc, _) = page_document(&bytes, page, &o)?;
let (doc, _) = opened_page_document(&bytes, &opened, page, &o)?;
out["thumbnail"] = json!(thumbnail(&doc, p)?);
}
Ok(out)
@@ -82,6 +82,25 @@ fn an_illustrator_ai_file_opens_from_its_editing_data() {
assert_ne!(names(&s.doc().unwrap().doc.layers), ["Art", "Hidden"]);
}
/// Opening a `.ai` or a PDF whose PDF part reads, or a `.ai` saved without PDF compatibility,
/// reads and checks the file once, on any thread, for the document it may carry, its pages, its
/// editing data and its File Info, with pages picked or not.
#[test]
fn an_ai_or_pdf_file_is_read_once_when_it_opens() {
let ai_file = ai::ai(&compressed(&ai::sample_data()), ai::page_pdf());
let pdf_file = pdf_with(&[PdfPage::new(100.0, 100.0, "0 0 1 rg 10 10 50 50 re f")], &[], None);
let placeholder = ai::ai(&compressed(&ai::sample_data()), PLACEHOLDER);
for (name, file) in [("art.ai", &ai_file), ("art.pdf", &pdf_file), ("placeholder.ai", &placeholder)] {
for opts in [LoadOptions::default(), LoadOptions { pages: Some("1".into()), ..Default::default() }] {
let before = vectorcraft_pdf::reads_of(file);
let loaded = load_with(name, file, &opts).unwrap();
assert_eq!(vectorcraft_pdf::reads_of(file) - before, 1, "{name}, pages {:?}", opts.pages);
let editing_data = name == "placeholder.ai" || (name == "art.ai" && opts.pages.is_none());
assert_eq!(loaded.editing_data, editing_data, "{name}, pages {:?}", opts.pages);
}
}
}
#[test]
fn an_ai_file_opens_with_its_artboard_options_raster_settings_and_author() {
let document = "%_/Document :\n%_/Dictionary :\n%_/Dictionary :\n%_150 /Int (dpi.) ,\n%_1 /Bool (mask) ,\n%_; (AI Auto Rasterize) ,\n\
+4
View File
@@ -8,6 +8,10 @@ description = "VectorCraft PDF export and import"
[lints]
workspace = true
[features]
# `vectorcraft_pdf::reads_of` for other crates' tests (dev-dependencies only).
read-count = []
[dependencies]
vectorcraft-geom = { workspace = true }
vectorcraft-doc = { workspace = true }
+1 -1
View File
@@ -40,7 +40,7 @@ mod state;
pub(crate) use cid::{CidText, font_chars, identify_glyph, unify_radical};
pub use entry::{
INVISIBLE_TEXT_NOTE, OFF_ARTBOARD_NOTE, OUTLINED_TEXT_NOTE, PAST_PAGE_NOTE, UNMATCHED_GLYPHS_NOTE, UNNAMED_CHARACTERS_NOTE, holds_outlines,
import, import_with_report, is_glyph_outlines, is_outlines_note,
import, import_opened, import_with_report, is_glyph_outlines, is_outlines_note,
};
#[cfg(test)]
pub(crate) use entry::{TEST_MAX_OBJECTS, import_with_reader_options, read};
+27 -10
View File
@@ -200,6 +200,12 @@ pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportRe
vectorcraft_doc::depth::with_deep_stack(|| read(bytes, opts))
}
/// [`import_with_report`] of `opened`, the PDF `bytes` already read and checked
/// ([`crate::open_pdf`] with `opts`' password), so the file isn't read again.
pub fn import_opened(bytes: &[u8], opened: &crate::Opened, opts: &ImportOptions) -> Result<ImportReport, PdfError> {
vectorcraft_doc::depth::with_deep_stack(|| read_opened(bytes, opts, &crate::pages::open_options(), &opened.pdf))
}
/// [`import_with_report`] on the thread that asks.
pub(crate) fn read(bytes: &[u8], opts: &ImportOptions) -> Result<ImportReport, PdfError> {
read_with_reader_options(bytes, opts, &crate::pages::open_options())
@@ -212,10 +218,21 @@ pub(crate) fn import_with_reader_options(bytes: &[u8], opts: &ImportOptions, rea
fn read_with_reader_options(bytes: &[u8], opts: &ImportOptions, reader: &craft_pdf::OpenOptions) -> Result<ImportReport, PdfError> {
let original = crate::pages::open_with_options(bytes, opts.password.as_deref(), reader)?;
read_opened(bytes, opts, reader, &original)
}
/// [`read_with_reader_options`] of `original`, the PDF `bytes` read with `reader`.
fn read_opened(
bytes: &[u8],
opts: &ImportOptions,
reader: &craft_pdf::OpenOptions,
original: &craft_pdf::syntax::Pdf,
) -> Result<ImportReport, PdfError> {
let picked = crate::pages::picked(opts, original.pages().len())?;
let mut ocgs = Ocgs::read(&original);
let mut ocgs = Ocgs::read(original);
let route = opts.layers && !ocgs.list.is_empty();
let (pdf, all_on, notes) = reopen_with_groups_on(bytes, opts, reader, original, &picked, &mut ocgs, route)?;
let (turned_on, all_on, notes) = reopen_with_groups_on(bytes, opts, reader, original, &picked, &mut ocgs, route)?;
let pdf = turned_on.as_ref().unwrap_or(original);
let pages = pdf.pages();
let sink: Arc<Mutex<Vec<String>>> = Arc::default();
@@ -227,7 +244,7 @@ fn read_with_reader_options(bytes: &[u8], opts: &ImportOptions, reader: &craft_p
doc.artboards.clear();
doc.layers.clear();
let taken: Vec<String> = doc.swatches_iter().map(|s| s.name.clone()).chain(doc.swatch_groups.iter().map(|g| g.name.clone())).collect();
let mut b = Builder::new(doc.peek_next_id(), Colors::new(&pdf, taken.clone()), opts.text_as, route);
let mut b = Builder::new(doc.peek_next_id(), Colors::new(pdf, taken.clone()), opts.text_as, route);
// The art of a group that is off (drawn as `all_on` turned it on) or doesn't print mustn't
// show or print on its page's layer.
b.unsure = route && ocgs.list.iter().any(|g| (all_on && !g.on) || !g.print);
@@ -440,7 +457,7 @@ fn read_with_reader_options(bytes: &[u8], opts: &ImportOptions, reader: &craft_p
}
warnings.extend(depth_notes);
let user_unit = picked.first().and_then(|&n| pages.get(n)).map_or(1.0, |page| f64::from(page.user_unit()));
Ok(ImportReport { document: doc, warnings, native: crate::editing::editing_in(&pdf), only_text, user_unit })
Ok(ImportReport { document: doc, warnings, native: crate::editing::editing_in(pdf), only_text, user_unit })
}
/// The page a PDF-compatible `.ai` paints under its layers is the editor's page, not art: an opaque
@@ -487,17 +504,17 @@ fn is_only_text(nodes: &[Arc<Node>]) -> bool {
}
/// Opens the file again with every layer group turned on when the pages hide art in groups that are
/// off, and tells whether it did. Gives the file to read, whether it is the turned-on copy, and the
/// notes for the import's report.
/// off, and tells whether it did. Gives the turned-on copy (`None`: read `original`), whether there
/// is one, and the notes for the import's report.
fn reopen_with_groups_on(
bytes: &[u8],
opts: &ImportOptions,
reader: &craft_pdf::OpenOptions,
original: craft_pdf::syntax::Pdf,
original: &craft_pdf::syntax::Pdf,
picked: &[usize],
ocgs: &mut Ocgs,
route: bool,
) -> Result<(craft_pdf::syntax::Pdf, bool, Vec<String>), PdfError> {
) -> Result<(Option<craft_pdf::syntax::Pdf>, bool, Vec<String>), PdfError> {
// Content that is off imports as hidden layers: an update of the file turns every group on
// (unless a form is hidden by its own group: its art couldn't be told apart).
let hide = route && ocgs.skips_any();
@@ -511,7 +528,7 @@ fn reopen_with_groups_on(
}
}
let turned_on = (hide && !hidden_form)
.then(|| all_on(bytes, &original))
.then(|| all_on(bytes, original))
.flatten()
.and_then(|b| crate::pages::open_with_options(&b, opts.password.as_deref(), reader).ok())
.filter(|p| Ocgs::read(p).list.is_empty());
@@ -520,7 +537,7 @@ fn reopen_with_groups_on(
notes.push("the art of hidden layers couldn't be read and was left out".to_string());
}
let all_on = turned_on.is_some();
Ok((turned_on.unwrap_or(original), all_on, notes))
Ok((turned_on, all_on, notes))
}
/// The interpreter's settings, its warnings noted once each in `sink` (truncated content in `incomplete`).
+7 -2
View File
@@ -78,9 +78,14 @@ pub use export::{export, export_with_report, page_areas};
pub use file_info::{FileInfo, file_info};
pub use import::{
INVISIBLE_TEXT_NOTE, OFF_ARTBOARD_NOTE, OUTLINED_TEXT_NOTE, PAST_PAGE_NOTE, UNMATCHED_GLYPHS_NOTE, UNNAMED_CHARACTERS_NOTE, holds_outlines,
import, import_with_report, is_glyph_outlines, is_outlines_note,
import, import_opened, import_with_report, is_glyph_outlines, is_outlines_note,
};
#[cfg(any(test, feature = "read-count"))]
pub use pages::reads_of;
pub use pages::{
IllustratorData, Opened, PageInfo, PdfInfo, has_illustrator_data, illustrator_data, illustrator_data_without_pdf_part, info, is_postscript,
open_pdf,
};
pub use pages::{IllustratorData, PageInfo, PdfInfo, has_illustrator_data, illustrator_data, illustrator_data_without_pdf_part, info, is_postscript};
pub use piece_info::{PRIVATE_CHUNK, PrivateData};
pub use post::{THUMBNAIL_SIZE, Thumbnail};
pub use presets::*;
+75 -3
View File
@@ -234,6 +234,54 @@ pub(crate) fn open(bytes: &[u8], password: Option<&str>) -> Result<Pdf, PdfError
open_with_options(bytes, password, &open_options())
}
/// A PDF read and checked once ([`open_pdf`]), for reading several things from one file: the
/// document it carries ([`Opened::editing`]), an editor's private data ([`Opened::illustrator_data`]),
/// its File Info ([`Opened::file_info`]), its pages and boxes ([`Opened::info`]) and its art
/// ([`crate::import_opened`]). Each of [`editing_with`](crate::editing_with), [`illustrator_data`],
/// [`crate::file_info`], [`info`] and [`crate::import_with_report`] reads and checks the whole file
/// again.
pub struct Opened {
pub(crate) pdf: Pdf,
}
/// The PDF `bytes`, read and checked as [`crate::import_with_report`] reads it (with `password`, as
/// [`info`] takes it, and on a thread with the import's stack), to read several things from.
pub fn open_pdf(bytes: &[u8], password: Option<&str>) -> Result<Opened, PdfError> {
vectorcraft_doc::depth::with_deep_stack(|| open(bytes, password)).map(|pdf| Opened { pdf })
}
impl Opened {
/// The pages and their boxes, as [`info`] lists them.
pub fn info(&self) -> PdfInfo {
info_in(&self.pdf)
}
/// The native document the PDF carries, as [`editing`](crate::editing) reads it.
pub fn editing(&self) -> Option<crate::Editing> {
crate::editing::editing_in(&self.pdf)
}
/// The private data of an Illustrator `.ai`, as [`illustrator_data`] reads it.
pub fn illustrator_data(&self) -> Option<Vec<u8>> {
private_data(&self.pdf)
}
/// Does the first page carry an editor's copy of the art, as [`has_illustrator_data`] says?
pub fn has_illustrator_data(&self) -> bool {
has_private_data_keys(&self.pdf)
}
/// The File Info, as [`crate::file_info`] reads it.
pub fn file_info(&self) -> crate::FileInfo {
crate::file_info::file_info_in(&self.pdf)
}
/// How many pages the PDF has, as [`info`] lists them.
pub fn page_count(&self) -> usize {
self.pdf.pages().len()
}
}
/// Preserve the application's existing accepted file sizes. Ordinary stream decoding still
/// uses the shared reader's immutable per-document limits, and its input copy is fallible.
pub(crate) fn open_options() -> craft_pdf::OpenOptions {
@@ -249,10 +297,25 @@ pub(crate) fn open_with_options(bytes: &[u8], password: Option<&str>, options: &
})
}
/// How many times each buffer has been read as a PDF, by its address (tests).
#[cfg(any(test, feature = "read-count"))]
static READS: std::sync::Mutex<std::collections::BTreeMap<usize, usize>> = std::sync::Mutex::new(std::collections::BTreeMap::new());
/// How many times `bytes`, at its address, has been read as a PDF on any thread (tests).
#[cfg(any(test, feature = "read-count"))]
pub fn reads_of(bytes: &[u8]) -> usize {
READS.lock().ok().and_then(|r| r.get(&(bytes.as_ptr() as usize)).copied()).unwrap_or(0)
}
/// [`open_with_options`] without looking at the file's objects ([`hostile`]): the reader builds its
/// table of objects and its page tree, and nothing on a page is read. For reading one object that
/// is checked on its own ([`illustrator_data_without_pdf_part`]), never for importing a page.
fn open_structural_with_options(bytes: &[u8], password: Option<&str>, options: &craft_pdf::OpenOptions) -> Result<Pdf, PdfError> {
#[cfg(any(test, feature = "read-count"))]
if let Ok(mut reads) = READS.lock() {
let n = reads.entry(bytes.as_ptr() as usize).or_default();
*n = n.saturating_add(1);
}
if is_postscript(bytes) {
return Err(PdfError::PostScript);
}
@@ -282,7 +345,11 @@ fn open_structural_with_options(bytes: &[u8], password: Option<&str>, options: &
/// The pages and their boxes. An encrypted PDF needs its `password`
/// ([`PdfError::NeedsPassword`] / [`PdfError::WrongPassword`]).
pub fn info(bytes: &[u8], password: Option<&str>) -> Result<PdfInfo, PdfError> {
let pdf = open(bytes, password)?;
Ok(info_in(&open(bytes, password)?))
}
/// [`info`] of a parsed PDF.
fn info_in(pdf: &Pdf) -> PdfInfo {
let pages = pdf
.pages()
.iter()
@@ -299,7 +366,7 @@ pub fn info(bytes: &[u8], password: Option<&str>) -> Result<PdfInfo, PdfError> {
PageInfo { width: w as f64, height: h as f64, rotation, boxes }
})
.collect();
Ok(PdfInfo { pages })
PdfInfo { pages }
}
/// The 0-based pages `opts` pick out of `count`.
@@ -419,7 +486,12 @@ fn private_data(pdf: &Pdf) -> Option<Vec<u8>> {
/// (`AIPrivateData` keys) or a PDF saved with its editing data (`AIPDFPrivateData`, #1345)? Without
/// reading the data. Either's trim boxes are its editor's artboards.
pub fn has_illustrator_data(bytes: &[u8], password: Option<&str>) -> bool {
open(bytes, password).is_ok_and(|pdf| pdf.pages().first().is_some_and(|p| piece_info_has(p, &[b"AIPrivateData", b"AIPDFPrivateData"])))
open(bytes, password).is_ok_and(|pdf| has_private_data_keys(&pdf))
}
/// [`has_illustrator_data`] of a parsed PDF.
fn has_private_data_keys(pdf: &Pdf) -> bool {
pdf.pages().first().is_some_and(|p| piece_info_has(p, &[b"AIPrivateData", b"AIPDFPrivateData"]))
}
/// `stream` decoded, up to `most` bytes: the decoder is run here rather than by the PDF reader's
+5 -3
View File
@@ -122,11 +122,13 @@ pub fn offer(app: &mut VectorcraftApp, name: &str, bytes: &[u8], path: Option<St
// A file carrying its VectorCraft document (a saved .ai, or a PDF saved with Preserve Editing)
// opens as that document, every artboard and layer: there are no pages to pick. Placing still
// asks, since a place takes one page.
if place.is_none() && vectorcraft_pdf::editing_with(bytes, None).is_some() {
// The file is read once for both checks.
let opened = vectorcraft_pdf::open_pdf(bytes, None);
if place.is_none() && opened.as_ref().is_ok_and(|o| o.editing().is_some()) {
return false;
}
let (pages, locked) = match vectorcraft_pdf::info(bytes, None) {
Ok(i) if i.pages.len() > 1 => (i.pages.len(), false),
let (pages, locked) = match opened.map(|o| o.page_count()) {
Ok(count) if count > 1 => (count, false),
Err(PdfError::NeedsPassword) => (0, true),
_ => return false,
};