diff --git a/crates/redact/src/globals.rs b/crates/redact/src/globals.rs index c11388b64a..b095059a7e 100644 --- a/crates/redact/src/globals.rs +++ b/crates/redact/src/globals.rs @@ -68,7 +68,27 @@ fn direct(doc: &Document, v: &Object) -> Object { } } -/// `s` with what `decoded_image` and `decoded_within` read as given made direct: its size and filters. +/// `/DecodeParms` as a reader takes them, through any references: the array or dictionary, each +/// entry, and each value made direct, but `/JBIG2Globals`, which stays the reference it is. +fn direct_parms(doc: &Document, v: &Object) -> Object { + let entry = |e: &Object| match &*followed(doc, e) { + Object::Dict(d) => { + let mut made = d.clone(); + for (key, value) in d.iter().filter(|(key, _)| key.as_slice() != b"JBIG2Globals") { + made.set(key.to_vec(), (*followed(doc, value)).clone()); + } + Object::Dict(made) + } + other => other.clone(), + }; + match &*followed(doc, v) { + Object::Array(a) => Object::Array(a.iter().map(entry).collect()), + other => entry(other), + } +} + +/// `s` with what `decoded_image` and `decoded_within` read as given made direct: its size, filters +/// and their parameters. fn readable(doc: &Document, s: &Stream) -> Stream { let mut readable = s.clone(); for key in [b"Width".as_slice(), b"Height", b"Filter"] { @@ -76,6 +96,9 @@ fn readable(doc: &Document, s: &Stream) -> Stream { readable.dict.set(key.to_vec(), direct(doc, v)); } } + if let Some(v) = s.dict.get(b"DecodeParms") { + readable.dict.set(b"DecodeParms".to_vec(), direct_parms(doc, v)); + } readable } diff --git a/crates/redact/src/tests.rs b/crates/redact/src/tests.rs index 171138c3a9..48d00da332 100644 --- a/crates/redact/src/tests.rs +++ b/crates/redact/src/tests.rs @@ -1791,6 +1791,44 @@ fn a_dictionary_that_names_itself_isnt_counted_as_stored() { assert!(matches!(apply(&mut doc, None), Err(RedactError::SharedJbig2Globals(1)))); } +/// A copy of the dictionary stored with a predictor is found whatever way its parameters are given, +/// through a chain of references among them, as a reader that follows them decodes it. Through a +/// chain, PdfCraft can't decode the image with the copy's symbols to store it again, so the run is +/// refused; it never goes on with the copy left in the file. +#[test] +fn a_copy_is_found_however_its_parameters_are_given() { + let scan = shared_scan(); + let globals = [jbig2_globals(b"LEAK_GLOBALS"), scan.symbols.clone()].concat(); + // PNG rows of one byte, each with no prediction: a byte for the row's tag, then the byte. + let predicted: Vec = globals.iter().flat_map(|b| [0u8, *b]).collect(); + let flate = pdfcraft_filters::encode_flate(&predicted); + let parms = b"<< /Predictor 12 /Columns 1 >>".to_vec(); + for (spelling, extra, stored) in [ + ("/DecodeParms << /Predictor 12 /Columns 1 >>", vec![], true), + ("/DecodeParms 11 0 R", vec![parms.clone()], true), + ("/DecodeParms 11 0 R", vec![b"12 0 R".to_vec(), parms.clone()], false), + ("/DecodeParms [11 0 R]", vec![b"12 0 R".to_vec(), parms.clone()], false), + ("/DecodeParms << /Predictor 11 0 R /Columns 1 >>", vec![b"12 0 R".to_vec(), b"12".to_vec()], false), + ] { + let mut objects = vec![stream(&format!("/Filter /FlateDecode {spelling}"), flate.clone())]; + objects.extend(extra.clone()); + let other = shared_image("/DecodeParms << /JBIG2Globals 10 0 R >>"); + let mut doc = SharedPages { other, extra: objects, ..SharedPages::new(&scan) }.build(&scan); + mark(&mut doc, 0, &[CLEARS], ""); + let at = format!("{spelling} {}", extra.len()); + match apply(&mut doc, None) { + Ok(report) => { + assert!(stored, "{at}: went on"); + assert_eq!(report.images_restored, 1, "{at}"); + let kept: std::collections::HashSet = pdfcraft_cos::reachable(&doc).into_iter().map(|r| r.num).collect(); + assert!(!kept.contains(&10) && !kept.contains(&9), "{at}: the copy or the dictionary stays"); + assert_eq!(drawn_samples(&reopen(&doc), 1), scan.bits, "{at}"); + } + Err(e) => assert!(!stored && matches!(e, RedactError::SharedJbig2Globals(1)), "{at}: {e:?}"), + } + } +} + /// An image that names the dictionary and another one is stored again without both, but the other /// was shared with nothing that left: an image that uses only it stays as it was, whichever of the /// two comes first in the file, and so does that dictionary.