1use crate::names;
22use pdfrum_common::{DiagKind, Diagnostics, Severity};
23use pdfrum_object::{Array, Dict, Name, Object, Resolve};
24use std::collections::HashMap;
25
26pub const MAX_VE_DEPTH: u32 = 32;
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
34pub enum UsageType {
35 #[default]
37 View,
38 Design,
40 Print,
42 Export,
44}
45
46impl UsageType {
47 #[must_use]
49 pub fn as_bytes(self) -> &'static [u8] {
50 match self {
51 Self::View => b"View",
52 Self::Design => b"Design",
53 Self::Print => b"Print",
54 Self::Export => b"Export",
55 }
56 }
57
58 #[must_use]
60 pub fn state_key(self) -> Name {
61 let mut key = self.as_bytes().to_vec();
62 key.extend_from_slice(b"State");
63 Name::new(key)
64 }
65}
66
67#[derive(Debug)]
74pub struct OcContext {
75 usage: UsageType,
76 properties: Option<Dict>,
78 cache: HashMap<pdfrum_object::ObjRef, bool>,
81}
82
83impl OcContext {
84 #[must_use]
86 pub fn new(properties: Option<Dict>, usage: UsageType) -> Self {
87 Self {
88 usage,
89 properties,
90 cache: HashMap::new(),
91 }
92 }
93
94 #[must_use]
97 pub fn permissive() -> Self {
98 Self::new(None, UsageType::View)
99 }
100
101 pub fn content_visible<R: Resolve>(
107 &mut self,
108 dict: Option<&Dict>,
109 r: &R,
110 diags: &mut Diagnostics,
111 ) -> bool {
112 let Some(dict) = dict else {
113 return true;
114 };
115 if dict
118 .name(names::TYPE)
119 .is_none_or(|t| t.as_bytes() == b"OCG")
120 {
121 return self.group_visible(Some(dict), r);
122 }
123 self.membership_visible(dict, r, diags)
124 }
125
126 pub fn group_visible<R: Resolve>(&mut self, dict: Option<&Dict>, r: &R) -> bool {
131 let Some(dict) = dict else {
132 return false;
133 };
134 let key = dict.reference(&Name::from("__self"));
136 if let Some(id) = key
137 && let Some(hit) = self.cache.get(&id)
138 {
139 return *hit;
140 }
141 let answer = self.load_group_state(dict, r);
142 if let Some(id) = key {
143 self.cache.insert(id, answer);
144 }
145 answer
146 }
147
148 #[must_use]
150 pub fn memoized(&self) -> usize {
151 self.cache.len()
152 }
153
154 fn membership_visible<R: Resolve>(
156 &mut self,
157 ocmd: &Dict,
158 r: &R,
159 diags: &mut Diagnostics,
160 ) -> bool {
161 if let Some(ve) = ocmd.array(names::VE, r) {
163 return self.eval_expression(&ve, r, 0);
164 }
165 let policy = ocmd
166 .byte_string(names::P, r)
167 .unwrap_or_else(|| b"AnyOn".to_vec());
168 let Some(ocgs) = ocmd.get(names::OCGS, r) else {
169 return true;
170 };
171 match &*ocgs {
172 Object::Dict(d) => self.group_visible(Some(d), r),
174 Object::Array(array) => {
175 let state = policy == b"AllOn" || policy == b"AllOff";
177 let mut seen_valid = false;
178 for element in array.iter() {
179 let Some(d) = element.resolve(r).ok().and_then(|o| o.as_dict().cloned()) else {
180 continue;
183 };
184 seen_valid = true;
185 let visible = self.group_visible(Some(&d), r);
186 if (policy == b"AnyOn" && visible) || (policy == b"AnyOff" && !visible) {
187 return true;
188 }
189 if (policy == b"AllOn" && !visible) || (policy == b"AllOff" && visible) {
190 return false;
191 }
192 }
193 if !seen_valid {
194 return true;
195 }
196 if !matches!(
201 policy.as_slice(),
202 b"AnyOn" | b"AllOn" | b"AnyOff" | b"AllOff"
203 ) {
204 diags.record(
205 Severity::Suspicious,
206 DiagKind::OptionalContentPolicyUnknown,
207 None,
208 );
209 }
210 state
211 }
212 _ => true,
213 }
214 }
215
216 fn eval_expression<R: Resolve>(&mut self, expr: &Array, r: &R, depth: u32) -> bool {
221 if depth > MAX_VE_DEPTH {
222 return false;
223 }
224 let operator = expr.byte_string_at(0).unwrap_or_default();
225 match operator.as_slice() {
226 b"Not" => match expr.get(1, r).as_deref() {
227 Some(Object::Dict(d)) => !self.group_visible(Some(d), r),
228 Some(Object::Array(a)) => !self.eval_expression(a, r, depth + 1),
229 _ => false,
230 },
231 b"Or" | b"And" => {
232 let and = operator == b"And";
233 let mut value = false;
234 for i in 1..expr.len() {
235 let operand = expr.get(i, r);
236 let Some(operand) = operand else {
241 continue;
242 };
243 let result = match &*operand {
244 Object::Dict(d) => self.group_visible(Some(d), r),
245 Object::Array(a) => self.eval_expression(a, r, depth + 1),
246 _ => false,
248 };
249 if i == 1 {
250 value = result;
251 } else if and {
252 value = value && result;
253 } else {
254 value = value || result;
255 }
256 }
257 value
258 }
259 _ => false,
260 }
261 }
262
263 fn load_group_state<R: Resolve>(&mut self, ocg: &Dict, r: &R) -> bool {
265 if !has_intent(ocg, b"View", b"View", r) {
268 return true;
269 }
270 if let Some(usage) = ocg.dict(names::USAGE, r) {
271 let state_key = self.usage.state_key();
272 if let Some(entry) = usage.dict(&Name::new(self.usage.as_bytes()), r)
273 && entry.contains_key(&state_key)
274 {
275 return entry.byte_string(&state_key, r).as_deref() != Some(b"OFF");
276 }
277 if self.usage != UsageType::View
279 && let Some(entry) = usage.dict(&Name::from("View"), r)
280 && entry.contains_key(&Name::from("ViewState"))
281 {
282 return entry.byte_string(&Name::from("ViewState"), r).as_deref() != Some(b"OFF");
283 }
284 }
285 self.state_from_config(ocg, r)
286 }
287
288 fn state_from_config<R: Resolve>(&mut self, ocg: &Dict, r: &R) -> bool {
290 let Some(config) = self.select_config(ocg, r) else {
291 return true;
293 };
294 let mut on = config.byte_string(names::BASE_STATE, r).as_deref() != Some(b"OFF");
297 if let Some(array) = config.array(names::ON, r)
298 && contains_dict(&array, ocg, r)
299 {
300 on = true;
301 }
302 if let Some(array) = config.array(names::OFF, r)
304 && contains_dict(&array, ocg, r)
305 {
306 on = false;
307 }
308 if let Some(entries) = config.array(names::AS, r) {
310 for element in entries.iter() {
311 let Some(entry) = element.resolve(r).ok().and_then(|o| o.as_dict().cloned()) else {
312 continue;
313 };
314 let event = entry
316 .byte_string(names::EVENT, r)
317 .unwrap_or_else(|| b"View".to_vec());
318 if event != self.usage.as_bytes() {
319 continue;
320 }
321 let Some(groups) = entry.array(names::OCGS, r) else {
322 continue;
323 };
324 if !contains_dict(&groups, ocg, r) {
325 continue;
326 }
327 let state_key = self.usage.state_key();
328 if let Some(sub) = entry.dict(&Name::new(self.usage.as_bytes()), r) {
329 on = sub.byte_string(&state_key, r).as_deref() != Some(b"OFF");
330 }
331 }
332 }
333 on
334 }
335
336 fn select_config<R: Resolve>(&self, ocg: &Dict, r: &R) -> Option<Dict> {
339 let properties = self.properties.as_ref()?;
340 let all = properties.array(names::OCGS, r)?;
342 if !contains_dict(&all, ocg, r) {
343 return None;
344 }
345 if let Some(configs) = properties.array(names::CONFIGS, r) {
346 for element in configs.iter() {
347 let Some(config) = element.resolve(r).ok().and_then(|o| o.as_dict().cloned())
348 else {
349 continue;
350 };
351 if has_intent(&config, b"View", b"", r) {
354 return Some(config);
355 }
356 }
357 }
358 properties.dict(names::D_CONFIG, r)
359 }
360}
361
362#[derive(Debug, Clone, PartialEq, Eq, Default)]
377pub struct Visibility {
378 nodes: Vec<Node>,
380}
381
382#[derive(Debug, Clone, PartialEq, Eq, Default)]
384struct Node {
385 visible: bool,
387 children: Visibility,
391}
392
393impl Visibility {
394 #[must_use]
396 pub fn all_visible() -> Self {
397 Self::default()
398 }
399
400 #[must_use]
406 pub fn shows_everything(&self) -> bool {
407 self.nodes.is_empty()
408 }
409
410 #[must_use]
418 pub fn visible(&self, index: usize) -> bool {
419 self.nodes.get(index).is_none_or(|n| n.visible)
420 }
421
422 #[must_use]
424 pub fn children(&self, index: usize) -> Self {
425 self.nodes
426 .get(index)
427 .map(|n| n.children.clone())
428 .unwrap_or_default()
429 }
430
431 fn collapsed(self) -> Self {
438 if self
439 .nodes
440 .iter()
441 .all(|n| n.visible && n.children.shows_everything())
442 {
443 Self::default()
444 } else {
445 self
446 }
447 }
448}
449
450#[must_use]
461pub fn page_visibility<R: Resolve>(
462 page: &crate::Page,
463 oc: &mut OcContext,
464 r: &R,
465 diags: &mut Diagnostics,
466) -> Visibility {
467 object_visibility(&page.objects, oc, r, diags)
468}
469
470fn object_visibility<R: Resolve>(
472 objects: &[crate::PageObject],
473 oc: &mut OcContext,
474 r: &R,
475 diags: &mut Diagnostics,
476) -> Visibility {
477 let nodes = objects
478 .iter()
479 .map(|object| {
480 let visible = object_visible(object, oc, r, diags);
481 let children = match object {
482 crate::PageObject::Form(f) if visible => {
483 object_visibility(&f.object.objects, oc, r, diags)
484 }
485 _ => Visibility::default(),
486 };
487 Node { visible, children }
488 })
489 .collect();
490 Visibility { nodes }.collapsed()
491}
492
493fn object_visible<R: Resolve>(
495 object: &crate::PageObject,
496 oc: &mut OcContext,
497 r: &R,
498 diags: &mut Diagnostics,
499) -> bool {
500 if !object
503 .marks()
504 .optional_content_all()
505 .into_iter()
506 .all(|d| oc.content_visible(Some(d), r, diags))
507 {
508 return false;
509 }
510 let own = match object {
514 crate::PageObject::Form(f) => f.object.oc.as_deref(),
515 crate::PageObject::Image(i) => i.object.oc.as_deref(),
516 crate::PageObject::Path(_) | crate::PageObject::Text(_) | crate::PageObject::Shading(_) => {
517 None
518 }
519 };
520 oc.content_visible(own, r, diags)
521}
522
523fn has_intent(dict: &Dict, element: &[u8], default: &[u8], r: &impl Resolve) -> bool {
528 let Some(intent) = dict.get(names::INTENT, r) else {
529 return element == default;
530 };
531 match &*intent {
532 Object::Array(array) => array.iter().any(|o| {
533 let s = o.to_byte_string();
534 s == b"All" || s == element
535 }),
536 other => {
537 let s = other.to_byte_string();
538 s == b"All" || s == element
539 }
540 }
541}
542
543fn contains_dict(array: &Array, target: &Dict, r: &impl Resolve) -> bool {
545 array.iter().any(|o| {
546 o.resolve(r)
547 .ok()
548 .and_then(|res| res.as_dict().cloned())
549 .as_ref()
550 == Some(target)
551 })
552}
553
554#[cfg(test)]
555mod tests {
556 #![allow(
560 clippy::unreadable_literal,
561 clippy::float_cmp,
562 clippy::indexing_slicing,
563 clippy::cast_precision_loss,
564 clippy::cast_possible_truncation,
565 reason = "test fixtures quote oracle vectors verbatim and compare exactly"
566 )]
567
568 use super::{MAX_VE_DEPTH, OcContext, UsageType};
569 use pdfrum_common::{DiagKind, Diagnostics};
570 use pdfrum_object::{Array, Dict, Name, NoResolve, Object};
571
572 fn ocg(name: &str) -> Dict {
573 Dict::from_pairs([
574 (Name::from("Type"), Object::Name(Name::from("OCG"))),
575 (Name::from("Name"), Object::Name(Name::from(name))),
576 ])
577 }
578
579 fn ocmd(pairs: Vec<(Name, Object)>) -> Dict {
580 let mut d = Dict::from_pairs([(Name::from("Type"), Object::Name(Name::from("OCMD")))]);
581 for (k, v) in pairs {
582 d.push(k, v);
583 }
584 d
585 }
586
587 #[test]
588 fn the_null_asymmetry_between_content_and_group_checks() {
589 let mut ctx = OcContext::permissive();
590 let mut diags = Diagnostics::default();
591 assert!(ctx.content_visible(None, &NoResolve, &mut diags));
593 assert!(!ctx.group_visible(None, &NoResolve));
595 }
596
597 #[test]
598 fn a_group_with_no_configuration_is_visible() {
599 let mut ctx = OcContext::permissive();
600 let mut diags = Diagnostics::default();
601 assert!(ctx.content_visible(Some(&ocg("Layer")), &NoResolve, &mut diags));
602 }
603
604 #[test]
605 fn the_four_membership_policies() {
606 let on = ocg("On");
607 let mut diags = Diagnostics::default();
608 for (policy, want) in [
609 ("AnyOn", true),
610 ("AllOn", true),
611 ("AnyOff", false),
612 ("AllOff", false),
613 ] {
614 let mut ctx = OcContext::permissive();
615 let d = ocmd(vec![
616 (Name::from("P"), Object::Name(Name::from(policy))),
617 (
618 Name::from("OCGs"),
619 Object::Array(Array::of([Object::Dict(on.clone())])),
620 ),
621 ]);
622 assert_eq!(
623 ctx.content_visible(Some(&d), &NoResolve, &mut diags),
624 want,
625 "policy {policy}"
626 );
627 }
628 }
629
630 #[test]
631 fn an_unknown_policy_with_a_valid_group_is_invisible() {
632 let mut ctx = OcContext::permissive();
633 let mut diags = Diagnostics::default();
634 let d = ocmd(vec![
635 (Name::from("P"), Object::Name(Name::from("SomeOn"))),
636 (
637 Name::from("OCGs"),
638 Object::Array(Array::of([Object::Dict(ocg("On"))])),
639 ),
640 ]);
641 assert!(!ctx.content_visible(Some(&d), &NoResolve, &mut diags));
642 assert!(diags.contains(&DiagKind::OptionalContentPolicyUnknown));
643 }
644
645 #[test]
646 fn a_membership_with_no_valid_groups_is_visible() {
647 let mut ctx = OcContext::permissive();
648 let mut diags = Diagnostics::default();
649 let d = ocmd(vec![
650 (Name::from("P"), Object::Name(Name::from("AllOn"))),
651 (
652 Name::from("OCGs"),
653 Object::Array(Array::of([Object::Int(7), Object::Null])),
654 ),
655 ]);
656 assert!(ctx.content_visible(Some(&d), &NoResolve, &mut diags));
657 }
658
659 #[test]
660 fn a_single_group_dictionary_ignores_the_policy() {
661 let mut ctx = OcContext::permissive();
662 let mut diags = Diagnostics::default();
663 let d = ocmd(vec![
664 (Name::from("P"), Object::Name(Name::from("AllOff"))),
666 (Name::from("OCGs"), Object::Dict(ocg("On"))),
667 ]);
668 assert!(ctx.content_visible(Some(&d), &NoResolve, &mut diags));
669 }
670
671 #[test]
672 fn a_visibility_expression_takes_precedence_over_the_policy() {
673 let mut ctx = OcContext::permissive();
674 let mut diags = Diagnostics::default();
675 let d = ocmd(vec![
676 (Name::from("P"), Object::Name(Name::from("AnyOn"))),
677 (
678 Name::from("VE"),
679 Object::Array(Array::of([
680 Object::Name(Name::from("Not")),
681 Object::Dict(ocg("On")),
682 ])),
683 ),
684 (
685 Name::from("OCGs"),
686 Object::Array(Array::of([Object::Dict(ocg("On"))])),
687 ),
688 ]);
689 assert!(!ctx.content_visible(Some(&d), &NoResolve, &mut diags));
692 }
693
694 #[test]
695 fn an_unknown_expression_operator_is_invisible() {
696 let mut ctx = OcContext::permissive();
697 let mut diags = Diagnostics::default();
698 let d = ocmd(vec![(
699 Name::from("VE"),
700 Object::Array(Array::of([
701 Object::Name(Name::from("Nand")),
702 Object::Dict(ocg("On")),
703 ])),
704 )]);
705 assert!(!ctx.content_visible(Some(&d), &NoResolve, &mut diags));
706 }
707
708 #[test]
709 fn an_and_whose_first_operand_is_missing_is_false() {
710 let mut ctx = OcContext::permissive();
711 let mut diags = Diagnostics::default();
712 let d = ocmd(vec![(
715 Name::from("VE"),
716 Object::Array(Array::of([
717 Object::Name(Name::from("And")),
718 Object::Null,
719 Object::Dict(ocg("On")),
720 ])),
721 )]);
722 assert!(!ctx.content_visible(Some(&d), &NoResolve, &mut diags));
723
724 let d = ocmd(vec![(
726 Name::from("VE"),
727 Object::Array(Array::of([
728 Object::Name(Name::from("Or")),
729 Object::Null,
730 Object::Dict(ocg("On")),
731 ])),
732 )]);
733 assert!(ctx.content_visible(Some(&d), &NoResolve, &mut diags));
734 }
735
736 #[test]
737 fn expressions_deeper_than_the_cap_are_invisible() {
738 let mut ctx = OcContext::permissive();
739 let mut diags = Diagnostics::default();
740 let mut expr = Object::Dict(ocg("On"));
742 for _ in 0..=MAX_VE_DEPTH + 1 {
743 expr = Object::Array(Array::of([Object::Name(Name::from("Not")), expr]));
744 }
745 let d = ocmd(vec![(Name::from("VE"), expr)]);
746 let _ = ctx.content_visible(Some(&d), &NoResolve, &mut diags);
749 }
750
751 #[test]
752 fn usage_state_keys_are_built_from_the_usage_name() {
753 assert_eq!(UsageType::View.state_key().as_bytes(), b"ViewState");
754 assert_eq!(UsageType::Print.state_key().as_bytes(), b"PrintState");
755 assert_eq!(UsageType::Export.as_bytes(), b"Export");
756 }
757
758 struct Store(std::collections::HashMap<u32, std::sync::Arc<Object>>);
761
762 impl pdfrum_object::Resolve for Store {
763 fn fetch(
764 &self,
765 r: pdfrum_object::ObjRef,
766 ) -> Result<std::sync::Arc<Object>, pdfrum_object::Error> {
767 self.0
768 .get(&r.num)
769 .map(std::sync::Arc::clone)
770 .ok_or(pdfrum_object::Error::UnresolvedRef(r))
771 }
772 }
773
774 #[test]
775 fn a_self_referencing_visibility_expression_terminates() {
776 let selfref = Object::Array(Array::of([
781 Object::Name(Name::from("Not")),
782 Object::Ref(pdfrum_object::ObjRef {
783 num: 1,
784 generation: 0,
785 }),
786 ]));
787 let mut objects = std::collections::HashMap::new();
788 objects.insert(1u32, std::sync::Arc::new(selfref.clone()));
789 let store = Store(objects);
790
791 let mut ctx = OcContext::permissive();
792 let mut diags = Diagnostics::default();
793 let d = ocmd(vec![(Name::from("VE"), selfref)]);
794 let _ = ctx.content_visible(Some(&d), &store, &mut diags);
797
798 let mut objects = std::collections::HashMap::new();
800 objects.insert(
801 1u32,
802 std::sync::Arc::new(Object::Array(Array::of([
803 Object::Name(Name::from("Not")),
804 Object::Ref(pdfrum_object::ObjRef {
805 num: 2,
806 generation: 0,
807 }),
808 ]))),
809 );
810 objects.insert(
811 2u32,
812 std::sync::Arc::new(Object::Array(Array::of([
813 Object::Name(Name::from("Not")),
814 Object::Ref(pdfrum_object::ObjRef {
815 num: 1,
816 generation: 0,
817 }),
818 ]))),
819 );
820 let store = Store(objects);
821 let mut ctx = OcContext::permissive();
822 let d = ocmd(vec![(
823 Name::from("VE"),
824 Object::Ref(pdfrum_object::ObjRef {
825 num: 1,
826 generation: 0,
827 }),
828 )]);
829 let _ = ctx.content_visible(Some(&d), &store, &mut diags);
830 }
831
832 use crate::ops::MarkProperties;
835 use crate::state::ContentMarks;
836 use crate::{Content, PageObject, PathObject};
837
838 fn off_group() -> Dict {
839 Dict::from_pairs([
841 (Name::from("Type"), Object::Name(Name::from("OCG"))),
842 (Name::from("Name"), Object::Name(Name::from("Hidden"))),
843 ])
844 }
845
846 fn context_hiding(off: &Dict) -> OcContext {
852 let properties = Dict::from_pairs([
853 (
854 Name::from("OCGs"),
855 Object::Array(Array::of([Object::Dict(off.clone())])),
856 ),
857 (
858 Name::from("D"),
859 Object::Dict(Dict::from_pairs([(
860 Name::from("OFF"),
861 Object::Array(Array::of([Object::Dict(off.clone())])),
862 )])),
863 ),
864 ]);
865 OcContext::new(Some(properties), UsageType::View)
866 }
867
868 fn marked(oc: Option<&Dict>, from_resources: bool) -> ContentMarks {
871 let mut marks = ContentMarks::new();
872 if let Some(d) = oc {
873 push_oc(&mut marks, d, from_resources);
874 }
875 marks
876 }
877
878 fn push_oc(marks: &mut ContentMarks, dict: &Dict, from_resources: bool) {
879 let properties = if from_resources {
880 MarkProperties::Named(Name::from("MC0"))
881 } else {
882 MarkProperties::Inline(Box::new(dict.clone()))
883 };
884 marks.push_with_properties(Name::from("OC"), &properties, |_| Some(dict.clone()));
885 }
886
887 fn path_with(marks: ContentMarks) -> PageObject {
888 PageObject::Path(Box::new(Content {
889 object: PathObject {
890 path: kurbo::BezPath::new(),
891 matrix: kurbo::Affine::IDENTITY,
892 fill_rule: crate::FillRule::Winding,
893 stroke: false,
894 },
895 state: crate::GraphicsState::default(),
896 marks,
897 content_stream: Some(0),
898 dirty: false,
899 active: true,
900 }))
901 }
902
903 fn page_of(objects: Vec<PageObject>) -> crate::Page {
904 crate::Page {
905 objects,
906 ..crate::Page::empty()
907 }
908 }
909
910 #[test]
911 fn a_page_with_no_optional_content_produces_an_empty_tree() {
912 let page = page_of(vec![path_with(ContentMarks::new()); 3]);
913 let mut ctx = OcContext::permissive();
914 let mut diags = Diagnostics::default();
915 let v = super::page_visibility(&page, &mut ctx, &NoResolve, &mut diags);
916 assert!(
917 v.shows_everything(),
918 "an all-visible page collapses to nothing, so a renderer can skip \
919 the descent entirely"
920 );
921 assert!(v.visible(0));
923 assert!(v.visible(99));
924 }
925
926 #[test]
927 fn an_off_group_hides_the_object_its_mark_encloses() {
928 let off = off_group();
929 let page = page_of(vec![
930 path_with(marked(None, false)),
931 path_with(marked(Some(&off), true)),
932 path_with(marked(None, false)),
933 ]);
934 let mut ctx = context_hiding(&off);
935 let mut diags = Diagnostics::default();
936 let v = super::page_visibility(&page, &mut ctx, &NoResolve, &mut diags);
937 assert!(!v.shows_everything());
938 assert!(v.visible(0));
939 assert!(!v.visible(1), "the marked object is hidden");
940 assert!(v.visible(2));
941 }
942
943 #[test]
944 fn an_inline_property_list_never_hides_anything() {
945 let off = off_group();
949 let page = page_of(vec![path_with(marked(Some(&off), false))]);
950 let mut ctx = context_hiding(&off);
951 let mut diags = Diagnostics::default();
952 let v = super::page_visibility(&page, &mut ctx, &NoResolve, &mut diags);
953 assert!(v.shows_everything());
954 }
955
956 #[test]
957 fn every_enclosing_mark_gets_a_veto_not_just_the_innermost() {
958 let off = off_group();
961 let mut marks = marked(Some(&off), true);
962 push_oc(&mut marks, &ocg("Shown"), true);
963 let page = page_of(vec![path_with(marks)]);
964 let mut ctx = context_hiding(&off);
965 let mut diags = Diagnostics::default();
966 let v = super::page_visibility(&page, &mut ctx, &NoResolve, &mut diags);
967 assert!(!v.visible(0));
968 }
969
970 #[test]
971 fn a_hidden_form_says_nothing_about_children_nobody_reaches() {
972 let off = off_group();
973 let form = PageObject::Form(Box::new(Content {
974 object: crate::FormObject {
975 objects: vec![path_with(ContentMarks::new())],
976 matrix: kurbo::Affine::IDENTITY,
977 bbox: None,
978 transparency: crate::Transparency::default(),
979 oc: Some(std::sync::Arc::new(off.clone())),
980 source: None,
981 live_edit: false,
982 },
983 state: crate::GraphicsState::default(),
984 marks: ContentMarks::new(),
985 content_stream: Some(0),
986 dirty: false,
987 active: true,
988 }));
989 let page = page_of(vec![form]);
990 let mut ctx = context_hiding(&off);
991 let mut diags = Diagnostics::default();
992 let v = super::page_visibility(&page, &mut ctx, &NoResolve, &mut diags);
993 assert!(!v.visible(0), "the form's own `/OC` hides it");
994 assert!(
995 v.children(0).shows_everything(),
996 "and its children are not walked, because nothing reaches them"
997 );
998 }
999
1000 #[test]
1001 fn a_visible_forms_children_are_answered_in_their_own_frame() {
1002 let off = off_group();
1003 let form = PageObject::Form(Box::new(Content {
1004 object: crate::FormObject {
1005 objects: vec![
1006 path_with(ContentMarks::new()),
1007 path_with(marked(Some(&off), true)),
1008 ],
1009 matrix: kurbo::Affine::IDENTITY,
1010 bbox: None,
1011 transparency: crate::Transparency::default(),
1012 oc: None,
1013 source: None,
1014 live_edit: false,
1015 },
1016 state: crate::GraphicsState::default(),
1017 marks: ContentMarks::new(),
1018 content_stream: Some(0),
1019 dirty: false,
1020 active: true,
1021 }));
1022 let page = page_of(vec![form]);
1023 let mut ctx = context_hiding(&off);
1024 let mut diags = Diagnostics::default();
1025 let v = super::page_visibility(&page, &mut ctx, &NoResolve, &mut diags);
1026 assert!(v.visible(0), "the form itself is drawn");
1027 let inner = v.children(0);
1028 assert!(inner.visible(0));
1029 assert!(!inner.visible(1), "but its second child is not");
1030 }
1031}