1use std::collections::HashSet;
18
19use crate::diagnostics::{LocationHint, ParsePhase, Severity, WarningSink};
20use crate::metadata::{decode_pdf_text_string_pub, pdf_string_to_rust_pub};
21use crate::objects::{PdfDict, PdfObj};
22use crate::resolver::Resolver;
23
24#[derive(Debug, Clone)]
26pub struct Layer {
27 pub ocg_id: u32,
31 pub name: String,
33 pub intent: LayerIntent,
36 pub locked: bool,
40 pub usage: LayerUsage,
43 pub creator_info: Option<CreatorInfo>,
47 pub default_visible: bool,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
59#[non_exhaustive]
60pub enum LayerIntent {
61 View,
63 Design,
67 Export,
70 Multiple(Vec<String>),
72 Other(String),
74}
75
76impl LayerIntent {
77 pub(super) fn from_obj(resolver: &Resolver, obj: &PdfObj) -> Self {
79 match resolver.deref(obj).ok().as_ref().unwrap_or(obj) {
80 PdfObj::Name(name) => Self::from_single_name(name),
81 PdfObj::Array(items) => {
82 let names: Vec<String> = items
83 .iter()
84 .filter_map(|o| {
85 let resolved = resolver.deref(o).ok();
86 let bytes = resolved
87 .as_ref()
88 .and_then(|r| r.as_name())
89 .or_else(|| o.as_name())?;
90 Some(String::from_utf8_lossy(bytes).into_owned())
91 })
92 .collect();
93 if names.len() == 1 {
94 Self::from_single_name(names[0].as_bytes())
95 } else {
96 LayerIntent::Multiple(names)
97 }
98 }
99 _ => LayerIntent::View,
100 }
101 }
102
103 fn from_single_name(name: &[u8]) -> Self {
104 match name {
105 b"View" => LayerIntent::View,
106 b"Design" => LayerIntent::Design,
107 b"Export" => LayerIntent::Export,
108 other => LayerIntent::Other(String::from_utf8_lossy(other).into_owned()),
109 }
110 }
111}
112
113#[derive(Debug, Clone, Default)]
120pub struct LayerUsage {
121 pub creator_info: Option<CreatorInfo>,
123 pub language: Option<LanguageUsage>,
125 pub export: Option<ExportUsage>,
127 pub zoom: Option<ZoomUsage>,
130 pub print: Option<PrintUsage>,
133 pub view: Option<ViewUsage>,
135 pub user: Option<UserUsage>,
137 pub page_element: Option<PageElementSubtype>,
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144#[non_exhaustive]
145pub enum UsageState {
146 On,
147 Off,
148}
149
150impl UsageState {
151 fn from_name(name: &[u8]) -> Option<Self> {
152 match name {
153 b"ON" => Some(UsageState::On),
154 b"OFF" => Some(UsageState::Off),
155 _ => None,
156 }
157 }
158}
159
160#[derive(Debug, Clone, Copy)]
162pub struct ViewUsage {
163 pub state: UsageState,
165}
166
167#[derive(Debug, Clone)]
169pub struct PrintUsage {
170 pub subtype: Option<String>,
173 pub state: UsageState,
175}
176
177#[derive(Debug, Clone, Copy)]
179pub struct ExportUsage {
180 pub state: UsageState,
182}
183
184#[derive(Debug, Clone, Copy, Default)]
186pub struct ZoomUsage {
187 pub min: Option<f64>,
190 pub max: Option<f64>,
193}
194
195#[derive(Debug, Clone)]
197pub struct LanguageUsage {
198 pub lang: String,
200 pub preferred: bool,
203}
204
205#[derive(Debug, Clone, Default)]
207pub struct UserUsage {
208 pub user_type: Option<String>,
211 pub names: Vec<String>,
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
218#[non_exhaustive]
219pub enum PageElementSubtype {
220 HeaderFooter,
222 Foreground,
224 Background,
226 Logo,
228}
229
230impl PageElementSubtype {
231 fn from_name(name: &[u8]) -> Option<Self> {
232 match name {
233 b"HF" => Some(PageElementSubtype::HeaderFooter),
234 b"FG" => Some(PageElementSubtype::Foreground),
235 b"BG" => Some(PageElementSubtype::Background),
236 b"L" => Some(PageElementSubtype::Logo),
237 _ => None,
238 }
239 }
240}
241
242#[derive(Debug, Clone)]
244pub struct CreatorInfo {
245 pub creator: String,
247 pub subtype: Option<String>,
250}
251
252pub fn parse_layers(resolver: &Resolver, sink: &WarningSink) -> Vec<Layer> {
263 let Some(catalog) = catalog_dict(resolver) else {
264 return Vec::new();
265 };
266 let Some(oc_props_obj) = catalog.get(b"OCProperties") else {
267 return Vec::new();
268 };
269 let Ok(oc_props) = resolver.deref(oc_props_obj) else {
270 return Vec::new();
271 };
272 let Some(oc_dict) = oc_props.as_dict() else {
273 return Vec::new();
274 };
275
276 let off_set = collect_ocg_id_set(resolver, oc_dict, b"D", b"OFF");
277 let locked_set = collect_ocg_id_set(resolver, oc_dict, b"D", b"Locked");
278
279 let Some(ocgs_obj) = oc_dict.get(b"OCGs") else {
281 return Vec::new();
282 };
283 let ocgs_resolved = match resolver.deref(ocgs_obj) {
284 Ok(o) => o,
285 Err(_) => {
286 sink.record(
287 ParsePhase::Layers,
288 None,
289 Severity::Warning,
290 "/OCProperties /OCGs could not be resolved",
291 );
292 return Vec::new();
293 }
294 };
295 let Some(ocgs) = ocgs_resolved.as_array() else {
296 sink.record(
297 ParsePhase::Layers,
298 None,
299 Severity::Warning,
300 "/OCProperties /OCGs is not an array",
301 );
302 return Vec::new();
303 };
304
305 let mut layers = Vec::with_capacity(ocgs.len());
306 let mut seen = HashSet::new();
307 for entry in ocgs {
308 let Some((num, gen_num)) = entry.as_ref() else {
309 sink.record(
310 ParsePhase::Layers,
311 None,
312 Severity::Warning,
313 "OCG entry is not an indirect reference; skipped",
314 );
315 continue;
316 };
317 if !seen.insert(num) {
318 continue;
319 }
320 match resolver.resolve(num, gen_num) {
321 Ok(obj) => {
322 if let Some(dict) = obj.as_dict() {
323 if let Some(layer) =
324 parse_layer(resolver, dict, num, &off_set, &locked_set, sink)
325 {
326 layers.push(layer);
327 }
328 } else {
329 sink.record(
330 ParsePhase::Layers,
331 Some(LocationHint::Object {
332 obj_num: num,
333 gen_num,
334 }),
335 Severity::Warning,
336 "OCG object is not a dict; skipped",
337 );
338 }
339 }
340 Err(_) => {
341 sink.record(
342 ParsePhase::Layers,
343 Some(LocationHint::Object {
344 obj_num: num,
345 gen_num,
346 }),
347 Severity::Warning,
348 "OCG object could not be resolved; skipped",
349 );
350 }
351 }
352 }
353 layers
354}
355
356pub fn parse_layer(
362 resolver: &Resolver,
363 dict: &PdfDict,
364 ocg_id: u32,
365 off_set: &HashSet<u32>,
366 locked_set: &HashSet<u32>,
367 sink: &WarningSink,
368) -> Option<Layer> {
369 if let Some(type_name) = dict.get_name(b"Type")
370 && type_name != b"OCG"
371 {
372 sink.record(
373 ParsePhase::Layers,
374 Some(LocationHint::Object {
375 obj_num: ocg_id,
376 gen_num: 0,
377 }),
378 Severity::Warning,
379 format!(
380 "OCG dict has unexpected /Type {}; skipped",
381 String::from_utf8_lossy(type_name)
382 ),
383 );
384 return None;
385 }
386
387 let name = dict
388 .get(b"Name")
389 .and_then(pdf_string_to_rust_pub)
390 .unwrap_or_default();
391
392 let intent = dict
393 .get(b"Intent")
394 .map(|obj| LayerIntent::from_obj(resolver, obj))
395 .unwrap_or(LayerIntent::View);
396
397 let usage = dict
398 .get(b"Usage")
399 .and_then(|obj| resolver.deref(obj).ok())
400 .and_then(|resolved| resolved.as_dict().cloned())
401 .map(|d| parse_usage_dict(resolver, &d))
402 .unwrap_or_default();
403
404 let creator_info = dict
405 .get(b"CreatorInfo")
406 .and_then(|obj| resolver.deref(obj).ok())
407 .and_then(|resolved| resolved.as_dict().cloned())
408 .and_then(|d| parse_creator_info(&d));
409
410 Some(Layer {
411 ocg_id,
412 name,
413 intent,
414 locked: locked_set.contains(&ocg_id),
415 usage,
416 creator_info,
417 default_visible: !off_set.contains(&ocg_id),
418 })
419}
420
421pub fn parse_usage_dict(resolver: &Resolver, dict: &PdfDict) -> LayerUsage {
423 LayerUsage {
424 creator_info: dict
425 .get(b"CreatorInfo")
426 .and_then(|obj| resolver.deref(obj).ok())
427 .and_then(|resolved| resolved.as_dict().cloned())
428 .and_then(|d| parse_creator_info(&d)),
429 language: dict
430 .get(b"Language")
431 .and_then(|obj| resolver.deref(obj).ok())
432 .and_then(|resolved| resolved.as_dict().cloned())
433 .and_then(|d| parse_language(&d)),
434 export: dict
435 .get(b"Export")
436 .and_then(|obj| resolver.deref(obj).ok())
437 .and_then(|resolved| resolved.as_dict().cloned())
438 .and_then(|d| parse_export(&d)),
439 zoom: dict
440 .get(b"Zoom")
441 .and_then(|obj| resolver.deref(obj).ok())
442 .and_then(|resolved| resolved.as_dict().cloned())
443 .map(|d| parse_zoom(&d)),
444 print: dict
445 .get(b"Print")
446 .and_then(|obj| resolver.deref(obj).ok())
447 .and_then(|resolved| resolved.as_dict().cloned())
448 .and_then(|d| parse_print(&d)),
449 view: dict
450 .get(b"View")
451 .and_then(|obj| resolver.deref(obj).ok())
452 .and_then(|resolved| resolved.as_dict().cloned())
453 .and_then(|d| parse_view(&d)),
454 user: dict
455 .get(b"User")
456 .and_then(|obj| resolver.deref(obj).ok())
457 .and_then(|resolved| resolved.as_dict().cloned())
458 .map(|d| parse_user(&d)),
459 page_element: dict
460 .get(b"PageElement")
461 .and_then(|obj| resolver.deref(obj).ok())
462 .and_then(|resolved| resolved.as_dict().cloned())
463 .and_then(|d| {
464 d.get_name(b"Subtype")
465 .and_then(PageElementSubtype::from_name)
466 }),
467 }
468}
469
470fn parse_creator_info(dict: &PdfDict) -> Option<CreatorInfo> {
471 let creator = dict.get(b"Creator").and_then(pdf_string_to_rust_pub)?;
472 let subtype = dict
473 .get_name(b"Subtype")
474 .map(|n| String::from_utf8_lossy(n).into_owned());
475 Some(CreatorInfo { creator, subtype })
476}
477
478fn parse_language(dict: &PdfDict) -> Option<LanguageUsage> {
479 let lang = dict.get(b"Lang").and_then(pdf_string_to_rust_pub)?;
480 let preferred = dict.get_name(b"Preferred") == Some(b"ON");
481 Some(LanguageUsage { lang, preferred })
482}
483
484fn parse_export(dict: &PdfDict) -> Option<ExportUsage> {
485 let state = dict
486 .get_name(b"ExportState")
487 .and_then(UsageState::from_name)?;
488 Some(ExportUsage { state })
489}
490
491fn parse_zoom(dict: &PdfDict) -> ZoomUsage {
492 ZoomUsage {
493 min: dict.get_f64(b"min"),
494 max: dict.get_f64(b"max"),
495 }
496}
497
498fn parse_print(dict: &PdfDict) -> Option<PrintUsage> {
499 let state = dict
500 .get_name(b"PrintState")
501 .and_then(UsageState::from_name)?;
502 let subtype = dict
503 .get_name(b"Subtype")
504 .map(|n| String::from_utf8_lossy(n).into_owned());
505 Some(PrintUsage { subtype, state })
506}
507
508fn parse_view(dict: &PdfDict) -> Option<ViewUsage> {
509 let state = dict
510 .get_name(b"ViewState")
511 .and_then(UsageState::from_name)?;
512 Some(ViewUsage { state })
513}
514
515fn parse_user(dict: &PdfDict) -> UserUsage {
516 let user_type = dict
517 .get_name(b"Type")
518 .map(|n| String::from_utf8_lossy(n).into_owned());
519 let names = match dict.get(b"Name") {
520 Some(PdfObj::Str(s)) => vec![decode_pdf_text_string_pub(s)],
521 Some(PdfObj::Array(items)) => items
522 .iter()
523 .filter_map(|o| match o {
524 PdfObj::Str(s) => Some(decode_pdf_text_string_pub(s)),
525 _ => None,
526 })
527 .collect(),
528 _ => Vec::new(),
529 };
530 UserUsage { user_type, names }
531}
532
533fn collect_ocg_id_set(
541 resolver: &Resolver,
542 oc_dict: &PdfDict,
543 config_key: &[u8],
544 set_key: &[u8],
545) -> HashSet<u32> {
546 let mut ids = HashSet::new();
547 let Some(config_obj) = oc_dict.get(config_key) else {
548 return ids;
549 };
550 let Ok(config_resolved) = resolver.deref(config_obj) else {
551 return ids;
552 };
553 let Some(config_dict) = config_resolved.as_dict() else {
554 return ids;
555 };
556 let Some(set_obj) = config_dict.get(set_key) else {
557 return ids;
558 };
559 let Ok(set_resolved) = resolver.deref(set_obj) else {
560 return ids;
561 };
562 if let Some(arr) = set_resolved.as_array() {
563 for o in arr {
564 if let Some((num, _gen)) = o.as_ref() {
565 ids.insert(num);
566 }
567 }
568 }
569 ids
570}
571
572fn catalog_dict(resolver: &Resolver) -> Option<PdfDict> {
573 if let Some((num, gen_num)) = resolver.trailer().get_ref(b"Root")
574 && let Ok(obj) = resolver.resolve(num, gen_num)
575 && let Some(dict) = obj.as_dict()
576 {
577 return Some(dict.clone());
578 }
579 crate::find_catalog(resolver).and_then(|obj| obj.as_dict().cloned())
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585
586 #[test]
587 fn intent_from_single_name() {
588 assert_eq!(LayerIntent::from_single_name(b"View"), LayerIntent::View);
589 assert_eq!(
590 LayerIntent::from_single_name(b"Design"),
591 LayerIntent::Design
592 );
593 assert_eq!(
594 LayerIntent::from_single_name(b"Export"),
595 LayerIntent::Export
596 );
597 match LayerIntent::from_single_name(b"Custom") {
598 LayerIntent::Other(s) => assert_eq!(s, "Custom"),
599 other => panic!("expected Other, got {other:?}"),
600 }
601 }
602
603 #[test]
604 fn page_element_names() {
605 assert_eq!(
606 PageElementSubtype::from_name(b"HF"),
607 Some(PageElementSubtype::HeaderFooter)
608 );
609 assert_eq!(
610 PageElementSubtype::from_name(b"FG"),
611 Some(PageElementSubtype::Foreground)
612 );
613 assert_eq!(
614 PageElementSubtype::from_name(b"BG"),
615 Some(PageElementSubtype::Background)
616 );
617 assert_eq!(
618 PageElementSubtype::from_name(b"L"),
619 Some(PageElementSubtype::Logo)
620 );
621 assert_eq!(PageElementSubtype::from_name(b"Other"), None);
622 }
623
624 #[test]
625 fn usage_state_names() {
626 assert_eq!(UsageState::from_name(b"ON"), Some(UsageState::On));
627 assert_eq!(UsageState::from_name(b"OFF"), Some(UsageState::Off));
628 assert_eq!(UsageState::from_name(b"on"), None);
629 }
630}