1use crate::diagnostics::{ParsePhase, Severity, WarningSink};
18use crate::metadata::pdf_string_to_rust_pub;
19use crate::objects::{PdfDict, PdfObj};
20use crate::resolver::Resolver;
21
22use super::metadata::LayerIntent;
23
24#[derive(Debug, Clone, Default)]
30pub struct LayerTree {
31 pub nodes: Vec<LayerTreeNode>,
33}
34
35#[derive(Debug, Clone)]
37#[non_exhaustive]
38pub enum LayerTreeNode {
39 Layer(u32),
41 Section {
51 label: Option<String>,
52 header_layer: Option<u32>,
53 children: Vec<LayerTreeNode>,
54 },
55}
56
57#[derive(Debug, Clone)]
62pub struct Configuration {
63 pub index: usize,
65 pub name: Option<String>,
67 pub creator: Option<String>,
69 pub base_state: BaseState,
72 pub on: Vec<u32>,
74 pub off: Vec<u32>,
76 pub intent: LayerIntent,
79 pub auto_state: Vec<AutoStateRule>,
82 pub order: LayerTree,
84 pub list_mode: ListMode,
87 pub rb_groups: Vec<Vec<u32>>,
90 pub locked: Vec<u32>,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97#[non_exhaustive]
98pub enum BaseState {
99 On,
101 Off,
103 Unchanged,
106}
107
108impl BaseState {
109 fn from_name(name: &[u8]) -> Option<Self> {
110 match name {
111 b"ON" => Some(BaseState::On),
112 b"OFF" => Some(BaseState::Off),
113 b"Unchanged" => Some(BaseState::Unchanged),
114 _ => None,
115 }
116 }
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
121#[non_exhaustive]
122pub enum ListMode {
123 #[default]
126 AllPages,
127 VisiblePages,
130}
131
132impl ListMode {
133 fn from_name(name: &[u8]) -> Option<Self> {
134 match name {
135 b"AllPages" => Some(ListMode::AllPages),
136 b"VisiblePages" => Some(ListMode::VisiblePages),
137 _ => None,
138 }
139 }
140}
141
142#[derive(Debug, Clone)]
144pub struct AutoStateRule {
145 pub event: AutoStateEvent,
147 pub categories: Vec<String>,
149 pub ocgs: Vec<u32>,
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155#[non_exhaustive]
156pub enum AutoStateEvent {
157 View,
158 Print,
159 Export,
160}
161
162impl AutoStateEvent {
163 fn from_name(name: &[u8]) -> Option<Self> {
164 match name {
165 b"View" => Some(AutoStateEvent::View),
166 b"Print" => Some(AutoStateEvent::Print),
167 b"Export" => Some(AutoStateEvent::Export),
168 _ => None,
169 }
170 }
171}
172
173pub fn parse_configurations(resolver: &Resolver, sink: &WarningSink) -> Vec<Configuration> {
180 let Some(catalog) = catalog_dict(resolver) else {
181 return Vec::new();
182 };
183 let Some(oc_props_obj) = catalog.get(b"OCProperties") else {
184 return Vec::new();
185 };
186 let Ok(oc_props) = resolver.deref(oc_props_obj) else {
187 return Vec::new();
188 };
189 let Some(oc_dict) = oc_props.as_dict() else {
190 return Vec::new();
191 };
192
193 let mut configs = Vec::new();
194
195 if let Some(d_obj) = oc_dict.get(b"D")
197 && let Ok(d_resolved) = resolver.deref(d_obj)
198 && let Some(d_dict) = d_resolved.as_dict()
199 {
200 configs.push(parse_configuration(resolver, d_dict, 0, sink));
201 } else {
202 sink.record(
203 ParsePhase::Layers,
204 None,
205 Severity::Warning,
206 "/OCProperties missing required /D configuration; skipped",
207 );
208 }
209
210 if let Some(configs_obj) = oc_dict.get(b"Configs")
212 && let Ok(configs_resolved) = resolver.deref(configs_obj)
213 && let Some(arr) = configs_resolved.as_array()
214 {
215 for (i, entry) in arr.iter().enumerate() {
216 let Ok(resolved) = resolver.deref(entry) else {
217 sink.record(
218 ParsePhase::Layers,
219 None,
220 Severity::Warning,
221 format!("/Configs[{i}] could not be resolved; skipped"),
222 );
223 continue;
224 };
225 let Some(dict) = resolved.as_dict() else {
226 sink.record(
227 ParsePhase::Layers,
228 None,
229 Severity::Warning,
230 format!("/Configs[{i}] is not a dict; skipped"),
231 );
232 continue;
233 };
234 configs.push(parse_configuration(resolver, dict, i + 1, sink));
235 }
236 }
237
238 configs
239}
240
241pub fn parse_configuration(
244 resolver: &Resolver,
245 dict: &PdfDict,
246 index: usize,
247 sink: &WarningSink,
248) -> Configuration {
249 let name = dict.get(b"Name").and_then(pdf_string_to_rust_pub);
250 let creator = dict.get(b"Creator").and_then(pdf_string_to_rust_pub);
251
252 let base_state = dict
253 .get_name(b"BaseState")
254 .and_then(BaseState::from_name)
255 .unwrap_or(BaseState::On);
256
257 let on = collect_ocg_refs(resolver, dict, b"ON");
258 let off = collect_ocg_refs(resolver, dict, b"OFF");
259 let locked = collect_ocg_refs(resolver, dict, b"Locked");
260
261 let intent = dict
262 .get(b"Intent")
263 .map(|obj| LayerIntent::from_obj(resolver, obj))
264 .unwrap_or(LayerIntent::View);
265
266 let order = dict
267 .get(b"Order")
268 .and_then(|obj| resolver.deref(obj).ok())
269 .map(|resolved| parse_order(resolver, &resolved, sink))
270 .unwrap_or_default();
271
272 let list_mode = dict
273 .get_name(b"ListMode")
274 .and_then(ListMode::from_name)
275 .unwrap_or_default();
276
277 let auto_state = dict
278 .get(b"AS")
279 .and_then(|obj| resolver.deref(obj).ok())
280 .map(|resolved| parse_auto_state(resolver, &resolved, sink))
281 .unwrap_or_default();
282
283 let rb_groups = dict
284 .get(b"RBGroups")
285 .and_then(|obj| resolver.deref(obj).ok())
286 .map(|resolved| parse_rb_groups(resolver, &resolved))
287 .unwrap_or_default();
288
289 Configuration {
290 index,
291 name,
292 creator,
293 base_state,
294 on,
295 off,
296 intent,
297 auto_state,
298 order,
299 list_mode,
300 rb_groups,
301 locked,
302 }
303}
304
305pub fn parse_order(resolver: &Resolver, obj: &PdfObj, sink: &WarningSink) -> LayerTree {
317 let Some(arr) = obj.as_array() else {
318 return LayerTree::default();
319 };
320 LayerTree {
321 nodes: parse_order_nodes(resolver, arr, sink),
322 }
323}
324
325fn parse_order_nodes(
326 resolver: &Resolver,
327 items: &[PdfObj],
328 sink: &WarningSink,
329) -> Vec<LayerTreeNode> {
330 let mut nodes = Vec::new();
331 let mut i = 0;
332 while i < items.len() {
333 let item = &items[i];
334
335 let resolved = resolver.deref(item).ok();
338 let view = resolved.as_ref().unwrap_or(item);
339
340 match view {
341 PdfObj::Str(s) => {
344 let label = crate::metadata::decode_pdf_text_string_pub(s);
345 if let Some(next) = items.get(i + 1) {
346 let next_resolved = resolver.deref(next).ok();
347 let next_view = next_resolved.as_ref().unwrap_or(next);
348 if let PdfObj::Array(children_arr) = next_view {
349 let children = parse_order_nodes(resolver, children_arr, sink);
350 nodes.push(LayerTreeNode::Section {
351 label: Some(label),
352 header_layer: None,
353 children,
354 });
355 i += 2;
356 continue;
357 }
358 }
359 sink.record(
360 ParsePhase::Layers,
361 None,
362 Severity::Warning,
363 format!("/Order string label {label:?} not followed by a child array; dropped"),
364 );
365 i += 1;
366 }
367
368 PdfObj::Array(children_arr) => {
370 let children = parse_order_nodes(resolver, children_arr, sink);
371 nodes.push(LayerTreeNode::Section {
372 label: None,
373 header_layer: None,
374 children,
375 });
376 i += 1;
377 }
378
379 PdfObj::Dict(_) => {
383 let Some((ocg_id, _gen)) = item.as_ref() else {
384 sink.record(
385 ParsePhase::Layers,
386 None,
387 Severity::Warning,
388 "/Order layer entry is an inline OCG dict (not a ref); skipped",
389 );
390 i += 1;
391 continue;
392 };
393 if let Some(next) = items.get(i + 1) {
394 let next_resolved = resolver.deref(next).ok();
395 let next_view = next_resolved.as_ref().unwrap_or(next);
396 if let PdfObj::Array(children_arr) = next_view {
397 let children = parse_order_nodes(resolver, children_arr, sink);
398 nodes.push(LayerTreeNode::Section {
399 label: None,
400 header_layer: Some(ocg_id),
401 children,
402 });
403 i += 2;
404 continue;
405 }
406 }
407 nodes.push(LayerTreeNode::Layer(ocg_id));
408 i += 1;
409 }
410
411 _ => {
412 sink.record(
413 ParsePhase::Layers,
414 None,
415 Severity::Warning,
416 "/Order item is neither layer ref, string, nor array; skipped",
417 );
418 i += 1;
419 }
420 }
421 }
422 nodes
423}
424
425pub fn parse_auto_state(
427 resolver: &Resolver,
428 obj: &PdfObj,
429 sink: &WarningSink,
430) -> Vec<AutoStateRule> {
431 let Some(arr) = obj.as_array() else {
432 return Vec::new();
433 };
434 let mut rules = Vec::with_capacity(arr.len());
435 for entry in arr {
436 let Ok(resolved) = resolver.deref(entry) else {
437 continue;
438 };
439 let Some(dict) = resolved.as_dict() else {
440 continue;
441 };
442 let Some(event) = dict.get_name(b"Event").and_then(AutoStateEvent::from_name) else {
443 sink.record(
444 ParsePhase::Layers,
445 None,
446 Severity::Warning,
447 "/AS rule missing or unknown /Event; skipped",
448 );
449 continue;
450 };
451
452 let categories = dict
453 .get_array(b"Category")
454 .map(|arr| {
455 arr.iter()
456 .filter_map(|o| o.as_name())
457 .map(|n| String::from_utf8_lossy(n).into_owned())
458 .collect()
459 })
460 .unwrap_or_default();
461
462 let ocgs = dict
463 .get(b"OCGs")
464 .and_then(|obj| resolver.deref(obj).ok())
465 .and_then(|resolved| resolved.as_array().map(<[PdfObj]>::to_vec))
466 .map(|arr| {
467 arr.iter()
468 .filter_map(|o| o.as_ref().map(|(n, _)| n))
469 .collect()
470 })
471 .unwrap_or_default();
472
473 rules.push(AutoStateRule {
474 event,
475 categories,
476 ocgs,
477 });
478 }
479 rules
480}
481
482pub fn parse_rb_groups(resolver: &Resolver, obj: &PdfObj) -> Vec<Vec<u32>> {
485 let Some(arr) = obj.as_array() else {
486 return Vec::new();
487 };
488 let mut groups = Vec::with_capacity(arr.len());
489 for entry in arr {
490 let Ok(resolved) = resolver.deref(entry) else {
491 continue;
492 };
493 let Some(group_arr) = resolved.as_array() else {
494 continue;
495 };
496 let group: Vec<u32> = group_arr
497 .iter()
498 .filter_map(|o| o.as_ref().map(|(n, _)| n))
499 .collect();
500 if !group.is_empty() {
501 groups.push(group);
502 }
503 }
504 groups
505}
506
507fn collect_ocg_refs(resolver: &Resolver, dict: &PdfDict, key: &[u8]) -> Vec<u32> {
510 let Some(obj) = dict.get(key) else {
511 return Vec::new();
512 };
513 let Ok(resolved) = resolver.deref(obj) else {
514 return Vec::new();
515 };
516 let Some(arr) = resolved.as_array() else {
517 return Vec::new();
518 };
519 arr.iter()
520 .filter_map(|o| o.as_ref().map(|(n, _)| n))
521 .collect()
522}
523
524fn catalog_dict(resolver: &Resolver) -> Option<PdfDict> {
525 if let Some((num, gen_num)) = resolver.trailer().get_ref(b"Root")
526 && let Ok(obj) = resolver.resolve(num, gen_num)
527 && let Some(dict) = obj.as_dict()
528 {
529 return Some(dict.clone());
530 }
531 crate::find_catalog(resolver).and_then(|obj| obj.as_dict().cloned())
532}
533
534#[cfg(test)]
535mod tests {
536 use super::*;
537
538 #[test]
539 fn base_state_names() {
540 assert_eq!(BaseState::from_name(b"ON"), Some(BaseState::On));
541 assert_eq!(BaseState::from_name(b"OFF"), Some(BaseState::Off));
542 assert_eq!(
543 BaseState::from_name(b"Unchanged"),
544 Some(BaseState::Unchanged)
545 );
546 assert_eq!(BaseState::from_name(b"on"), None);
547 }
548
549 #[test]
550 fn list_mode_names() {
551 assert_eq!(ListMode::from_name(b"AllPages"), Some(ListMode::AllPages));
552 assert_eq!(
553 ListMode::from_name(b"VisiblePages"),
554 Some(ListMode::VisiblePages)
555 );
556 assert_eq!(ListMode::from_name(b"Other"), None);
557 }
558
559 #[test]
560 fn auto_state_event_names() {
561 assert_eq!(
562 AutoStateEvent::from_name(b"View"),
563 Some(AutoStateEvent::View)
564 );
565 assert_eq!(
566 AutoStateEvent::from_name(b"Print"),
567 Some(AutoStateEvent::Print)
568 );
569 assert_eq!(
570 AutoStateEvent::from_name(b"Export"),
571 Some(AutoStateEvent::Export)
572 );
573 assert_eq!(AutoStateEvent::from_name(b"Save"), None);
574 }
575}