stet_graphics/layer_set.rs
1// stet - A PostScript Interpreter
2// Copyright (c) 2026 Scott Bowman
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! Per-render OCG visibility overrides.
6//!
7//! [`LayerSet`] is the renderer-side contract for "which OCGs are
8//! currently on". The display list carries every OcgGroup's
9//! per-variant `default_visible` fallback baked from the document's
10//! default configuration, but a consumer building a layer panel needs
11//! to flip a layer on or off without re-parsing the PDF or rebuilding
12//! its display list.
13//!
14//! The flow:
15//!
16//! 1. The PDF reader (or any other producer) builds a display list
17//! whose [`crate::display_list::DisplayElement::OcgGroup`] elements
18//! each carry an [`crate::display_list::OcgVisibility`] predicate.
19//! 2. The consumer constructs a `LayerSet` — empty (every OCG falls
20//! back to its `default_visible`) or populated from a particular
21//! PDF configuration.
22//! 3. Mutate it as needed, e.g. via [`LayerSet::set`].
23//! 4. Pass it into the renderer; rendering calls
24//! [`LayerSet::evaluate`] for each OcgGroup.
25//!
26//! Higher-level constructors that build a `LayerSet` from a parsed
27//! document live in `stet-pdf-reader`'s `layers` module.
28
29use std::collections::HashMap;
30
31use crate::display_list::{MembershipPolicy, OcgVisibility, VisibilityExpr};
32
33/// Per-render override of OCG visibility.
34///
35/// An OCG with no entry here falls back to its display-list-baked
36/// `default_visible`. Entries are explicit `bool`s so a consumer can
37/// override a layer in either direction.
38#[derive(Clone, Debug, Default)]
39pub struct LayerSet {
40 states: HashMap<u32, bool>,
41}
42
43impl LayerSet {
44 /// Construct an empty set — every OCG falls back to its
45 /// `default_visible`.
46 pub fn new() -> Self {
47 Self::default()
48 }
49
50 /// Override the visibility of an OCG.
51 pub fn set(&mut self, ocg_id: u32, visible: bool) {
52 self.states.insert(ocg_id, visible);
53 }
54
55 /// Get the explicit override for an OCG, if any. `None` means the
56 /// renderer should fall back to `default_visible`.
57 pub fn get(&self, ocg_id: u32) -> Option<bool> {
58 self.states.get(&ocg_id).copied()
59 }
60
61 /// Drop an explicit override, restoring fallback behaviour.
62 pub fn clear(&mut self, ocg_id: u32) {
63 self.states.remove(&ocg_id);
64 }
65
66 /// Number of explicit overrides.
67 pub fn len(&self) -> usize {
68 self.states.len()
69 }
70
71 /// True when no explicit overrides exist.
72 pub fn is_empty(&self) -> bool {
73 self.states.is_empty()
74 }
75
76 /// Resolve a single OCG's visibility — explicit override if
77 /// present, otherwise the supplied fallback.
78 fn resolve(&self, ocg_id: u32, fallback: bool) -> bool {
79 self.states.get(&ocg_id).copied().unwrap_or(fallback)
80 }
81
82 /// Evaluate an [`OcgVisibility`] predicate.
83 ///
84 /// Each variant's `default_visible` is the renderer's fallback
85 /// **for the whole group** when this `LayerSet` has no opinion on
86 /// any of its leaves. Specifically:
87 ///
88 /// - `Single` → consult the LayerSet for `ocg_id`; fall back to
89 /// `default_visible`.
90 /// - `Membership` / `Expression` → if **none** of the relevant
91 /// leaves are overridden by this LayerSet, return
92 /// `default_visible` directly. This is the "consumer has no
93 /// opinion at all" path and preserves byte-identity for OCMD
94 /// defaults baked at parse time.
95 /// - With at least one leaf overridden, evaluate the policy or
96 /// expression: overridden leaves use their override value,
97 /// missing leaves fall back to the variant's `default_visible`.
98 pub fn evaluate(&self, vis: &OcgVisibility) -> bool {
99 match vis {
100 OcgVisibility::Single {
101 ocg_id,
102 default_visible,
103 } => self.resolve(*ocg_id, *default_visible),
104 OcgVisibility::Membership {
105 ocg_ids,
106 policy,
107 default_visible,
108 } => {
109 if ocg_ids.is_empty() {
110 // PDF spec: an OCMD with no /OCGs is always visible.
111 return true;
112 }
113 // Fast path: no leaf has an override → return the
114 // OCMD's overall default (matches the document's
115 // statically-evaluated visibility under the default
116 // configuration).
117 if ocg_ids.iter().all(|id| !self.states.contains_key(id)) {
118 return *default_visible;
119 }
120 let on_count = ocg_ids
121 .iter()
122 .filter(|id| self.resolve(**id, *default_visible))
123 .count();
124 let total = ocg_ids.len();
125 match policy {
126 MembershipPolicy::AllOn => on_count == total,
127 MembershipPolicy::AnyOn => on_count > 0,
128 MembershipPolicy::AllOff => on_count == 0,
129 MembershipPolicy::AnyOff => on_count < total,
130 }
131 }
132 OcgVisibility::Expression {
133 expr,
134 default_visible,
135 } => {
136 if !expr_touches_overrides(expr, &self.states) {
137 return *default_visible;
138 }
139 self.evaluate_expr(expr, *default_visible)
140 }
141 }
142 }
143
144 /// Recursively evaluate a `/VE` expression.
145 fn evaluate_expr(&self, expr: &VisibilityExpr, default_visible: bool) -> bool {
146 match expr {
147 VisibilityExpr::Layer(id) => self.resolve(*id, default_visible),
148 VisibilityExpr::Not(inner) => !self.evaluate_expr(inner, default_visible),
149 VisibilityExpr::And(operands) => operands
150 .iter()
151 .all(|o| self.evaluate_expr(o, default_visible)),
152 VisibilityExpr::Or(operands) => operands
153 .iter()
154 .any(|o| self.evaluate_expr(o, default_visible)),
155 }
156 }
157
158 /// Apply a radio-button-group constraint: when one layer in the
159 /// group is turned ON, all the others get explicitly turned OFF.
160 /// `newly_on` is left ON.
161 ///
162 /// Layers in `group` other than `newly_on` are explicitly forced
163 /// OFF (they get an entry in this set, not just a missing entry).
164 pub fn enforce_rb_group(&mut self, group: &[u32], newly_on: u32) {
165 for &id in group {
166 if id == newly_on {
167 self.states.insert(id, true);
168 } else {
169 self.states.insert(id, false);
170 }
171 }
172 }
173}
174
175/// Return true when any `Layer(id)` leaf of the expression has an
176/// explicit entry in `states`. Used by [`LayerSet::evaluate`] to
177/// detect "consumer has no opinion" and short-circuit to the
178/// variant's `default_visible`.
179fn expr_touches_overrides(expr: &VisibilityExpr, states: &HashMap<u32, bool>) -> bool {
180 match expr {
181 VisibilityExpr::Layer(id) => states.contains_key(id),
182 VisibilityExpr::Not(inner) => expr_touches_overrides(inner, states),
183 VisibilityExpr::And(operands) | VisibilityExpr::Or(operands) => {
184 operands.iter().any(|o| expr_touches_overrides(o, states))
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 fn single(id: u32, default_visible: bool) -> OcgVisibility {
194 OcgVisibility::Single {
195 ocg_id: id,
196 default_visible,
197 }
198 }
199
200 #[test]
201 fn empty_layer_set_uses_defaults() {
202 let s = LayerSet::new();
203 assert!(s.evaluate(&single(1, true)));
204 assert!(!s.evaluate(&single(2, false)));
205 }
206
207 #[test]
208 fn override_flips_visibility() {
209 let mut s = LayerSet::new();
210 s.set(1, false);
211 assert!(!s.evaluate(&single(1, true)));
212 s.set(1, true);
213 assert!(s.evaluate(&single(1, true)));
214 s.clear(1);
215 assert!(s.evaluate(&single(1, true)));
216 assert!(s.is_empty());
217 }
218
219 #[test]
220 fn membership_empty_layer_set_returns_default() {
221 // With no overrides, every Membership / Expression returns
222 // its own `default_visible` directly — the OCMD's overall
223 // baked-in default. Policy is irrelevant on this fast path.
224 let s = LayerSet::new();
225 for policy in [
226 MembershipPolicy::AnyOn,
227 MembershipPolicy::AllOn,
228 MembershipPolicy::AllOff,
229 MembershipPolicy::AnyOff,
230 ] {
231 for default in [true, false] {
232 let vis = OcgVisibility::Membership {
233 ocg_ids: vec![1, 2],
234 policy,
235 default_visible: default,
236 };
237 assert_eq!(
238 s.evaluate(&vis),
239 default,
240 "{policy:?} default={default}: empty set should pass through default"
241 );
242 }
243 }
244 }
245
246 #[test]
247 fn membership_overrides_run_policy() {
248 // When at least one leaf is overridden the policy runs;
249 // missing leaves fall back to `default_visible`.
250 let any_on = OcgVisibility::Membership {
251 ocg_ids: vec![1, 2],
252 policy: MembershipPolicy::AnyOn,
253 default_visible: false,
254 };
255 let mut s = LayerSet::new();
256 s.set(2, true);
257 assert!(s.evaluate(&any_on), "leaf 2 ON → AnyOn=true");
258 s.set(1, false);
259 s.set(2, false);
260 assert!(!s.evaluate(&any_on), "all leaves OFF → AnyOn=false");
261
262 let all_on = OcgVisibility::Membership {
263 ocg_ids: vec![1, 2],
264 policy: MembershipPolicy::AllOn,
265 default_visible: true,
266 };
267 let mut s = LayerSet::new();
268 s.set(1, false);
269 // Leaf 1 overridden false; leaf 2 falls back to default_visible=true.
270 assert!(!s.evaluate(&all_on));
271 s.set(2, true);
272 s.set(1, true);
273 assert!(s.evaluate(&all_on));
274 }
275
276 #[test]
277 fn membership_all_off_and_any_off() {
278 let all_off = OcgVisibility::Membership {
279 ocg_ids: vec![1, 2],
280 policy: MembershipPolicy::AllOff,
281 default_visible: false,
282 };
283 let any_off = OcgVisibility::Membership {
284 ocg_ids: vec![1, 2],
285 policy: MembershipPolicy::AnyOff,
286 default_visible: true,
287 };
288
289 // Force-overriding both leaves OFF makes AllOff true.
290 let mut s = LayerSet::new();
291 s.set(1, false);
292 s.set(2, false);
293 assert!(s.evaluate(&all_off));
294
295 // Forcing one leaf OFF makes AnyOff true.
296 let mut s = LayerSet::new();
297 s.set(1, false);
298 assert!(s.evaluate(&any_off));
299 }
300
301 #[test]
302 fn membership_empty_ocgs_always_visible() {
303 let s = LayerSet::new();
304 let vis = OcgVisibility::Membership {
305 ocg_ids: vec![],
306 policy: MembershipPolicy::AllOff,
307 default_visible: false,
308 };
309 assert!(s.evaluate(&vis));
310 }
311
312 #[test]
313 fn expression_truth_table() {
314 // /VE /And [/Layer 1] [/Or [/Layer 2] [/Not [/Layer 3]]]
315 let expr = VisibilityExpr::And(vec![
316 VisibilityExpr::Layer(1),
317 VisibilityExpr::Or(vec![
318 VisibilityExpr::Layer(2),
319 VisibilityExpr::Not(Box::new(VisibilityExpr::Layer(3))),
320 ]),
321 ]);
322 let vis = OcgVisibility::Expression {
323 expr,
324 default_visible: false,
325 };
326
327 for a in [false, true] {
328 for b in [false, true] {
329 for c in [false, true] {
330 let mut s = LayerSet::new();
331 s.set(1, a);
332 s.set(2, b);
333 s.set(3, c);
334 let expected = a && (b || !c);
335 assert_eq!(
336 s.evaluate(&vis),
337 expected,
338 "a={a} b={b} c={c} -> expected {expected}"
339 );
340 }
341 }
342 }
343 }
344
345 #[test]
346 fn rb_group_enforcement() {
347 let mut s = LayerSet::new();
348 s.enforce_rb_group(&[10, 20, 30], 20);
349 assert_eq!(s.get(10), Some(false));
350 assert_eq!(s.get(20), Some(true));
351 assert_eq!(s.get(30), Some(false));
352 }
353}