diff --git a/crates/redact/src/lib.rs b/crates/redact/src/lib.rs index c865c78399..e2724b0ff0 100644 --- a/crates/redact/src/lib.rs +++ b/crates/redact/src/lib.rs @@ -704,7 +704,12 @@ fn redact_page_content( // longer draws so the removed image or form bytes cannot survive in the saved file. let page_content: Vec> = new_list .iter() - .map(|o| match &*doc.resolve(o) { + .zip(&list) + .zip(&data) + .map(|((now, was), read)| match &*doc.resolve(now) { + // Left as it was: what was read above, with its parameters by reference. + _ if now == was => read.clone(), + // Written above by `Stream::flate`, which leaves no `/DecodeParms`. Object::Stream(s) => s.decoded().unwrap_or_default(), _ => Vec::new(), }) diff --git a/crates/redact/src/tests.rs b/crates/redact/src/tests.rs index 4bcacefbab..8b91d1128a 100644 --- a/crates/redact/src/tests.rs +++ b/crates/redact/src/tests.rs @@ -81,31 +81,36 @@ fn glyphs_under_a_mark_go_and_the_rest_stays_put() { assert!(!content(&doc, 0).contains("1234")); } +/// `content` behind a PNG Up predictor whose `/DecodeParms` is an indirect object added to `doc`, +/// in a stream with `dict`'s entries too. +fn predicted(doc: &mut Document, dict: pdfcraft_cos::Dict, content: &[u8]) -> pdfcraft_cos::Stream { + use pdfcraft_cos::{Dict, Object, Stream}; + // One column: each byte a row of its own, tagged 2. + let mut rows = Vec::new(); + let mut above = 0u8; + for &b in content { + rows.extend([2, b.wrapping_sub(above)]); + above = b; + } + let parms = doc.add(Object::Dict([(b"Predictor".to_vec(), Object::Int(12))].into_iter().collect::())); + let mut s = Stream::flate(Dict::new(), &rows); + for (k, v) in dict.iter() { + s.dict.set(k.to_vec(), v.clone()); + } + s.dict.set(b"DecodeParms".to_vec(), Object::Ref(parms)); + s +} + /// Content whose `/DecodeParms` is given by reference is read through the reference, on the page /// and in a form: the text under a mark goes. Read with the predictor's tag bytes left in, no glyph /// was found under the mark, and the redaction left the text in the file and said it was done. #[test] fn content_with_parameters_given_by_reference_is_redacted() { - use pdfcraft_cos::{Dict, ObjRef, Object, Stream}; - // PNG Up, one column: each byte a row of its own, tagged 2. - let predicted = |doc: &mut Document, mut dict: Dict| { - let mut rows = Vec::new(); - let mut above = 0u8; - for &b in b"BT /F1 10 Tf 10 100 Td (SECRET) Tj ET" { - rows.extend([2, b.wrapping_sub(above)]); - above = b; - } - let parms = doc.add(Object::Dict([(b"Predictor".to_vec(), Object::Int(12))].into_iter().collect::())); - dict.set(b"DecodeParms".to_vec(), Object::Ref(parms)); - let mut s = Stream::flate(Dict::new(), &rows); - for (k, v) in dict.iter() { - s.dict.set(k.to_vec(), v.clone()); - } - s - }; + use pdfcraft_cos::{Dict, ObjRef, Object}; + let secret = b"BT /F1 10 Tf 10 100 Td (SECRET) Tj ET"; let marked = [[5.0, 90.0, 120.0, 115.0]]; let mut page = one_page(b"", "", vec![]); - let s = predicted(&mut page, Dict::new()); + let s = predicted(&mut page, Dict::new(), secret); page.set(ObjRef::new(4, 0), s); // The parameters take object 7 and the form 8. let mut form = one_page(b"/Fm Do", "/XObject << /Fm 8 0 R >>", vec![]); @@ -118,7 +123,7 @@ fn content_with_parameters_given_by_reference_is_redacted() { [(b"Font".to_vec(), Object::Dict([(b"F1".to_vec(), Object::Ref(ObjRef::new(5, 0)))].into_iter().collect()))].into_iter().collect(), ), ); - let s = predicted(&mut form, dict); + let s = predicted(&mut form, dict, secret); form.set(ObjRef::new(8, 0), s); for (mut doc, what) in [(page, "page"), (form, "form")] { assert_eq!(under(&mut doc, 0, &marked), 6, "{what}"); @@ -129,6 +134,31 @@ fn content_with_parameters_given_by_reference_is_redacted() { } } +/// An image removed under a mark stays in the page's resources while another of its content +/// streams, with `/DecodeParms` given by reference, still draws it. Read without the parameters, +/// that stream seemed not to draw it, and the page lost the image there. +#[test] +fn an_image_drawn_again_in_content_with_parameters_by_reference_stays() { + use pdfcraft_cos::{Dict, ObjRef, Object}; + let img = stream("/Type /XObject /Subtype /Image /Width 4 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8", [255, 255, 255, 255]); + let mut doc = one_page(b"q 10 0 0 10 10 10 cm /Im1 Do Q", "/XObject << /Im1 7 0 R >>", vec![img]); + let again = predicted(&mut doc, Dict::new(), b"q 10 0 0 10 200 200 cm /Im1 Do Q"); + let again = doc.add(Object::Stream(again)); + doc.update_dict(ObjRef::new(3, 0), |d| d.set(b"Contents".to_vec(), Object::Array(vec![Object::Ref(ObjRef::new(4, 0)), Object::Ref(again)]))) + .unwrap(); + mark(&mut doc, 0, &[[5.0, 5.0, 25.0, 25.0]], ""); + let r = apply(&mut doc, None).unwrap(); + assert_eq!(r.images_removed, 1, "{r:?}"); + let doc = reopen(&doc); + assert!(content(&doc, 0).contains("200 200 cm /Im1 Do"), "{}", content(&doc, 0)); + let p = pdfcraft_model::pages(&doc).swap_remove(0); + let res = doc.resolve(p.dict.get(b"Resources").unwrap()).as_dict().cloned().unwrap(); + let xo = doc.resolve(res.get(b"XObject").unwrap()).as_dict().cloned().unwrap(); + // The save numbers objects afresh: the image is told by what it is. + let Some(Object::Stream(image)) = xo.reference(b"Im1").map(|r| (*doc.get(r)).clone()) else { panic!("no /Im1: {xo:?}") }; + assert_eq!((image.dict.name(b"Subtype"), image.decoded().ok()), (Some(&b"Image"[..]), Some(vec![255; 4]))); +} + /// #2130: a font with no /Widths is drawn with its embedded program's advances. Measured with a /// guess from its name instead (Courier's 600 units for "Mono"), the end of the line seemed to /// lie past a mark that covers all of it on screen, and was kept.