diff --git a/crates/cos/src/document/objects.rs b/crates/cos/src/document/objects.rs index e7d7255306..fb20b50cf1 100644 --- a/crates/cos/src/document/objects.rs +++ b/crates/cos/src/document/objects.rs @@ -162,18 +162,20 @@ impl Document { /// an indirect dictionary resolved, and of an array (itself perhaps indirect), the entries at /// the filters' places (`/Crypt` counts, ISO 32000-2 ยง7.4.1) resolved, the rest dropped. Of /// each dictionary only the entries the decoders read are kept, numbers and flags given by - /// reference (`/Predictor 7 0 R`) resolved; `/JBIG2Globals` stays a reference. `s` itself - /// when none of those is indirect, or when the chain is longer than any decoder takes + /// reference (`/Predictor 7 0 R`) resolved; `/JBIG2Globals` stays a reference. A `/Crypt` + /// entry, which no decoder reads ([`Stream::filters`] leaves it out), is null. `s` itself + /// when none of those is indirect, or when the other filters are more than any decoder takes /// ([`pdfcraft_filters::MAX_CHAIN`]). Only small values are copied, so a file can't have a /// large object copied once for each key and filter. pub fn with_direct_parms<'s>(&self, s: &'s Stream) -> Cow<'s, Stream> { let Some(p) = s.dict.get(b"DecodeParms") else { return Cow::Borrowed(s) }; - let filters = match s.dict.get(b"Filter") { - Some(Object::Array(a)) => a.len(), - Some(_) => 1, - None => 0, + let crypt = |f: &Object| matches!(f.as_name(), Some(b"Crypt")); + let filters: Vec = match s.dict.get(b"Filter") { + Some(Object::Array(a)) => a.iter().map(crypt).collect(), + Some(f) => vec![crypt(f)], + None => Vec::new(), }; - if filters > pdfcraft_filters::MAX_CHAIN { + if filters.iter().filter(|c| !**c).count() > pdfcraft_filters::MAX_CHAIN { return Cow::Borrowed(s); } let mut changed = matches!(p, Object::Ref(_)); @@ -196,7 +198,9 @@ impl Document { Object::Dict(made) }; let made = match &*self.held(p) { - Object::Array(a) => Object::Array(a.get(..filters.min(a.len())).unwrap_or_default().iter().map(|e| one(e, &mut changed)).collect()), + Object::Array(a) => { + Object::Array(a.iter().zip(&filters).map(|(e, crypt)| if *crypt { Object::Null } else { one(e, &mut changed) }).collect()) + } _ => one(p, &mut changed), }; if !changed { diff --git a/crates/cos/src/document/tests.rs b/crates/cos/src/document/tests.rs index edbd68f562..d8d75c4af1 100644 --- a/crates/cos/src/document/tests.rs +++ b/crates/cos/src/document/tests.rs @@ -58,6 +58,25 @@ fn decode_parms_given_by_reference_are_read_once_direct() { assert_eq!(made.dict.get(b"DecodeParms").and_then(Object::as_array).map(Vec::len), Some(1)); let long = stream(pdfcraft_filters::MAX_CHAIN + 1, Object::Array(vec![Object::Ref(r); 9])); assert!(matches!(doc.with_direct_parms(&long), Cow::Borrowed(_))); + // A chain as long as a decoder takes is resolved, and a `/Crypt` in it doesn't count, as + // the decoders leave it out: ASCIIHex as often as the limit allows, then the predictor's + // Flate, its parameters by reference. + let hex = |b: &[u8]| b.iter().flat_map(|x| format!("{x:02X}").into_bytes()).chain(*b">").collect::>(); + let wrapped = (1..pdfcraft_filters::MAX_CHAIN).fold(coded.clone(), |data, _| hex(&data)); + for crypt in [false, true] { + let mut names = vec![Object::name("ASCIIHexDecode"); pdfcraft_filters::MAX_CHAIN - 1]; + names.push(Object::name("FlateDecode")); + if crypt { + names.insert(0, Object::name("Crypt")); + } + let mut entries = vec![Object::Null; names.len() - 1]; + entries.push(Object::Ref(r)); + let mut d = Dict::new(); + d.set(b"Filter".to_vec(), Object::Array(names)); + d.set(b"DecodeParms".to_vec(), Object::Array(entries)); + let s = Stream::from_raw(d, wrapped.clone()); + assert_eq!(doc.with_direct_parms(&s).decoded().unwrap(), rows, "with /Crypt: {crypt}"); + } } #[test]