mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-06 02:21:45 +00:00
M6: field detection reads operators one at a time; the PDF/A check reads XMP as preflight does
From the Resources review of 66415d737:
- page_shapes collected every parsed operator, about 80 bytes for each
decoded byte of dense content, so a page within its 256 MiB allowance
could take some 20 GB. It now reads them one at a time
(pdfcraft_content::ops) and drops the pieces once joined.
- declares_pdfa decoded through a DecodeBudget, which counts every filter
stage against the 8 MiB cap, so XMP stored through a chain of filters
stopped declaring PDF/A while preflight's Standards panel, which caps
each stage, still read it. It reads the metadata as preflight does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
This commit is contained in:
1 parent
66415d7373
commit
c00e3d2fb4
3 files changed
+27
-9
No files matched your search
@@ -1,6 +1,6 @@
|
||||
use super::read::{acroform, fields};
|
||||
use super::values::merged_appearance;
|
||||
use super::{DecodeBudget, Dict, Document, FieldKind, ObjRef, Object, Widget, appearance, name_bytes};
|
||||
use super::{Dict, Document, FieldKind, ObjRef, Object, Widget, appearance, name_bytes};
|
||||
|
||||
/// Whether the form asks viewers to draw its fields' appearances themselves (`/NeedAppearances`).
|
||||
pub fn need_appearances(doc: &Document) -> bool {
|
||||
@@ -212,18 +212,21 @@ impl FieldAppearanceCache {
|
||||
}
|
||||
}
|
||||
|
||||
/// The most of the XMP metadata stream decoded to find a PDF/A declaration, as preflight's
|
||||
/// Standards panel reads it: past it, metadata declares nothing there either.
|
||||
/// The most each filter stage of the XMP metadata stream may decode for its PDF/A declaration to
|
||||
/// be read, as preflight's Standards panel reads it: past it, metadata declares nothing there
|
||||
/// either.
|
||||
pub(crate) const MAX_XMP_BYTES: usize = 8 << 20;
|
||||
|
||||
/// Whether the document's XMP metadata declares PDF/A conformance (`pdfaid:part`). Metadata that
|
||||
/// decodes past [`MAX_XMP_BYTES`] doesn't.
|
||||
/// Whether the document's XMP metadata declares PDF/A conformance (`pdfaid:part`). Metadata a
|
||||
/// filter stage of which decodes past [`MAX_XMP_BYTES`] doesn't.
|
||||
pub fn declares_pdfa(doc: &Document) -> bool {
|
||||
let Some(root) = doc.root() else { return false };
|
||||
let Some(m) = doc.get(root).as_dict().and_then(|c| c.get(b"Metadata").cloned()) else { return false };
|
||||
let m = doc.resolve(&m);
|
||||
match &*m {
|
||||
Object::Stream(s) => DecodeBudget::new(MAX_XMP_BYTES, 0).decode(s).is_ok_and(|b| b.windows(11).any(|w| w == b"pdfaid:part")),
|
||||
// Not a `DecodeBudget`, which counts every stage against one allowance: preflight bounds
|
||||
// each stage, and the two must agree on metadata stored through a chain of filters.
|
||||
Object::Stream(s) => s.decoded_within(MAX_XMP_BYTES).is_ok_and(|b| b.windows(11).any(|w| w == b"pdfaid:part")),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +156,11 @@ pub(crate) fn shapes_within(doc: &Document, page: usize, budget: &mut DecodeBudg
|
||||
let mut subpaths: Vec<DetectionPath> = Vec::new();
|
||||
let mut path = DetectionPath::default();
|
||||
let mut cur: Option<[f64; 2]> = None;
|
||||
for op in Pieces::join(&pieces).parse().ops {
|
||||
let joined = Pieces::join(&pieces);
|
||||
drop(pieces);
|
||||
// An operator at a time: dense content (`n n n …`) parsed whole holds about 80 bytes for each
|
||||
// byte it decoded.
|
||||
for op in pdfcraft_content::ops(joined.data()) {
|
||||
match op.op.as_slice() {
|
||||
b"q" => stack.push(ctm),
|
||||
b"Q" => ctm = stack.pop().unwrap_or(ctm),
|
||||
|
||||
@@ -3645,8 +3645,9 @@ fn page_shapes_decode_a_repeated_content_stream_within_the_budget() {
|
||||
assert!(budget.exhausted());
|
||||
}
|
||||
|
||||
/// The PDF/A declaration is read from at most [`crate::appearances::MAX_XMP_BYTES`] of metadata, as
|
||||
/// preflight reads it: a field drawn with characters its font lacks decodes no more than that.
|
||||
/// The PDF/A declaration is read from metadata each filter stage of which decodes within
|
||||
/// [`crate::appearances::MAX_XMP_BYTES`], as preflight reads it: a field drawn with characters its
|
||||
/// font lacks decodes no more than that a stage.
|
||||
#[test]
|
||||
fn a_pdfa_declaration_is_read_from_metadata_within_its_cap() {
|
||||
let declared = |xmp: String| declares_pdfa(&document(&["<< /Type /Catalog /Metadata 2 0 R >>".into(), raw_stream(&xmp)]));
|
||||
@@ -3654,6 +3655,16 @@ fn a_pdfa_declaration_is_read_from_metadata_within_its_cap() {
|
||||
assert!(declared(pdfa(1 << 20)));
|
||||
assert!(!declared(pdfa(crate::appearances::MAX_XMP_BYTES)), "metadata past the cap declares nothing");
|
||||
assert!(!declared("<x:xmpmeta><dc:title>pdfaid</dc:title></x:xmpmeta>".into()), "metadata without pdfaid:part declares nothing");
|
||||
// 3.5 MiB stored as hex, then Flate: its stages decode 7 and 3.5 MiB, each within the cap.
|
||||
let xmp = pdfa(7 << 19);
|
||||
let hex: Vec<u8> = xmp.bytes().flat_map(|b| [b"0123456789ABCDEF"[usize::from(b >> 4)], b"0123456789ABCDEF"[usize::from(b & 15)]]).collect();
|
||||
let mut stream = pdfcraft_cos::Stream::flate(pdfcraft_cos::Dict::new(), &hex);
|
||||
stream.dict.set(b"Filter".to_vec(), Object::Array(vec![Object::name("FlateDecode"), Object::name("ASCIIHexDecode")]));
|
||||
let mut doc = Document::new_empty();
|
||||
let metadata = doc.add(Object::Stream(stream));
|
||||
let catalog = doc.add(Object::Dict([(b"Metadata".to_vec(), Object::Ref(metadata))].into_iter().collect()));
|
||||
doc.trailer_mut().set(b"Root".to_vec(), Object::Ref(catalog));
|
||||
assert!(declares_pdfa(&doc), "metadata through a chain of filters declares PDF/A, as preflight reads it");
|
||||
}
|
||||
|
||||
/// A widget listed many times, by one field's `/Kids` or by a `/Kids` array several fields share,
|
||||
|
||||
Reference in new issue
Block a user