diff --git a/crates/redact/src/interp/layers.rs b/crates/redact/src/interp/layers.rs index e7cad30a8e..acc9a97bd5 100644 --- a/crates/redact/src/interp/layers.rs +++ b/crates/redact/src/interp/layers.rs @@ -258,5 +258,5 @@ pub(super) fn strip_hidden_layers(doc: &Document, scope: &mut Scope<'_>, ops: Ve /// Is this dictionary's `/Type` the given name? An indirect `/Type` is followed, as a viewer /// follows it: a group or membership dictionary must not escape classification through one. fn has_type(doc: &Document, d: &Dict, name: &[u8]) -> bool { - d.get(b"Type").is_some_and(|t| doc.resolve(t).as_name() == Some(name)) + d.get(b"Type").is_some_and(|t| crate::resolve_chain(doc, t).as_name() == Some(name)) } diff --git a/crates/redact/src/lib.rs b/crates/redact/src/lib.rs index 41ac47357e..9895a9e64c 100644 --- a/crates/redact/src/lib.rs +++ b/crates/redact/src/lib.rs @@ -859,3 +859,16 @@ pub fn clear_marks(doc: &mut Document, pages: Option<&[usize]>) -> Result std::sync::Arc { + let mut o = doc.resolve(o); + for _ in 0..16 { + if !matches!(*o, pdfcraft_cos::Object::Ref(_)) { + break; + } + o = doc.resolve(&o); + } + o +} diff --git a/crates/redact/src/sanitize.rs b/crates/redact/src/sanitize.rs index a869d0510f..2c8812e652 100644 --- a/crates/redact/src/sanitize.rs +++ b/crates/redact/src/sanitize.rs @@ -334,10 +334,12 @@ fn hidden_layers(doc: &Document) -> Vec { let list = |k: &[u8]| -> Vec { d.get(k).and_then(|g| doc.resolve(g).as_array().map(|a| a.iter().filter_map(Object::as_ref).collect())).unwrap_or_default() }; - let (on, off) = (list(b"ON"), list(b"OFF")); + // sets: the lists are the document's, and a filter over every group would scan them each time + let (on, off): (std::collections::HashSet, std::collections::HashSet) = + (list(b"ON").into_iter().collect(), list(b"OFF").into_iter().collect()); // The default state is /BaseState, then /ON turns groups on and /OFF turns them off, so a // group both lists name ends up off (§8.11.4.3, Table 101) — the viewer reads it the same way. - let base_off = d.get(b"BaseState").map(|b| doc.resolve(b)).is_some_and(|b| b.as_name() == Some(b"OFF".as_slice())); + let base_off = d.get(b"BaseState").map(|b| crate::resolve_chain(doc, b)).is_some_and(|b| b.as_name() == Some(b"OFF".as_slice())); all.into_iter().filter(|g| off.contains(g) || (base_off && !on.contains(g))).collect() } diff --git a/crates/redact/src/tests.rs b/crates/redact/src/tests.rs index 6f9bda38c6..ce23f0a078 100644 --- a/crates/redact/src/tests.rs +++ b/crates/redact/src/tests.rs @@ -3904,6 +3904,24 @@ fn an_indirect_base_state_off_hides_the_layer() { assert_layer_secret_removed(layer_state_page(vec![("BaseState", Object::Ref(ObjRef::new(8, 0)))], "7 0 R", vec![b"/OFF".to_vec()])); } +#[test] +fn a_chained_base_state_off_hides_the_layer() { + assert_layer_secret_removed(layer_state_page( + vec![("BaseState", Object::Ref(ObjRef::new(8, 0)))], + "7 0 R", + vec![b"9 0 R".to_vec(), b"/OFF".to_vec()], + )); +} + +#[test] +fn a_chained_membership_type_is_evaluated() { + assert_layer_secret_removed(layer_state_page( + vec![("BaseState", Object::name("ON"))], + "8 0 R", + vec![b"<< /Type 9 0 R /OCGs [7 0 R] /P /AllOff >>".to_vec(), b"10 0 R".to_vec(), b"/OCMD".to_vec()], + )); +} + #[test] fn off_wins_over_on_when_the_base_state_is_off() { assert_layer_secret_removed(layer_state_page(