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