Skip to main content

zpdf_document/
optional_content.rs

1//! Optional-content (layer) configuration: the catalog's `/OCProperties`
2//! default configuration determines which optional-content groups render.
3//! Membership evaluation for `/OC` entries (OCG refs, OCMDs and visibility
4//! expressions) lives in zpdf-content, which has the object graph in hand;
5//! this module only answers "is group X on?".
6
7use std::collections::HashSet;
8
9use zpdf_core::{ObjectId, PdfObject};
10use zpdf_parser::PdfFile;
11
12const MAX_OC_GROUPS_PER_LIST: usize = 65_536;
13
14/// The document's default optional-content configuration (`/OCProperties /D`).
15#[derive(Debug, Clone, Default)]
16pub struct OcConfig {
17    /// Groups explicitly turned off.
18    off: HashSet<ObjectId>,
19    /// Groups explicitly turned on (overrides /BaseState /OFF).
20    on: HashSet<ObjectId>,
21    /// /BaseState /OFF: groups default to hidden unless listed in /ON.
22    base_state_off: bool,
23}
24
25impl OcConfig {
26    /// Visibility of a single optional-content group. Per 8.11.4.3 the
27    /// config applies in order BaseState → /ON → /OFF, so OFF wins when a
28    /// group is listed in both arrays.
29    pub fn group_visible(&self, id: ObjectId) -> bool {
30        if self.off.contains(&id) {
31            return false;
32        }
33        if self.on.contains(&id) {
34            return true;
35        }
36        !self.base_state_off
37    }
38
39    /// True when every group renders (no config means everything visible).
40    pub fn all_visible(&self) -> bool {
41        self.off.is_empty() && !self.base_state_off
42    }
43}
44
45/// Parse `/OCProperties` from the document catalog. Returns `None` when the
46/// document declares no optional content.
47pub fn parse_oc_config(file: &PdfFile) -> Option<OcConfig> {
48    let root_ref = file.trailer.get_ref("Root").ok()?;
49    let root = file.resolve(root_ref).ok()?;
50    let root_dict = root.as_dict().ok()?;
51
52    let ocp = resolve_dict(file, root_dict.get("OCProperties")?)?;
53    let d = resolve_dict(file, ocp.get("D")?).unwrap_or_default();
54
55    let mut config = OcConfig {
56        base_state_off: matches!(d.get_name("BaseState"), Ok("OFF")),
57        ..Default::default()
58    };
59    for id in ref_array(file, d.get("OFF")) {
60        config.off.insert(id);
61    }
62    for id in ref_array(file, d.get("ON")) {
63        config.on.insert(id);
64    }
65    Some(config)
66}
67
68fn resolve_dict(file: &PdfFile, obj: &PdfObject) -> Option<zpdf_core::PdfDict> {
69    match obj {
70        PdfObject::Dict(d) => Some(d.clone()),
71        PdfObject::Ref(r) => match file.resolve(*r).ok()? {
72            PdfObject::Dict(d) => Some(d),
73            _ => None,
74        },
75        _ => None,
76    }
77}
78
79fn ref_array(file: &PdfFile, obj: Option<&PdfObject>) -> Vec<ObjectId> {
80    let arr: std::borrow::Cow<'_, [PdfObject]> = match obj {
81        Some(PdfObject::Array(a)) => std::borrow::Cow::Borrowed(a),
82        Some(PdfObject::Ref(r)) => match file.resolve(*r) {
83            Ok(PdfObject::Array(a)) => std::borrow::Cow::Owned(a),
84            _ => return Vec::new(),
85        },
86        _ => return Vec::new(),
87    };
88    arr.iter()
89        .filter_map(|o| match o {
90            PdfObject::Ref(r) => Some(*r),
91            _ => None,
92        })
93        .take(MAX_OC_GROUPS_PER_LIST)
94        .collect()
95}