Skip to main content

stet_pdf_reader/layers/
mod.rs

1// stet-pdf-reader
2// Copyright (c) 2026 Scott Bowman
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! PDF Optional Content (layers).
6//!
7//! Layers — formally Optional Content Groups (OCGs) in ISO 32000-2
8//! §8.11 — let a PDF mark slices of its content for selective
9//! visibility: CAD layers, watermarks, multilingual annotations,
10//! print-only or screen-only overlays, and so on.
11//!
12//! This module exposes the read-only metadata side of the OCG model:
13//!
14//! - One [`Layer`] per OCG, carrying the layer's name, intent, lock
15//!   state, default visibility, full `/Usage` sub-dict, and any
16//!   `/CreatorInfo` hint.
17//!
18//! Hierarchy (`/Order`), alternate configurations, runtime visibility
19//! overrides, OCMD policies, and `/AS` automatic-state rules land in
20//! later phases. Phase 1 is just the per-layer record so consumers can
21//! enumerate what a document contains.
22//!
23//! # Quick reference
24//!
25//! ```no_run
26//! use stet_pdf_reader::PdfDocument;
27//!
28//! let data = std::fs::read("layered.pdf")?;
29//! let doc = PdfDocument::from_bytes(&data)?;
30//!
31//! for layer in doc.layers() {
32//!     println!(
33//!         "{} (id={}, locked={}, default_visible={})",
34//!         layer.name, layer.ocg_id, layer.locked, layer.default_visible
35//!     );
36//! }
37//! # Ok::<(), Box<dyn std::error::Error>>(())
38//! ```
39
40pub mod configuration;
41pub mod metadata;
42pub mod ocmd;
43
44pub use configuration::{
45    AutoStateEvent, AutoStateRule, BaseState, Configuration, LayerTree, LayerTreeNode, ListMode,
46};
47pub use metadata::{
48    CreatorInfo, ExportUsage, LanguageUsage, Layer, LayerIntent, LayerUsage, PageElementSubtype,
49    PrintUsage, UsageState, UserUsage, ViewUsage, ZoomUsage,
50};
51
52// Re-export the underlying renderer types so consumers don't have to
53// reach into `stet-graphics` to construct visibility predicates.
54pub use stet_graphics::display_list::{MembershipPolicy, OcgVisibility, VisibilityExpr};
55pub use stet_graphics::layer_set::LayerSet;
56
57use crate::PdfDocument;
58
59/// Which audience a render is for.
60///
61/// Drives [`PdfDocument::layer_set_for`]: the resulting [`LayerSet`]
62/// has every `/AS` automatic-state rule whose `/Event` matches this
63/// intent applied on top of the default-configuration starting point.
64///
65/// PDF authors use this to hide print-only watermarks during
66/// interactive viewing, or to surface annotations only for export.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68#[non_exhaustive]
69pub enum RenderIntent {
70    /// `/View` — interactive on-screen display.
71    View,
72    /// `/Print` — printing pipeline.
73    Print,
74    /// `/Export` — conversion or extraction.
75    Export,
76}
77
78impl RenderIntent {
79    fn matches(self, event: AutoStateEvent) -> bool {
80        matches!(
81            (self, event),
82            (RenderIntent::View, AutoStateEvent::View)
83                | (RenderIntent::Print, AutoStateEvent::Print)
84                | (RenderIntent::Export, AutoStateEvent::Export)
85        )
86    }
87}
88
89/// Build a [`LayerSet`] that reflects the document's default
90/// configuration with every `/AS` automatic-state rule for the given
91/// [`RenderIntent`] applied.
92///
93/// Algorithm:
94///
95/// 1. Start from [`layer_set_from_document`].
96/// 2. For each auto-state rule on the default configuration whose
97///    `/Event` matches the requested intent:
98///     - For each OCG listed in the rule's `/OCGs`:
99///         - For each `/Category` in the rule:
100///             - If that category's `/Usage` sub-dict on the OCG
101///               carries an explicit ON/OFF state, override that
102///               OCG's entry in the LayerSet.
103///
104/// PDF spec doesn't define precedence when multiple rules touch the
105/// same OCG; this implementation is **last-wins** in the order rules
106/// appear in `/AS`.
107///
108/// Layers carrying `/Usage` hints with **no** matching `/AS` rule are
109/// untouched — by spec their hints are informational only, not
110/// auto-applied. Some viewers heuristically apply them anyway; stet
111/// does not.
112pub fn layer_set_for(doc: &PdfDocument<'_>, intent: RenderIntent) -> LayerSet {
113    let mut set = layer_set_from_document(doc);
114    let Some(cfg) = doc.default_configuration() else {
115        return set;
116    };
117    for rule in &cfg.auto_state {
118        if !intent.matches(rule.event) {
119            continue;
120        }
121        for &ocg_id in &rule.ocgs {
122            let Some(layer) = doc.layer(ocg_id) else {
123                continue;
124            };
125            for category in &rule.categories {
126                if let Some(state) = usage_state_for_category(&layer.usage, category) {
127                    set.set(ocg_id, matches!(state, UsageState::On));
128                }
129            }
130        }
131    }
132    set
133}
134
135/// Look up an OCG's `/Usage` sub-dict for the given `/Category` name
136/// and return its ON/OFF state, if it carries one. Categories that
137/// don't carry a state (Zoom, Language, User, PageElement,
138/// CreatorInfo) return `None` and are silently skipped by
139/// [`layer_set_for`].
140fn usage_state_for_category(usage: &LayerUsage, category: &str) -> Option<UsageState> {
141    match category {
142        "View" => usage.view.as_ref().map(|v| v.state),
143        "Print" => usage.print.as_ref().map(|p| p.state),
144        "Export" => usage.export.as_ref().map(|e| e.state),
145        _ => None,
146    }
147}
148
149/// Build a [`LayerSet`] populated from a [`PdfDocument`]'s default
150/// configuration (`/OCProperties /D`).
151///
152/// Each layer gets an explicit override matching its `default_visible`
153/// flag; the resulting set is therefore equivalent to "what the
154/// document looks like with no user toggles applied" but lets a UI
155/// toggle individual layers from a known starting point.
156pub fn layer_set_from_document(doc: &PdfDocument<'_>) -> LayerSet {
157    let mut set = LayerSet::new();
158    for layer in doc.layers() {
159        set.set(layer.ocg_id, layer.default_visible);
160    }
161    set
162}
163
164/// Build a [`LayerSet`] populated from one of the document's
165/// alternate configurations (`/OCProperties /Configs`).
166///
167/// `index = 0` is the default configuration; `1..N` are the entries
168/// of `/Configs`. Returns `None` for an out-of-range index.
169///
170/// `BaseState::On` starts every layer ON before applying the
171/// configuration's `/OFF` overrides; `BaseState::Off` starts every
172/// layer OFF before applying `/ON`; `BaseState::Unchanged` carries
173/// each layer's metadata-level `default_visible` forward.
174pub fn layer_set_from_configuration(doc: &PdfDocument<'_>, index: usize) -> Option<LayerSet> {
175    let cfg = doc.configuration(index)?;
176    let mut set = LayerSet::new();
177    let initial = match cfg.base_state {
178        BaseState::On => Some(true),
179        BaseState::Off => Some(false),
180        BaseState::Unchanged => None,
181    };
182    for layer in doc.layers() {
183        let v = match initial {
184            Some(b) => b,
185            None => layer.default_visible,
186        };
187        set.set(layer.ocg_id, v);
188    }
189    for &id in &cfg.on {
190        set.set(id, true);
191    }
192    for &id in &cfg.off {
193        set.set(id, false);
194    }
195    Some(set)
196}