use crate::diagnostics::{ParsePhase, Severity, WarningSink};
use crate::metadata::pdf_string_to_rust_pub;
use crate::objects::{PdfDict, PdfObj};
use crate::resolver::Resolver;
use super::metadata::LayerIntent;
#[derive(Debug, Clone, Default)]
pub struct LayerTree {
pub nodes: Vec<LayerTreeNode>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum LayerTreeNode {
Layer(u32),
Section {
label: Option<String>,
header_layer: Option<u32>,
children: Vec<LayerTreeNode>,
},
}
#[derive(Debug, Clone)]
pub struct Configuration {
pub index: usize,
pub name: Option<String>,
pub creator: Option<String>,
pub base_state: BaseState,
pub on: Vec<u32>,
pub off: Vec<u32>,
pub intent: LayerIntent,
pub auto_state: Vec<AutoStateRule>,
pub order: LayerTree,
pub list_mode: ListMode,
pub rb_groups: Vec<Vec<u32>>,
pub locked: Vec<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum BaseState {
On,
Off,
Unchanged,
}
impl BaseState {
fn from_name(name: &[u8]) -> Option<Self> {
match name {
b"ON" => Some(BaseState::On),
b"OFF" => Some(BaseState::Off),
b"Unchanged" => Some(BaseState::Unchanged),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum ListMode {
#[default]
AllPages,
VisiblePages,
}
impl ListMode {
fn from_name(name: &[u8]) -> Option<Self> {
match name {
b"AllPages" => Some(ListMode::AllPages),
b"VisiblePages" => Some(ListMode::VisiblePages),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct AutoStateRule {
pub event: AutoStateEvent,
pub categories: Vec<String>,
pub ocgs: Vec<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum AutoStateEvent {
View,
Print,
Export,
}
impl AutoStateEvent {
fn from_name(name: &[u8]) -> Option<Self> {
match name {
b"View" => Some(AutoStateEvent::View),
b"Print" => Some(AutoStateEvent::Print),
b"Export" => Some(AutoStateEvent::Export),
_ => None,
}
}
}
pub fn parse_configurations(resolver: &Resolver, sink: &WarningSink) -> Vec<Configuration> {
let Some(catalog) = catalog_dict(resolver) else {
return Vec::new();
};
let Some(oc_props_obj) = catalog.get(b"OCProperties") else {
return Vec::new();
};
let Ok(oc_props) = resolver.deref(oc_props_obj) else {
return Vec::new();
};
let Some(oc_dict) = oc_props.as_dict() else {
return Vec::new();
};
let mut configs = Vec::new();
if let Some(d_obj) = oc_dict.get(b"D")
&& let Ok(d_resolved) = resolver.deref(d_obj)
&& let Some(d_dict) = d_resolved.as_dict()
{
configs.push(parse_configuration(resolver, d_dict, 0, sink));
} else {
sink.record(
ParsePhase::Layers,
None,
Severity::Warning,
"/OCProperties missing required /D configuration; skipped",
);
}
if let Some(configs_obj) = oc_dict.get(b"Configs")
&& let Ok(configs_resolved) = resolver.deref(configs_obj)
&& let Some(arr) = configs_resolved.as_array()
{
for (i, entry) in arr.iter().enumerate() {
let Ok(resolved) = resolver.deref(entry) else {
sink.record(
ParsePhase::Layers,
None,
Severity::Warning,
format!("/Configs[{i}] could not be resolved; skipped"),
);
continue;
};
let Some(dict) = resolved.as_dict() else {
sink.record(
ParsePhase::Layers,
None,
Severity::Warning,
format!("/Configs[{i}] is not a dict; skipped"),
);
continue;
};
configs.push(parse_configuration(resolver, dict, i + 1, sink));
}
}
configs
}
pub fn parse_configuration(
resolver: &Resolver,
dict: &PdfDict,
index: usize,
sink: &WarningSink,
) -> Configuration {
let name = dict.get(b"Name").and_then(pdf_string_to_rust_pub);
let creator = dict.get(b"Creator").and_then(pdf_string_to_rust_pub);
let base_state = dict
.get_name(b"BaseState")
.and_then(BaseState::from_name)
.unwrap_or(BaseState::On);
let on = collect_ocg_refs(resolver, dict, b"ON");
let off = collect_ocg_refs(resolver, dict, b"OFF");
let locked = collect_ocg_refs(resolver, dict, b"Locked");
let intent = dict
.get(b"Intent")
.map(|obj| LayerIntent::from_obj(resolver, obj))
.unwrap_or(LayerIntent::View);
let order = dict
.get(b"Order")
.and_then(|obj| resolver.deref(obj).ok())
.map(|resolved| parse_order(resolver, &resolved, sink))
.unwrap_or_default();
let list_mode = dict
.get_name(b"ListMode")
.and_then(ListMode::from_name)
.unwrap_or_default();
let auto_state = dict
.get(b"AS")
.and_then(|obj| resolver.deref(obj).ok())
.map(|resolved| parse_auto_state(resolver, &resolved, sink))
.unwrap_or_default();
let rb_groups = dict
.get(b"RBGroups")
.and_then(|obj| resolver.deref(obj).ok())
.map(|resolved| parse_rb_groups(resolver, &resolved))
.unwrap_or_default();
Configuration {
index,
name,
creator,
base_state,
on,
off,
intent,
auto_state,
order,
list_mode,
rb_groups,
locked,
}
}
pub fn parse_order(resolver: &Resolver, obj: &PdfObj, sink: &WarningSink) -> LayerTree {
let Some(arr) = obj.as_array() else {
return LayerTree::default();
};
LayerTree {
nodes: parse_order_nodes(resolver, arr, sink),
}
}
fn parse_order_nodes(
resolver: &Resolver,
items: &[PdfObj],
sink: &WarningSink,
) -> Vec<LayerTreeNode> {
let mut nodes = Vec::new();
let mut i = 0;
while i < items.len() {
let item = &items[i];
let resolved = resolver.deref(item).ok();
let view = resolved.as_ref().unwrap_or(item);
match view {
PdfObj::Str(s) => {
let label = crate::metadata::decode_pdf_text_string_pub(s);
if let Some(next) = items.get(i + 1) {
let next_resolved = resolver.deref(next).ok();
let next_view = next_resolved.as_ref().unwrap_or(next);
if let PdfObj::Array(children_arr) = next_view {
let children = parse_order_nodes(resolver, children_arr, sink);
nodes.push(LayerTreeNode::Section {
label: Some(label),
header_layer: None,
children,
});
i += 2;
continue;
}
}
sink.record(
ParsePhase::Layers,
None,
Severity::Warning,
format!("/Order string label {label:?} not followed by a child array; dropped"),
);
i += 1;
}
PdfObj::Array(children_arr) => {
let children = parse_order_nodes(resolver, children_arr, sink);
nodes.push(LayerTreeNode::Section {
label: None,
header_layer: None,
children,
});
i += 1;
}
PdfObj::Dict(_) => {
let Some((ocg_id, _gen)) = item.as_ref() else {
sink.record(
ParsePhase::Layers,
None,
Severity::Warning,
"/Order layer entry is an inline OCG dict (not a ref); skipped",
);
i += 1;
continue;
};
if let Some(next) = items.get(i + 1) {
let next_resolved = resolver.deref(next).ok();
let next_view = next_resolved.as_ref().unwrap_or(next);
if let PdfObj::Array(children_arr) = next_view {
let children = parse_order_nodes(resolver, children_arr, sink);
nodes.push(LayerTreeNode::Section {
label: None,
header_layer: Some(ocg_id),
children,
});
i += 2;
continue;
}
}
nodes.push(LayerTreeNode::Layer(ocg_id));
i += 1;
}
_ => {
sink.record(
ParsePhase::Layers,
None,
Severity::Warning,
"/Order item is neither layer ref, string, nor array; skipped",
);
i += 1;
}
}
}
nodes
}
pub fn parse_auto_state(
resolver: &Resolver,
obj: &PdfObj,
sink: &WarningSink,
) -> Vec<AutoStateRule> {
let Some(arr) = obj.as_array() else {
return Vec::new();
};
let mut rules = Vec::with_capacity(arr.len());
for entry in arr {
let Ok(resolved) = resolver.deref(entry) else {
continue;
};
let Some(dict) = resolved.as_dict() else {
continue;
};
let Some(event) = dict.get_name(b"Event").and_then(AutoStateEvent::from_name) else {
sink.record(
ParsePhase::Layers,
None,
Severity::Warning,
"/AS rule missing or unknown /Event; skipped",
);
continue;
};
let categories = dict
.get_array(b"Category")
.map(|arr| {
arr.iter()
.filter_map(|o| o.as_name())
.map(|n| String::from_utf8_lossy(n).into_owned())
.collect()
})
.unwrap_or_default();
let ocgs = dict
.get(b"OCGs")
.and_then(|obj| resolver.deref(obj).ok())
.and_then(|resolved| resolved.as_array().map(<[PdfObj]>::to_vec))
.map(|arr| {
arr.iter()
.filter_map(|o| o.as_ref().map(|(n, _)| n))
.collect()
})
.unwrap_or_default();
rules.push(AutoStateRule {
event,
categories,
ocgs,
});
}
rules
}
pub fn parse_rb_groups(resolver: &Resolver, obj: &PdfObj) -> Vec<Vec<u32>> {
let Some(arr) = obj.as_array() else {
return Vec::new();
};
let mut groups = Vec::with_capacity(arr.len());
for entry in arr {
let Ok(resolved) = resolver.deref(entry) else {
continue;
};
let Some(group_arr) = resolved.as_array() else {
continue;
};
let group: Vec<u32> = group_arr
.iter()
.filter_map(|o| o.as_ref().map(|(n, _)| n))
.collect();
if !group.is_empty() {
groups.push(group);
}
}
groups
}
fn collect_ocg_refs(resolver: &Resolver, dict: &PdfDict, key: &[u8]) -> Vec<u32> {
let Some(obj) = dict.get(key) else {
return Vec::new();
};
let Ok(resolved) = resolver.deref(obj) else {
return Vec::new();
};
let Some(arr) = resolved.as_array() else {
return Vec::new();
};
arr.iter()
.filter_map(|o| o.as_ref().map(|(n, _)| n))
.collect()
}
fn catalog_dict(resolver: &Resolver) -> Option<PdfDict> {
if let Some((num, gen_num)) = resolver.trailer().get_ref(b"Root")
&& let Ok(obj) = resolver.resolve(num, gen_num)
&& let Some(dict) = obj.as_dict()
{
return Some(dict.clone());
}
crate::find_catalog(resolver).and_then(|obj| obj.as_dict().cloned())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base_state_names() {
assert_eq!(BaseState::from_name(b"ON"), Some(BaseState::On));
assert_eq!(BaseState::from_name(b"OFF"), Some(BaseState::Off));
assert_eq!(
BaseState::from_name(b"Unchanged"),
Some(BaseState::Unchanged)
);
assert_eq!(BaseState::from_name(b"on"), None);
}
#[test]
fn list_mode_names() {
assert_eq!(ListMode::from_name(b"AllPages"), Some(ListMode::AllPages));
assert_eq!(
ListMode::from_name(b"VisiblePages"),
Some(ListMode::VisiblePages)
);
assert_eq!(ListMode::from_name(b"Other"), None);
}
#[test]
fn auto_state_event_names() {
assert_eq!(
AutoStateEvent::from_name(b"View"),
Some(AutoStateEvent::View)
);
assert_eq!(
AutoStateEvent::from_name(b"Print"),
Some(AutoStateEvent::Print)
);
assert_eq!(
AutoStateEvent::from_name(b"Export"),
Some(AutoStateEvent::Export)
);
assert_eq!(AutoStateEvent::from_name(b"Save"), None);
}
}