use std::collections::HashSet;
use lopdf::{Document, Object};
#[derive(Debug, Clone, Default)]
pub struct OcgVisibility {
pub on: HashSet<(u32, u16)>,
pub off: HashSet<(u32, u16)>,
pub default_on: bool,
}
impl OcgVisibility {
pub fn is_visible(&self, id: (u32, u16)) -> bool {
if self.off.contains(&id) {
return false;
}
if self.on.contains(&id) {
return true;
}
self.default_on
}
pub fn is_empty(&self) -> bool {
self.on.is_empty() && self.off.is_empty()
}
}
pub fn load_ocg_visibility(doc: &Document) -> OcgVisibility {
let mut vis = OcgVisibility {
default_on: true,
..OcgVisibility::default()
};
let Ok(catalog) = doc.catalog() else {
return vis;
};
let Ok(oc_props) = catalog.get(b"OCProperties") else {
return vis;
};
let oc_dict = match resolve(doc, oc_props) {
Object::Dictionary(d) => d,
_ => return vis,
};
let mut all_ocgs: HashSet<(u32, u16)> = HashSet::new();
if let Ok(ocgs) = oc_dict.get(b"OCGs") {
collect_refs(doc, ocgs, &mut all_ocgs);
}
let Ok(d_obj) = oc_dict.get(b"D") else {
vis.on = all_ocgs;
return vis;
};
let d_dict = match resolve(doc, d_obj) {
Object::Dictionary(d) => d,
_ => {
vis.on = all_ocgs;
return vis;
}
};
if let Ok(Object::Name(name)) = d_dict.get(b"BaseState") {
match name.as_slice() {
b"OFF" => vis.default_on = false,
_ => vis.default_on = true,
}
}
if let Ok(on_arr) = d_dict.get(b"ON") {
collect_refs(doc, on_arr, &mut vis.on);
}
if let Ok(off_arr) = d_dict.get(b"OFF") {
collect_refs(doc, off_arr, &mut vis.off);
}
if vis.default_on && vis.on.is_empty() {
for id in &all_ocgs {
if !vis.off.contains(id) {
vis.on.insert(*id);
}
}
}
vis
}
fn resolve(doc: &Document, obj: &Object) -> Object {
match obj {
Object::Reference(id) => doc.get_object(*id).cloned().unwrap_or(Object::Null),
other => other.clone(),
}
}
fn collect_refs(doc: &Document, obj: &Object, out: &mut HashSet<(u32, u16)>) {
match obj {
Object::Array(arr) => {
for item in arr {
collect_refs(doc, item, out);
}
}
Object::Reference(id) => {
out.insert(*id);
}
other => {
match resolve(doc, other) {
Object::Array(arr) => {
for item in arr {
collect_refs(doc, &item, out);
}
}
Object::Reference(id) => {
out.insert(id);
}
_ => {}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use lopdf::dictionary;
#[test]
fn empty_doc_all_visible() {
let doc = Document::new();
let vis = load_ocg_visibility(&doc);
assert!(vis.is_visible((1, 0)));
assert!(vis.default_on);
}
#[test]
fn off_list_hides_ocg() {
let mut doc = Document::with_version("1.5");
let ocg_off = doc.add_object(dictionary! {
"Type" => "OCG",
"Name" => Object::string_literal("Hidden"),
});
let ocg_on = doc.add_object(dictionary! {
"Type" => "OCG",
"Name" => Object::string_literal("Shown"),
});
let pages_id = doc.new_object_id();
let pages = dictionary! {
"Type" => "Pages",
"Kids" => Vec::<Object>::new(),
"Count" => 0,
};
doc.objects.insert(pages_id, Object::Dictionary(pages));
let catalog_id = doc.add_object(dictionary! {
"Type" => "Catalog",
"Pages" => pages_id,
"OCProperties" => dictionary! {
"OCGs" => vec![ocg_off.into(), ocg_on.into()],
"D" => dictionary! {
"BaseState" => "ON",
"OFF" => vec![ocg_off.into()],
"ON" => vec![ocg_on.into()],
},
},
});
doc.trailer.set("Root", catalog_id);
let vis = load_ocg_visibility(&doc);
assert!(!vis.is_visible(ocg_off));
assert!(vis.is_visible(ocg_on));
}
}