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}