diff --git a/crates/redact/src/globals.rs b/crates/redact/src/globals.rs index 05b7c3bf0d..c11388b64a 100644 --- a/crates/redact/src/globals.rs +++ b/crates/redact/src/globals.rs @@ -21,7 +21,7 @@ pub(crate) fn kept(doc: &Document) -> HashSet { } /// `o`, or what references lead to from it. A ring of them ends as a reference, which is no dictionary. -fn followed(doc: &Document, o: &Object) -> Arc { +pub(crate) fn followed(doc: &Document, o: &Object) -> Arc { let mut at = doc.resolve(o); for _ in 0..MAX_HOPS { let next = match &*at { @@ -60,10 +60,10 @@ fn named(doc: &Document, s: &Stream) -> Vec { found } -/// `v`, or what it refers to, inside an array too. +/// `v`, or what references lead to from it, inside an array too. fn direct(doc: &Document, v: &Object) -> Object { - match &*doc.resolve(v) { - Object::Array(a) => Object::Array(a.iter().map(|e| (*doc.resolve(e)).clone()).collect()), + match &*followed(doc, v) { + Object::Array(a) => Object::Array(a.iter().map(|e| (*followed(doc, e)).clone()).collect()), other => other.clone(), } } diff --git a/crates/redact/src/image.rs b/crates/redact/src/image.rs index 9935055159..f429729929 100644 --- a/crates/redact/src/image.rs +++ b/crates/redact/src/image.rs @@ -13,8 +13,9 @@ use pdfcraft_cos::{Dict, Document, Object, Stream}; /// Most pixels an image may have to be cleared in place (larger ones are removed). pub(crate) const MAX_PIXELS: u64 = 64 << 20; +/// The components of image `s`'s colour space, through any references (`None` when unknown). pub(crate) fn components(doc: &Document, s: &Stream) -> Option { - let cs = doc.resolve(s.dict.get(b"ColorSpace")?); + let cs = crate::globals::followed(doc, s.dict.get(b"ColorSpace")?); let cs = &*cs; let name = match cs { Object::Name(n) => n.as_slice(), @@ -27,7 +28,7 @@ pub(crate) fn components(doc: &Document, s: &Stream) -> Option { b"DeviceCMYK" | b"CMYK" => 4, b"ICCBased" => { // /N lives in the ICC profile stream, and shall be 1, 3 or 4 (ISO 32000-2 §8.6.5.5). - match &*doc.resolve(cs.as_array()?.get(1)?) { + match &*crate::globals::followed(doc, cs.as_array()?.get(1)?) { Object::Stream(icc) => usize::try_from(icc.dict.int(b"N")?).ok().filter(|n| matches!(n, 1 | 3 | 4))?, _ => return None, } @@ -119,9 +120,9 @@ fn decode_and_clear(doc: &Document, s: &Stream, ctm: &Matrix, rects: &[[f64; 4]] data.truncate(bytes); // Image masks: a sample of 0 paints unless /Decode is [1 0]; cleared samples must not paint. let clear_bit = if mask { - // The array, and its first number, given by reference too. - let decode = s.dict.get(b"Decode").map(|d| doc.resolve(d)); - let first = decode.as_deref().and_then(Object::as_array).and_then(|d| d.first()).map(|v| doc.resolve(v)); + // The array, and its first number, given by reference too, through any references. + let decode = s.dict.get(b"Decode").map(|d| crate::globals::followed(doc, d)); + let first = decode.as_deref().and_then(Object::as_array).and_then(|d| d.first()).map(|v| crate::globals::followed(doc, v)); let inverted = first.as_deref().and_then(Object::as_f64) == Some(1.0); !inverted } else { @@ -175,10 +176,10 @@ fn decode_and_clear(doc: &Document, s: &Stream, ctm: &Matrix, rects: &[[f64; 4]] } /// Whether `s`'s data is in another file, which a reader may draw instead of what is here: a `/F` -/// that isn't null (a null entry is as if absent, and a reference to no object is null, ISO -/// 32000-2 §7.3.7 and §7.3.10). +/// that isn't null, through any references (a null entry is as if absent, and a reference to no +/// object is null, ISO 32000-2 §7.3.7 and §7.3.10). A ring of references counts as a file. pub(crate) fn data_elsewhere(doc: &Document, s: &Stream) -> bool { - s.dict.get(b"F").is_some_and(|f| !matches!(&*doc.resolve(f), Object::Null)) + s.dict.get(b"F").is_some_and(|f| !matches!(&*crate::globals::followed(doc, f), Object::Null)) } /// The cleared samples `data` of a `w` × `h` image as a stream with `dict`: Flate, or, for one bit diff --git a/crates/redact/src/tests.rs b/crates/redact/src/tests.rs index 4e9a9f1fd7..171138c3a9 100644 --- a/crates/redact/src/tests.rs +++ b/crates/redact/src/tests.rs @@ -620,6 +620,25 @@ fn a_fax_image_has_its_covered_pixels_cleared_instead_of_being_removed() { vec![b"1".to_vec()], [0x00, 0xFF, 0xFF, 0xFF], ), + // Through a reference to a reference: the array, its first number, and a colour space. + ( + format!("{image} /ImageMask true /Decode 8 0 R /Filter /CCITTFaxDecode /DecodeParms {parms}"), + g4.clone(), + vec![b"9 0 R".to_vec(), b"[1 0]".to_vec()], + [0x00, 0xFF, 0xFF, 0xFF], + ), + ( + format!("{image} /ImageMask true /Decode [8 0 R 0] /Filter /CCITTFaxDecode /DecodeParms {parms}"), + g4.clone(), + vec![b"9 0 R".to_vec(), b"1".to_vec()], + [0x00, 0xFF, 0xFF, 0xFF], + ), + ( + format!("{image} /ColorSpace 8 0 R /Filter /CCITTFaxDecode /DecodeParms {parms}"), + g4.clone(), + vec![b"9 0 R".to_vec(), b"/DeviceGray".to_vec()], + [0x00, 0xFF, 0xFF, 0xFF], + ), ] { let mut objs = vec![stream(&dict, data)]; objs.extend(extra); @@ -1544,6 +1563,17 @@ fn what_stays_is_stored_without_the_dictionary_however_it_names_it() { (format!("{image} /F null"), scan.page.clone(), vec![]), (format!("{image} /F 10 0 R"), scan.page.clone(), vec![b"null".to_vec()]), (format!("{image} /F 99 0 R"), scan.page.clone(), vec![]), + // Through a reference to a reference: /F to null, the size, the filter, the colour space. + (format!("{image} /F 10 0 R"), scan.page.clone(), vec![b"11 0 R".to_vec(), b"null".to_vec()]), + (image.replace("/Width 400", "/Width 10 0 R"), scan.page.clone(), vec![b"11 0 R".to_vec(), b"400".to_vec()]), + (image.replace("/Height 40", "/Height 10 0 R"), scan.page.clone(), vec![b"11 0 R".to_vec(), b"40".to_vec()]), + (image.replace("/Filter /JBIG2Decode", "/Filter 10 0 R"), scan.page.clone(), vec![b"11 0 R".to_vec(), b"/JBIG2Decode".to_vec()]), + ( + shared_image("/DecodeParms [null << /JBIG2Globals 9 0 R >>]").replace("/Filter /JBIG2Decode", "/Filter [/ASCIIHexDecode 10 0 R]"), + hex.clone(), + vec![b"11 0 R".to_vec(), b"/JBIG2Decode".to_vec()], + ), + (image.replace("/ColorSpace /DeviceGray", "/ColorSpace 10 0 R"), scan.page.clone(), vec![b"11 0 R".to_vec(), b"/DeviceGray".to_vec()]), ] { let mut doc = SharedPages { other: other.clone(), other_data: data, extra, ..SharedPages::new(&scan) }.build(&scan); mark(&mut doc, 0, &[CLEARS], ""); @@ -1641,6 +1671,12 @@ fn a_shared_jbig2_dictionary_that_cant_leave_refuses_the_redaction() { for (other, data, extra) in [ (image.replace("/Height 40", "/Height 41"), scan.page.clone(), vec![]), (image.replace("/DeviceGray", "/DeviceRGB"), scan.page.clone(), vec![]), + (image.replace("/ColorSpace /DeviceGray", "/ColorSpace 10 0 R"), scan.page.clone(), vec![b"11 0 R".to_vec(), b"/DeviceRGB".to_vec()]), + ( + format!("{image} /ColorSpace [/ICCBased 10 0 R]").replace("/ColorSpace /DeviceGray ", ""), + scan.page.clone(), + vec![b"11 0 R".to_vec(), stream("/N 3", b"")], + ), (shared_image("/DecodeParms << /JBIG2Globals 10 0 R >>"), scan.page.clone(), vec![b"9 0 R".to_vec()]), (elsewhere.to_string(), vec![0], vec![]), (not_jbig2, pdfcraft_filters::encode_flate(&scan.bits), vec![]),