.ai import: a file whose PDF part can't be read opens from its editing data

Squashed from #5051 onto main.
This commit is contained in:
dotCooCoo
2026-11-02 12:04:04 +07:00
committed by bflatastic
parent 54436b0aa2
commit 100730d4d3
43 changed files with 476 additions and 42 deletions
+1 -1
View File
@@ -78,7 +78,7 @@ 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,
};
pub use pages::{PageInfo, PdfInfo, has_illustrator_data, illustrator_data, info, is_postscript};
pub use pages::{IllustratorData, PageInfo, PdfInfo, has_illustrator_data, illustrator_data, illustrator_data_without_pdf_part, info, is_postscript};
pub use post::{THUMBNAIL_SIZE, Thumbnail};
pub use presets::*;
pub use print::*;
+67 -13
View File
@@ -241,6 +241,18 @@ pub(crate) fn open_options() -> craft_pdf::OpenOptions {
}
pub(crate) fn open_with_options(bytes: &[u8], password: Option<&str>, options: &craft_pdf::OpenOptions) -> Result<Pdf, PdfError> {
// Twice: looking at every object makes the reader repair a damaged table of objects (once
// only), and the objects are other ones after that.
open_structural_with_options(bytes, password, options).and_then(|pdf| match (0..2).find_map(|_| hostile(&pdf)) {
Some(why) => Err(PdfError::Parse(why)),
None => Ok(pdf),
})
}
/// [`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> {
if is_postscript(bytes) {
return Err(PdfError::PostScript);
}
@@ -265,12 +277,6 @@ pub(crate) fn open_with_options(bytes: &[u8], password: Option<&str>, options: &
craft_pdf::OpenError::Load(LoadPdfError::Decryption(DecryptionError::PasswordProtected)) => PdfError::WrongPassword,
e => PdfError::Parse(e.to_string()),
})
// Twice: looking at every object makes the reader repair a damaged table of objects (once
// only), and the objects are other ones after that.
.and_then(|pdf| match (0..2).find_map(|_| hostile(&pdf)) {
Some(why) => Err(PdfError::Parse(why)),
None => Ok(pdf),
})
}
/// The pages and their boxes. An encrypted PDF needs its `password`
@@ -367,7 +373,37 @@ const MAX_PRIVATE: usize = 256 << 20;
/// `AIPrivateData1`. `None` for a PDF without either (or with a stream that can't be read).
/// What it holds is the editor's own copy of the art (see `vectorcraft_eps::layered_ai`).
pub fn illustrator_data(bytes: &[u8], password: Option<&str>) -> Option<Vec<u8>> {
let pdf = open(bytes, password).ok()?;
private_data(&open(bytes, password).ok()?)
}
/// The private data of a file whose PDF part can't be read ([`illustrator_data_without_pdf_part`]),
/// with the size of its first page.
#[derive(Clone, Debug, PartialEq)]
pub struct IllustratorData {
/// The private data, as [`illustrator_data`] reads it.
pub data: Vec<u8>,
/// The size in points of a unit of the first page's user space (its `UserUnit`).
pub user_unit: f64,
/// The first page's trim box (its crop box when it has none) in points, rotated as the page
/// shows: width and height.
pub trim: (f64, f64),
}
/// [`illustrator_data`] for a file whose PDF part can't be read (a stream the import refuses, or
/// objects built on one another without end): the file is opened without looking at its other
/// objects, and only the private data streams are read, with the same limits. `None` when the
/// file itself can't be opened, or as for [`illustrator_data`].
pub fn illustrator_data_without_pdf_part(bytes: &[u8], password: Option<&str>) -> Option<IllustratorData> {
let pdf = open_structural_with_options(bytes, password, &open_options()).ok()?;
let data = private_data(&pdf)?;
let page = pdf.pages().first()?;
// (The page transform is in points: it applies the `UserUnit`.)
let trim = frame(page, CropTo::Trim).1;
Some(IllustratorData { data, user_unit: f64::from(page.user_unit()), trim: (trim.width(), trim.height()) })
}
/// The private data of `pdf`'s first page, as [`illustrator_data`] reads it.
fn private_data(pdf: &Pdf) -> Option<Vec<u8>> {
let page = pdf.pages().first()?;
let private = page.raw().get::<Dict<'_>>(b"PieceInfo")?.get::<Dict<'_>>(PRIVATE_DATA_OWNER)?.get::<Dict<'_>>(b"Private")?;
let key = if private.contains_key(b"AIPrivateData1") { "AIPrivateData" } else { "AIPDFPrivateData" };
@@ -388,19 +424,37 @@ pub fn has_illustrator_data(bytes: &[u8], password: Option<&str>) -> bool {
/// `stream` decoded, up to `most` bytes: the decoder is run here rather than by the PDF reader's
/// `decoded()`, which has no limit, so a stream of zeros can't grow to the size of the memory. Only a
/// stream that is plain or only deflated is read (the private data is no other); else, or when it
/// is larger once decoded, `None`.
/// stream that is plain or only deflated is read (the private data is no other), without decode
/// parameters; else, or when it is larger once decoded, `None`. A filter the reader doesn't know
/// counts as another filter.
fn inflated(stream: &Stream<'_>, most: usize) -> Option<Vec<u8>> {
use std::io::Read;
if stream.dict().get::<Dict<'_>>(b"DecodeParms").is_some() {
let dict = stream.dict();
let named = |key: &[u8]| match dict.get::<Object<'_>>(key) {
None | Some(Object::Null(_)) => Some(0),
Some(Object::Name(_)) => Some(1),
Some(Object::Array(a)) => Some(items(&a).count()),
Some(_) => None,
};
let params = |key: &[u8]| match dict.get::<Object<'_>>(key) {
None | Some(Object::Null(_)) => false,
Some(Object::Array(a)) => items(&a).any(|m| !matches!(m, MaybeRef::NotRef(Object::Null(_)))),
Some(_) => true,
};
if params(b"DecodeParms") || params(b"DP") {
return None;
}
let filters = stream.filters();
// (The reader leaves out the filters it doesn't know.)
if named(b"Filter")?.max(named(b"F")?) != filters.len() {
return None;
}
let raw = stream.raw_data();
let mut out = vec![];
let limit = u64::try_from(most).ok()?.checked_add(1)?;
match stream.filters().len() {
0 => out.extend_from_slice(&raw),
1 => {
match filters.as_slice() {
[] if raw.len() <= most => out.extend_from_slice(&raw),
[craft_pdf::syntax::Filter::FlateDecode] => {
flate2::read::ZlibDecoder::new(&*raw).take(limit).read_to_end(&mut out).ok()?;
}
_ => return None,
+32
View File
@@ -416,3 +416,35 @@ fn a_pdfs_private_data_is_its_pdf_streams_joined() {
let both = format!("/AIPDFPrivateData1 {a} 0 R /AIPDFPrivateData2 {b} 0 R /AIPrivateData1 {c} 0 R");
assert_eq!(illustrator_data(&pdf_with_catalog(&[page(both)], &[&one, &two, &other], "", None), None).unwrap(), b"%AI24_ZStandard_Data other");
}
/// The private data of a file whose other objects the import refuses (#4366) is read without its
/// PDF part, and only then; a private data stream under a filter the reader doesn't know, or with
/// decode parameters, is read neither way.
#[test]
fn private_data_reads_without_a_pdf_part_that_cant_be_read() {
let (a, b) = (first_extra(1), first_extra(1) + 1);
let page = PdfPage {
entries: format!("/PieceInfo << /Illustrator << /Private << /AIPrivateData1 {a} 0 R /AIPrivateData2 {b} 0 R /NumBlock 2 >> >> >>"),
..PdfPage::new(100.0, 100.0, "1 0 0 rg 10 10 30 30 re f")
};
let (one, two) = (stream("", "%AI24_ZStandard_Data first "), stream("", "second"));
let refused = stream("/DecodeParms << /Predictor 12 /Columns 0 >>", "x");
let pdf = pdf_with_catalog(std::slice::from_ref(&page), &[&one, &two, &refused], "", None);
assert!(illustrator_data(&pdf, None).is_none());
let read = illustrator_data_without_pdf_part(&pdf, None).unwrap();
assert_eq!((read.data.as_slice(), read.user_unit, read.trim), (&b"%AI24_ZStandard_Data first second"[..], 1.0, (100.0, 100.0)));
let whole = pdf_with_catalog(std::slice::from_ref(&page), &[&one, &two], "", None);
assert_eq!(illustrator_data_without_pdf_part(&whole, None).map(|p| p.data), illustrator_data(&whole, None));
// The trim box in points, as the page shows it.
let large = PdfPage { entries: format!("{} /UserUnit 10 /TrimBox [10 10 60 30] /Rotate 90", page.entries), ..page.clone() };
let read = illustrator_data_without_pdf_part(&pdf_with_catalog(&[large], &[&one, &two, &refused], "", None), None).unwrap();
assert_eq!((read.user_unit, read.trim), (10.0, (200.0, 500.0)));
assert!(illustrator_data_without_pdf_part(b"not a pdf", None).is_none());
for odd in ["/Filter /Bogus", "/Filter [/Bogus]", "/DP << /Predictor 2 >>", "/Filter /FlateDecode /DecodeParms [<< /Predictor 2 >>]", "/Filter 5"]
{
let first = stream(odd, "%AI24_ZStandard_Data first ");
let pdf = pdf_with_catalog(std::slice::from_ref(&page), &[&first, &two], "", None);
assert!(illustrator_data(&pdf, None).is_none(), "{odd}");
assert!(illustrator_data_without_pdf_part(&pdf, None).is_none(), "{odd}");
}
}