mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 01:20:59 +00:00
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:
@@ -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 }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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\
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user