use std::collections::{HashMap, HashSet};
use crate::document::PdfDocument;
use crate::object::Object;
pub fn compute_default_off_ocgs(doc: &PdfDocument) -> HashSet<String> {
let mut off = HashSet::new();
let catalog = match doc.catalog() {
Ok(c) => c,
Err(_) => return off,
};
let catalog_dict = match catalog.as_dict() {
Some(d) => d,
None => return off,
};
let oc_props = match resolve_indirect(catalog_dict.get("OCProperties"), doc) {
Some(o) => o,
None => return off,
};
let oc_dict = match oc_props.as_dict() {
Some(d) => d,
None => return off,
};
let d_obj = match resolve_indirect(oc_dict.get("D"), doc) {
Some(o) => o,
None => return off,
};
let d_dict = match d_obj.as_dict() {
Some(d) => d,
None => return off,
};
let base_state = d_dict
.get("BaseState")
.and_then(|o| o.as_name())
.unwrap_or("ON");
let on_set = collect_ocg_name_set(d_dict.get("ON"), doc);
let off_set = collect_ocg_name_set(d_dict.get("OFF"), doc);
if base_state == "OFF" {
let all_ocgs = collect_ocg_name_set(oc_dict.get("OCGs"), doc);
for name in all_ocgs {
if !on_set.contains(&name) {
off.insert(name);
}
}
} else {
off.extend(off_set);
}
off
}
fn resolve_indirect(obj: Option<&Object>, doc: &PdfDocument) -> Option<Object> {
let obj = obj?;
match obj.as_reference() {
Some(r) => doc.load_object(r).ok(),
None => Some(obj.clone()),
}
}
fn collect_ocg_name_set(arr_obj: Option<&Object>, doc: &PdfDocument) -> HashSet<String> {
let mut names = HashSet::new();
let resolved = match resolve_indirect(arr_obj, doc) {
Some(o) => o,
None => return names,
};
let arr = match resolved.as_array() {
Some(a) => a,
None => return names,
};
for item in arr {
let resolved = match item.as_reference() {
Some(r) => match doc.load_object(r) {
Ok(o) => o,
Err(_) => continue,
},
None => item.clone(),
};
if let Some(d) = resolved.as_dict() {
if let Some(Object::Name(n)) = d.get("Name") {
names.insert(n.clone());
} else if let Some(name_obj) = d.get("Name") {
if let Some(b) = name_obj.as_string() {
names.insert(decode_pdf_text_string(b));
}
}
}
}
names
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OcmdPolicy {
AnyOn,
AllOn,
AnyOff,
AllOff,
}
impl OcmdPolicy {
fn from_name(s: &str) -> Self {
match s {
"AllOn" => OcmdPolicy::AllOn,
"AnyOff" => OcmdPolicy::AnyOff,
"AllOff" => OcmdPolicy::AllOff,
_ => OcmdPolicy::AnyOn,
}
}
}
pub fn decode_pdf_text_string(bytes: &[u8]) -> String {
if bytes.len() >= 2 && bytes[0] == 0xFE && bytes[1] == 0xFF {
let utf16_pairs: Vec<u16> = bytes[2..]
.chunks_exact(2)
.map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]))
.collect();
String::from_utf16(&utf16_pairs)
.unwrap_or_else(|_| String::from_utf8_lossy(bytes).to_string())
} else if bytes.len() >= 2 && bytes[0] == 0xFF && bytes[1] == 0xFE {
let utf16_pairs: Vec<u16> = bytes[2..]
.chunks_exact(2)
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
.collect();
String::from_utf16(&utf16_pairs)
.unwrap_or_else(|_| String::from_utf8_lossy(bytes).to_string())
} else {
String::from_utf8(bytes.to_vec()).unwrap_or_else(|_| {
bytes
.iter()
.filter_map(|&b| crate::fonts::font_dict::pdfdoc_encoding_lookup(b))
.collect()
})
}
}
pub fn ocg_name_is_excluded(name_obj: &Object, excluded: &HashSet<String>) -> bool {
if let Some(name_str) = name_obj.as_name() {
return excluded.contains(name_str);
}
if let Some(name_bytes) = name_obj.as_string() {
let name_str = decode_pdf_text_string(name_bytes);
return excluded.contains(&name_str);
}
false
}
pub fn resolve_bdc_properties(
properties: &Object,
resources: Option<&Object>,
doc: Option<&PdfDocument>,
) -> Option<HashMap<String, Object>> {
if let Some(dict) = properties.as_dict() {
return Some(dict.clone());
}
let prop_name = properties.as_name()?;
let resources = resources?;
let doc = doc?;
let res_dict = if let Some(res_ref) = resources.as_reference() {
doc.load_object(res_ref).ok()?
} else {
resources.clone()
};
let res_dict = res_dict.as_dict()?;
let properties_dict_obj = res_dict.get("Properties")?;
let properties_dict = if let Some(r) = properties_dict_obj.as_reference() {
doc.load_object(r).ok()?
} else {
properties_dict_obj.clone()
};
let properties_dict = properties_dict.as_dict()?;
let prop_obj = properties_dict.get(prop_name)?;
let resolved = if let Some(r) = prop_obj.as_reference() {
doc.load_object(r).ok()?
} else {
prop_obj.clone()
};
resolved.as_dict().cloned()
}
fn collect_ocmd_ocg_names(ocgs_obj: &Object, doc: &PdfDocument) -> Vec<Object> {
let refs: Vec<&Object> = if let Some(arr) = ocgs_obj.as_array() {
arr.iter().collect()
} else {
vec![ocgs_obj]
};
let mut names = Vec::with_capacity(refs.len());
for obj in refs {
let resolved = if let Some(r) = obj.as_reference() {
match doc.load_object(r) {
Ok(o) => o,
Err(_) => continue,
}
} else {
obj.clone()
};
if let Some(d) = resolved.as_dict() {
if let Some(name_obj) = d.get("Name") {
names.push(name_obj.clone());
}
}
}
names
}
pub fn check_ocg_excluded(
props_dict: &HashMap<String, Object>,
doc: &PdfDocument,
excluded: &HashSet<String>,
) -> bool {
if let Some(ocg_name) = props_dict.get("Name") {
return ocg_name_is_excluded(ocg_name, excluded);
}
if let Some(Object::Name(t)) = props_dict.get("Type") {
if t == "OCMD" {
if let Some(ve) = props_dict.get("VE") {
return !evaluate_visibility_expression(ve, doc, excluded);
}
let policy = props_dict
.get("P")
.and_then(|o| o.as_name())
.map(OcmdPolicy::from_name)
.unwrap_or(OcmdPolicy::AnyOn);
let names = match props_dict.get("OCGs") {
Some(o) => collect_ocmd_ocg_names(o, doc),
None => return false,
};
return ocmd_is_hidden(&names, policy, excluded);
}
}
false
}
fn evaluate_visibility_expression(
expr: &Object,
doc: &PdfDocument,
excluded: &HashSet<String>,
) -> bool {
fn eval(expr: &Object, doc: &PdfDocument, excluded: &HashSet<String>, depth: u8) -> bool {
if depth > 16 {
return true; }
let resolved = match expr.as_reference() {
Some(r) => doc.load_object(r).unwrap_or_else(|_| expr.clone()),
None => expr.clone(),
};
if let Some(d) = resolved.as_dict() {
if let Some(name) = d.get("Name") {
return !ocg_name_is_excluded(name, excluded);
}
return true;
}
let arr = match resolved.as_array() {
Some(a) => a,
None => return true,
};
let op = match arr.first().and_then(|o| o.as_name()) {
Some(n) => n,
None => return true,
};
match op {
"Not" => !arr
.get(1)
.map(|o| eval(o, doc, excluded, depth + 1))
.unwrap_or(true),
"And" => arr[1..].iter().all(|o| eval(o, doc, excluded, depth + 1)),
"Or" => arr[1..].iter().any(|o| eval(o, doc, excluded, depth + 1)),
_ => true,
}
}
eval(expr, doc, excluded, 0)
}
pub fn resolve_and_check_ocg_excluded(
properties: &Object,
resources: Option<&Object>,
doc: Option<&PdfDocument>,
excluded: &HashSet<String>,
) -> bool {
let props_dict = match resolve_bdc_properties(properties, resources, doc) {
Some(d) => d,
None => return false,
};
match doc {
Some(d) => check_ocg_excluded(&props_dict, d, excluded),
None => {
if let Some(ocg_name) = props_dict.get("Name") {
return ocg_name_is_excluded(ocg_name, excluded);
}
false
},
}
}
pub fn annotation_is_excluded(
oc_obj: &Object,
doc: &PdfDocument,
excluded: &HashSet<String>,
) -> bool {
if excluded.is_empty() {
return false;
}
let resolved = if let Some(r) = oc_obj.as_reference() {
match doc.load_object(r) {
Ok(o) => o,
Err(_) => return false,
}
} else {
oc_obj.clone()
};
let dict = match resolved.as_dict() {
Some(d) => d,
None => return false,
};
check_ocg_excluded(dict, doc, excluded)
}
fn ocmd_is_hidden(ocg_names: &[Object], policy: OcmdPolicy, excluded: &HashSet<String>) -> bool {
if ocg_names.is_empty() {
return false;
}
let mut any_on = false;
let mut any_off = false;
let mut all_on = true;
let mut all_off = true;
for name in ocg_names {
let is_off = ocg_name_is_excluded(name, excluded);
if is_off {
any_off = true;
all_on = false;
} else {
any_on = true;
all_off = false;
}
}
match policy {
OcmdPolicy::AnyOn => !any_on, OcmdPolicy::AllOn => !all_on, OcmdPolicy::AnyOff => !any_off, OcmdPolicy::AllOff => !all_off, }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_utf16be_bom() {
let bytes = b"\xFE\xFF\x00L\x00a\x00y\x00e\x00r";
assert_eq!(decode_pdf_text_string(bytes), "Layer");
}
#[test]
fn decode_utf16le_bom() {
let bytes = b"\xFF\xFEL\x00a\x00y\x00e\x00r\x00";
assert_eq!(decode_pdf_text_string(bytes), "Layer");
}
#[test]
fn decode_utf8_ascii() {
assert_eq!(decode_pdf_text_string(b"Hello"), "Hello");
}
#[test]
fn decode_pdfdoc_fallback() {
assert_eq!(decode_pdf_text_string(&[0x85]), "\u{2013}");
}
#[test]
fn ocg_name_is_excluded_matches_name_token() {
let mut excluded = HashSet::new();
excluded.insert("Watermark".to_string());
assert!(ocg_name_is_excluded(&Object::Name("Watermark".into()), &excluded));
assert!(!ocg_name_is_excluded(&Object::Name("Other".into()), &excluded));
}
#[test]
fn ocg_name_is_excluded_matches_utf16le_string() {
let mut excluded = HashSet::new();
excluded.insert("Layer".to_string());
let bytes: Vec<u8> = b"\xFF\xFEL\x00a\x00y\x00e\x00r\x00".to_vec();
assert!(ocg_name_is_excluded(&Object::String(bytes), &excluded));
}
#[test]
fn policy_default_is_any_on() {
assert_eq!(OcmdPolicy::from_name("nonsense"), OcmdPolicy::AnyOn);
assert_eq!(OcmdPolicy::from_name("AnyOn"), OcmdPolicy::AnyOn);
assert_eq!(OcmdPolicy::from_name("AllOn"), OcmdPolicy::AllOn);
assert_eq!(OcmdPolicy::from_name("AnyOff"), OcmdPolicy::AnyOff);
assert_eq!(OcmdPolicy::from_name("AllOff"), OcmdPolicy::AllOff);
}
fn names(slice: &[&str]) -> Vec<Object> {
slice.iter().map(|s| Object::Name((*s).into())).collect()
}
#[test]
fn policy_any_on_hides_when_all_excluded() {
let mut excluded = HashSet::new();
excluded.insert("A".to_string());
excluded.insert("B".to_string());
assert!(ocmd_is_hidden(&names(&["A", "B"]), OcmdPolicy::AnyOn, &excluded));
assert!(!ocmd_is_hidden(&names(&["A", "C"]), OcmdPolicy::AnyOn, &excluded));
}
#[test]
fn policy_all_on_hides_when_any_excluded() {
let mut excluded = HashSet::new();
excluded.insert("A".to_string());
assert!(ocmd_is_hidden(&names(&["A", "B"]), OcmdPolicy::AllOn, &excluded));
assert!(!ocmd_is_hidden(&names(&["C", "B"]), OcmdPolicy::AllOn, &excluded));
}
#[test]
fn policy_any_off_hides_when_all_on() {
let mut excluded = HashSet::new();
excluded.insert("A".to_string());
assert!(!ocmd_is_hidden(&names(&["A", "B"]), OcmdPolicy::AnyOff, &excluded));
assert!(ocmd_is_hidden(&names(&["B", "C"]), OcmdPolicy::AnyOff, &excluded));
}
#[test]
fn policy_all_off_hides_when_any_on() {
let mut excluded = HashSet::new();
excluded.insert("A".to_string());
excluded.insert("B".to_string());
assert!(ocmd_is_hidden(&names(&["A", "C"]), OcmdPolicy::AllOff, &excluded));
assert!(!ocmd_is_hidden(&names(&["A", "B"]), OcmdPolicy::AllOff, &excluded));
}
#[test]
fn empty_ocgs_is_not_hidden() {
let excluded = HashSet::new();
assert!(!ocmd_is_hidden(&[], OcmdPolicy::AnyOn, &excluded));
assert!(!ocmd_is_hidden(&[], OcmdPolicy::AllOn, &excluded));
assert!(!ocmd_is_hidden(&[], OcmdPolicy::AnyOff, &excluded));
assert!(!ocmd_is_hidden(&[], OcmdPolicy::AllOff, &excluded));
}
}