edgeparse_core/pdf/
ocg.rs1use std::collections::HashSet;
7
8use lopdf::{Document, Object};
9
10#[derive(Debug, Clone, Default)]
12pub struct OcgVisibility {
13 pub on: HashSet<(u32, u16)>,
15 pub off: HashSet<(u32, u16)>,
17 pub default_on: bool,
20}
21
22impl OcgVisibility {
23 pub fn is_visible(&self, id: (u32, u16)) -> bool {
25 if self.off.contains(&id) {
26 return false;
27 }
28 if self.on.contains(&id) {
29 return true;
30 }
31 self.default_on
32 }
33
34 pub fn is_empty(&self) -> bool {
36 self.on.is_empty() && self.off.is_empty()
37 }
38}
39
40pub fn load_ocg_visibility(doc: &Document) -> OcgVisibility {
42 let mut vis = OcgVisibility {
43 default_on: true,
44 ..OcgVisibility::default()
45 };
46
47 let Ok(catalog) = doc.catalog() else {
48 return vis;
49 };
50 let Ok(oc_props) = catalog.get(b"OCProperties") else {
51 return vis;
52 };
53 let oc_dict = match resolve(doc, oc_props) {
54 Object::Dictionary(d) => d,
55 _ => return vis,
56 };
57
58 let mut all_ocgs: HashSet<(u32, u16)> = HashSet::new();
60 if let Ok(ocgs) = oc_dict.get(b"OCGs") {
61 collect_refs(doc, ocgs, &mut all_ocgs);
62 }
63
64 let Ok(d_obj) = oc_dict.get(b"D") else {
65 vis.on = all_ocgs;
67 return vis;
68 };
69 let d_dict = match resolve(doc, d_obj) {
70 Object::Dictionary(d) => d,
71 _ => {
72 vis.on = all_ocgs;
73 return vis;
74 }
75 };
76
77 if let Ok(Object::Name(name)) = d_dict.get(b"BaseState") {
79 match name.as_slice() {
80 b"OFF" => vis.default_on = false,
81 _ => vis.default_on = true,
82 }
83 }
84
85 if let Ok(on_arr) = d_dict.get(b"ON") {
86 collect_refs(doc, on_arr, &mut vis.on);
87 }
88 if let Ok(off_arr) = d_dict.get(b"OFF") {
89 collect_refs(doc, off_arr, &mut vis.off);
90 }
91
92 if vis.default_on && vis.on.is_empty() {
94 for id in &all_ocgs {
95 if !vis.off.contains(id) {
96 vis.on.insert(*id);
97 }
98 }
99 }
100
101 vis
102}
103
104fn resolve(doc: &Document, obj: &Object) -> Object {
105 match obj {
106 Object::Reference(id) => doc.get_object(*id).cloned().unwrap_or(Object::Null),
107 other => other.clone(),
108 }
109}
110
111fn collect_refs(doc: &Document, obj: &Object, out: &mut HashSet<(u32, u16)>) {
112 match obj {
113 Object::Array(arr) => {
114 for item in arr {
115 collect_refs(doc, item, out);
116 }
117 }
118 Object::Reference(id) => {
119 out.insert(*id);
120 }
121 other => {
122 match resolve(doc, other) {
124 Object::Array(arr) => {
125 for item in arr {
126 collect_refs(doc, &item, out);
127 }
128 }
129 Object::Reference(id) => {
130 out.insert(id);
131 }
132 _ => {}
133 }
134 }
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use lopdf::dictionary;
142
143 #[test]
144 fn empty_doc_all_visible() {
145 let doc = Document::new();
146 let vis = load_ocg_visibility(&doc);
147 assert!(vis.is_visible((1, 0)));
148 assert!(vis.default_on);
149 }
150
151 #[test]
152 fn off_list_hides_ocg() {
153 let mut doc = Document::with_version("1.5");
154 let ocg_off = doc.add_object(dictionary! {
155 "Type" => "OCG",
156 "Name" => Object::string_literal("Hidden"),
157 });
158 let ocg_on = doc.add_object(dictionary! {
159 "Type" => "OCG",
160 "Name" => Object::string_literal("Shown"),
161 });
162 let pages_id = doc.new_object_id();
163 let pages = dictionary! {
164 "Type" => "Pages",
165 "Kids" => Vec::<Object>::new(),
166 "Count" => 0,
167 };
168 doc.objects.insert(pages_id, Object::Dictionary(pages));
169
170 let catalog_id = doc.add_object(dictionary! {
171 "Type" => "Catalog",
172 "Pages" => pages_id,
173 "OCProperties" => dictionary! {
174 "OCGs" => vec![ocg_off.into(), ocg_on.into()],
175 "D" => dictionary! {
176 "BaseState" => "ON",
177 "OFF" => vec![ocg_off.into()],
178 "ON" => vec![ocg_on.into()],
179 },
180 },
181 });
182 doc.trailer.set("Root", catalog_id);
183
184 let vis = load_ocg_visibility(&doc);
185 assert!(!vis.is_visible(ocg_off));
186 assert!(vis.is_visible(ocg_on));
187 }
188}