1use std::sync::Arc;
6
7use crate::hash::FastSet;
8use crate::object::{Dict, ObjRef, Object};
9use crate::source::AsyncObjectSource;
10
11const MAX_VE_DEPTH: u32 = 8;
15
16#[derive(Debug, Clone, Default, PartialEq, Eq)]
25pub struct OcState {
26 off: FastSet<ObjRef>,
27}
28
29impl OcState {
30 pub async fn load_with<S: AsyncObjectSource>(src: &S, trailer: &Dict) -> Option<OcState> {
36 let root = trailer.get("Root")?;
37 let catalog = src.resolve(root).await.ok()?;
38 let props = src
39 .resolve(catalog.as_dict()?.get("OCProperties")?)
40 .await
41 .ok()?;
42 let props = props.as_dict()?;
43 let config = match props.get("D") {
44 Some(o) => src.resolve(o).await.ok(),
45 None => None,
46 };
47 let config = config.as_ref().and_then(Object::as_dict);
48 let base_off = match config.and_then(|d| d.get("BaseState")) {
49 Some(o) => matches!(
50 src.resolve(o).await.ok().as_ref().and_then(Object::as_name),
51 Some(n) if n.0 == "OFF"
52 ),
53 None => false,
54 };
55 let mut off: FastSet<ObjRef> = FastSet::default();
56 if base_off {
57 off.extend(group_refs(src, props.get("OCGs")).await);
58 }
59 if let Some(config) = config {
60 for group in group_refs(src, config.get("ON")).await {
61 off.remove(&group);
62 }
63 off.extend(group_refs(src, config.get("OFF")).await);
64 }
65 Some(OcState { off })
66 }
67
68 pub fn hidden(&self, group: ObjRef) -> bool {
70 self.off.contains(&group)
71 }
72
73 pub async fn visible_with<S: AsyncObjectSource>(&self, src: &S, value: &Object) -> bool {
81 match value {
82 Object::Ref(r) => {
83 let Ok(resolved) = src.resolve(value).await else {
84 return true;
85 };
86 let Some(dict) = resolved.as_dict() else {
87 return true;
88 };
89 if is_ocmd(dict) {
90 return self.ocmd_visible(src, dict).await;
91 }
92 !self.hidden(*r)
93 }
94 Object::Dict(dict) => {
95 if is_ocmd(dict) {
96 return self.ocmd_visible(src, dict).await;
97 }
98 true
99 }
100 _ => true,
101 }
102 }
103
104 pub async fn props_visible_with<S: AsyncObjectSource>(
110 &self,
111 src: &S,
112 chain: &[Arc<Dict>],
113 props: &Object,
114 ) -> bool {
115 let named;
116 let value = match props {
117 Object::Name(name) => {
118 named = properties_value(src, chain, &name.0).await;
119 match &named {
120 Some(value) => value,
121 None => return true,
122 }
123 }
124 other => other,
125 };
126 self.visible_with(src, value).await
127 }
128
129 async fn ocmd_visible<S: AsyncObjectSource>(&self, src: &S, dict: &Dict) -> bool {
135 if let Some(ve) = dict.get("VE") {
136 let Ok(Object::Array(expr)) = src.resolve(ve).await else {
137 return true;
138 };
139 return self.expression_visible(src, expr).await.unwrap_or(true);
140 }
141 let groups: Vec<ObjRef> = match dict.get("OCGs") {
142 None => return true,
143 Some(indirect @ Object::Ref(r)) => match src.resolve(indirect).await {
144 Ok(Object::Array(items)) => items.iter().filter_map(Object::as_ref).collect(),
145 Ok(Object::Dict(_)) => vec![*r],
146 _ => Vec::new(),
147 },
148 Some(Object::Array(items)) => items.iter().filter_map(Object::as_ref).collect(),
149 Some(_) => Vec::new(),
150 };
151 if groups.is_empty() {
152 return true;
153 }
154 let policy = match dict.get("P") {
155 Some(o) => src
156 .resolve(o)
157 .await
158 .ok()
159 .and_then(|o| o.as_name().map(|n| n.0.clone())),
160 None => None,
161 };
162 match policy.as_deref() {
163 Some("AllOn") => groups.iter().all(|g| !self.hidden(*g)),
164 Some("AnyOff") => groups.iter().any(|g| self.hidden(*g)),
165 Some("AllOff") => groups.iter().all(|g| self.hidden(*g)),
166 _ => groups.iter().any(|g| !self.hidden(*g)),
167 }
168 }
169
170 async fn expression_visible<S: AsyncObjectSource>(
182 &self,
183 src: &S,
184 expr: Vec<Object>,
185 ) -> Option<bool> {
186 let mut stack = vec![VeFrame::new(src, expr).await?];
187 loop {
188 let top = stack.len() - 1;
189 let Some(operand) = stack[top].operands.get(stack[top].next).cloned() else {
190 let done = stack.pop()?;
191 let value = match done.operator.as_str() {
192 "And" => done.all,
193 "Or" => done.any,
194 _ => !done.any,
195 };
196 let Some(parent) = stack.last_mut() else {
197 return Some(value);
198 };
199 parent.fold(value);
200 continue;
201 };
202 stack[top].next += 1;
203 let value = match operand {
204 Object::Array(items) => {
205 if stack.len() > MAX_VE_DEPTH as usize {
206 return None;
207 }
208 stack.push(VeFrame::new(src, items).await?);
209 continue;
210 }
211 Object::Ref(group) => match src.resolve(&operand).await.ok()? {
212 Object::Array(items) => {
213 if stack.len() > MAX_VE_DEPTH as usize {
214 return None;
215 }
216 stack.push(VeFrame::new(src, items).await?);
217 continue;
218 }
219 Object::Dict(_) => !self.hidden(group),
220 _ => return None,
221 },
222 _ => return None,
223 };
224 stack[top].fold(value);
225 }
226 }
227}
228
229struct VeFrame {
232 operator: String,
233 operands: Vec<Object>,
234 next: usize,
235 all: bool,
236 any: bool,
237}
238
239impl VeFrame {
240 async fn new<S: AsyncObjectSource>(src: &S, mut expr: Vec<Object>) -> Option<VeFrame> {
242 let operator = match expr.first()? {
243 Object::Name(n) => n.0.clone(),
244 other => src.resolve(other).await.ok()?.as_name()?.0.clone(),
245 };
246 if !matches!(operator.as_str(), "And" | "Or" | "Not") {
247 return None;
248 }
249 let operands = expr.split_off(1);
250 if operands.is_empty() || (operator == "Not" && operands.len() != 1) {
251 return None;
252 }
253 Some(VeFrame {
254 operator,
255 operands,
256 next: 0,
257 all: true,
258 any: false,
259 })
260 }
261
262 fn fold(&mut self, value: bool) {
264 self.all &= value;
265 self.any |= value;
266 }
267}
268
269fn is_ocmd(dict: &Dict) -> bool {
274 match dict.get_name("Type") {
275 Some(n) => n.0 == "OCMD",
276 None => dict.get("OCGs").is_some() || dict.get("VE").is_some(),
277 }
278}
279
280async fn group_refs<S: AsyncObjectSource>(src: &S, value: Option<&Object>) -> Vec<ObjRef> {
284 let Some(value) = value else {
285 return Vec::new();
286 };
287 let Ok(Object::Array(items)) = src.resolve(value).await else {
288 return Vec::new();
289 };
290 items.iter().filter_map(Object::as_ref).collect()
291}
292
293async fn properties_value<S: AsyncObjectSource>(
297 src: &S,
298 chain: &[Arc<Dict>],
299 name: &str,
300) -> Option<Object> {
301 for res in chain {
302 let Some(cat) = res.get("Properties") else {
303 continue;
304 };
305 let Ok(Object::Dict(dict)) = src.resolve(cat).await else {
306 continue;
307 };
308 let Some(value) = dict.get(name) else {
309 continue;
310 };
311 return Some(value.clone());
312 }
313 None
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319 use crate::{block_on, Document, Immediate, Name};
320 use pdfboss_testkit::PdfBuilder;
321
322 fn gref(num: u32) -> ObjRef {
323 ObjRef { num, gen: 0 }
324 }
325
326 fn doc_with_oc(oc_props: &str, extra: impl FnOnce(&mut PdfBuilder)) -> Document {
330 let mut b = PdfBuilder::new();
331 let oc = if oc_props.is_empty() {
332 String::new()
333 } else {
334 format!(" /OCProperties {oc_props}")
335 };
336 b.object(1, &format!("<< /Type /Catalog /Pages 2 0 R{oc} >>"));
337 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
338 b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
339 b.object(10, "<< /Type /OCG /Name (one) >>");
340 b.object(11, "<< /Type /OCG /Name (two) >>");
341 extra(&mut b);
342 Document::load(b.build(1)).expect("load")
343 }
344
345 fn visible(doc: &Document, state: &OcState, value: &Object) -> bool {
346 block_on(state.visible_with(&Immediate(doc), value))
347 }
348
349 #[test]
350 fn no_ocproperties_means_no_state() {
351 let doc = doc_with_oc("", |_| {});
352 assert_eq!(doc.oc_state(), None);
353 }
354
355 #[test]
356 fn base_state_defaults_on_and_off_hides() {
357 let doc = doc_with_oc("<< /OCGs [10 0 R 11 0 R] /D << /OFF [11 0 R] >> >>", |_| {});
358 let state = doc.oc_state().expect("state");
359 assert!(!state.hidden(gref(10)));
360 assert!(state.hidden(gref(11)));
361 }
362
363 #[test]
364 fn base_state_off_hides_all_but_on() {
365 let doc = doc_with_oc(
366 "<< /OCGs [10 0 R 11 0 R] /D << /BaseState /OFF /ON [10 0 R] >> >>",
367 |_| {},
368 );
369 let state = doc.oc_state().expect("state");
370 assert!(!state.hidden(gref(10)));
371 assert!(state.hidden(gref(11)));
372 }
373
374 #[test]
377 fn off_is_applied_after_on() {
378 let doc = doc_with_oc(
379 "<< /OCGs [10 0 R] /D << /ON [10 0 R] /OFF [10 0 R] >> >>",
380 |_| {},
381 );
382 let state = doc.oc_state().expect("state");
383 assert!(state.hidden(gref(10)));
384 }
385
386 #[test]
389 fn configuration_resolves_indirection() {
390 let doc = doc_with_oc("<< /OCGs 20 0 R /D 21 0 R >>", |b| {
391 b.object(20, "[10 0 R 11 0 R]");
392 b.object(21, "<< /BaseState 22 0 R /ON 23 0 R >>");
393 b.object(22, "/OFF");
394 b.object(23, "[10 0 R]");
395 });
396 let state = doc.oc_state().expect("state");
397 assert!(!state.hidden(gref(10)));
398 assert!(state.hidden(gref(11)));
399 }
400
401 fn split_state() -> (Document, OcState) {
403 let doc = doc_with_oc("<< /OCGs [10 0 R 11 0 R] /D << /OFF [11 0 R] >> >>", |b| {
404 b.object(30, "<< /Type /OCMD /OCGs [10 0 R 11 0 R] >>");
405 b.object(31, "<< /Type /OCMD /OCGs [10 0 R 11 0 R] /P /AllOn >>");
406 b.object(32, "<< /Type /OCMD /OCGs [10 0 R 11 0 R] /P /AnyOff >>");
407 b.object(33, "<< /Type /OCMD /OCGs [10 0 R 11 0 R] /P /AllOff >>");
408 b.object(34, "<< /Type /OCMD /OCGs [11 0 R] >>");
409 b.object(35, "<< /Type /OCMD /OCGs [] /P /AllOff >>");
410 b.object(36, "<< /Type /OCMD /OCGs [null null] /P /AllOff >>");
411 b.object(37, "<< /Type /OCMD /OCGs 11 0 R /P /AnyOn >>");
412 b.object(38, "<< /Type /OCMD /OCGs [11 0 R] /P /Bogus >>");
413 });
414 let state = doc.oc_state().expect("state");
415 (doc, state)
416 }
417
418 #[test]
419 fn group_reference_visibility_follows_the_off_set() {
420 let (doc, state) = split_state();
421 assert!(visible(&doc, &state, &Object::Ref(gref(10))));
422 assert!(!visible(&doc, &state, &Object::Ref(gref(11))));
423 }
424
425 #[test]
426 fn membership_policies_follow_the_specification() {
427 let (doc, state) = split_state();
428 let ocmd = |num| Object::Ref(gref(num));
429 assert!(visible(&doc, &state, &ocmd(30)), "AnyOn default: 10 is on");
430 assert!(!visible(&doc, &state, &ocmd(31)), "AllOn: 11 is off");
431 assert!(visible(&doc, &state, &ocmd(32)), "AnyOff: 11 is off");
432 assert!(!visible(&doc, &state, &ocmd(33)), "AllOff: 10 is on");
433 assert!(
434 !visible(&doc, &state, &ocmd(34)),
435 "AnyOn over one off group"
436 );
437 assert!(visible(&doc, &state, &ocmd(35)), "empty /OCGs is visible");
438 assert!(visible(&doc, &state, &ocmd(36)), "nulls are ignored");
439 assert!(
440 !visible(&doc, &state, &ocmd(37)),
441 "single group by reference"
442 );
443 assert!(
444 !visible(&doc, &state, &ocmd(38)),
445 "unknown policy reads AnyOn"
446 );
447 }
448
449 #[test]
450 fn malformed_values_stay_visible() {
451 let (doc, state) = split_state();
452 assert!(visible(&doc, &state, &Object::Null));
453 assert!(visible(&doc, &state, &Object::Int(3)));
454 assert!(visible(&doc, &state, &Object::Ref(gref(999))), "dangling");
455 let direct_group = Object::Dict({
456 let mut d = Dict::new();
457 d.insert(Name("Type".into()), Object::Name(Name("OCG".into())));
458 d
459 });
460 assert!(
461 visible(&doc, &state, &direct_group),
462 "a direct group dictionary has no identity to be off"
463 );
464 }
465
466 fn ve_doc(ve: &str) -> (Document, OcState) {
467 let doc = doc_with_oc("<< /OCGs [10 0 R 11 0 R] /D << /OFF [11 0 R] >> >>", |b| {
468 b.object(40, &format!("<< /Type /OCMD /OCGs [10 0 R] /VE {ve} >>"));
469 });
470 let state = doc.oc_state().expect("state");
471 (doc, state)
472 }
473
474 fn ve_visible(ve: &str) -> bool {
475 let (doc, state) = ve_doc(ve);
476 visible(&doc, &state, &Object::Ref(gref(40)))
477 }
478
479 #[test]
480 fn visibility_expressions_evaluate() {
481 assert!(ve_visible("[/Not 11 0 R]"), "Not of an off group");
482 assert!(!ve_visible("[/Not 10 0 R]"), "Not of an on group");
483 assert!(!ve_visible("[/And 10 0 R 11 0 R]"));
484 assert!(ve_visible("[/Or 10 0 R 11 0 R]"));
485 assert!(!ve_visible("[/Or 11 0 R 11 0 R]"));
486 assert!(
487 ve_visible("[/Or 11 0 R [/Not 11 0 R]]"),
488 "nested expression"
489 );
490 assert!(
491 !ve_visible("[/And 10 0 R [/Not [/Not 11 0 R]]]"),
492 "double negation"
493 );
494 }
495
496 #[test]
500 fn expression_takes_precedence_over_policy() {
501 assert!(!ve_visible("[/And 11 0 R]"));
502 }
503
504 #[test]
505 fn malformed_expressions_are_visible() {
506 assert!(ve_visible("[]"), "no operator");
507 assert!(ve_visible("[/And]"), "no operands");
508 assert!(ve_visible("[/Not 11 0 R 11 0 R]"), "Not takes one operand");
509 assert!(ve_visible("[/Xor 11 0 R]"), "unknown operator");
510 assert!(ve_visible("[/And 11 0 R (text)]"), "non-group operand");
511 }
512
513 #[test]
514 fn expression_depth_is_capped() {
515 let mut ve = "11 0 R".to_string();
516 for _ in 0..(MAX_VE_DEPTH + 2) {
517 ve = format!("[/Not {ve}]");
518 }
519 assert!(ve_visible(&ve), "past the cap reads as visible");
520 }
521
522 #[test]
525 fn named_properties_keep_group_identity() {
526 let (doc, state) = split_state();
527 let mut properties = Dict::new();
528 properties.insert(Name("On".into()), Object::Ref(gref(10)));
529 properties.insert(Name("Off".into()), Object::Ref(gref(11)));
530 let mut res = Dict::new();
531 res.insert(Name("Properties".into()), Object::Dict(properties));
532 let chain = vec![Arc::new(res)];
533 let src = Immediate(&doc);
534 let named = |name: &str| Object::Name(Name(name.into()));
535 assert!(block_on(state.props_visible_with(
536 &src,
537 &chain,
538 &named("On")
539 )));
540 assert!(!block_on(state.props_visible_with(
541 &src,
542 &chain,
543 &named("Off")
544 )));
545 assert!(
546 block_on(state.props_visible_with(&src, &chain, &named("Nope"))),
547 "an unresolvable name stays visible"
548 );
549 }
550}