Merge pull request #5552 from dotCooCoo/fix/pdf-open-reads-once

PDF/.ai import: the file is read and checked once when it opens
This commit is contained in:
BFlat
2026-11-04 16:04:48 +07:00
committed by GitHub
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,
};