mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 00:16:39 +00:00
address review: follow chained /Type and /BaseState references, hash-set ON/OFF lists
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
||||
@@ -859,3 +859,16 @@ pub fn clear_marks(doc: &mut Document, pages: Option<&[usize]>) -> Result<usize,
|
||||
}
|
||||
Ok(targets.len())
|
||||
}
|
||||
|
||||
/// `o` with a chain of references followed, as a viewer follows it, to at most 16 links (so a
|
||||
/// reference cycle ends): hidden-layer state must not escape classification through one.
|
||||
pub(crate) fn resolve_chain(doc: &pdfcraft_cos::Document, o: &pdfcraft_cos::Object) -> std::sync::Arc<pdfcraft_cos::Object> {
|
||||
let mut o = doc.resolve(o);
|
||||
for _ in 0..16 {
|
||||
if !matches!(*o, pdfcraft_cos::Object::Ref(_)) {
|
||||
break;
|
||||
}
|
||||
o = doc.resolve(&o);
|
||||
}
|
||||
o
|
||||
}
|
||||
|
||||
@@ -334,10 +334,12 @@ fn hidden_layers(doc: &Document) -> Vec<ObjRef> {
|
||||
let list = |k: &[u8]| -> Vec<ObjRef> {
|
||||
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<ObjRef>, std::collections::HashSet<ObjRef>) =
|
||||
(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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user