Skip to main content

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}