use std::collections::HashSet;
use crate::diagnostics::{LocationHint, ParsePhase, Severity, WarningSink};
use crate::metadata::{decode_pdf_text_string_pub, pdf_string_to_rust_pub};
use crate::objects::{PdfDict, PdfObj};
use crate::resolver::Resolver;
#[derive(Debug, Clone)]
pub struct Layer {
pub ocg_id: u32,
pub name: String,
pub intent: LayerIntent,
pub locked: bool,
pub usage: LayerUsage,
pub creator_info: Option<CreatorInfo>,
pub default_visible: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum LayerIntent {
View,
Design,
Export,
Multiple(Vec<String>),
Other(String),
}
impl LayerIntent {
pub(super) fn from_obj(resolver: &Resolver, obj: &PdfObj) -> Self {
match resolver.deref(obj).ok().as_ref().unwrap_or(obj) {
PdfObj::Name(name) => Self::from_single_name(name),
PdfObj::Array(items) => {
let names: Vec<String> = items
.iter()
.filter_map(|o| {
let resolved = resolver.deref(o).ok();
let bytes = resolved
.as_ref()
.and_then(|r| r.as_name())
.or_else(|| o.as_name())?;
Some(String::from_utf8_lossy(bytes).into_owned())
})
.collect();
if names.len() == 1 {
Self::from_single_name(names[0].as_bytes())
} else {
LayerIntent::Multiple(names)
}
}
_ => LayerIntent::View,
}
}
fn from_single_name(name: &[u8]) -> Self {
match name {
b"View" => LayerIntent::View,
b"Design" => LayerIntent::Design,
b"Export" => LayerIntent::Export,
other => LayerIntent::Other(String::from_utf8_lossy(other).into_owned()),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct LayerUsage {
pub creator_info: Option<CreatorInfo>,
pub language: Option<LanguageUsage>,
pub export: Option<ExportUsage>,
pub zoom: Option<ZoomUsage>,
pub print: Option<PrintUsage>,
pub view: Option<ViewUsage>,
pub user: Option<UserUsage>,
pub page_element: Option<PageElementSubtype>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum UsageState {
On,
Off,
}
impl UsageState {
fn from_name(name: &[u8]) -> Option<Self> {
match name {
b"ON" => Some(UsageState::On),
b"OFF" => Some(UsageState::Off),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ViewUsage {
pub state: UsageState,
}
#[derive(Debug, Clone)]
pub struct PrintUsage {
pub subtype: Option<String>,
pub state: UsageState,
}
#[derive(Debug, Clone, Copy)]
pub struct ExportUsage {
pub state: UsageState,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ZoomUsage {
pub min: Option<f64>,
pub max: Option<f64>,
}
#[derive(Debug, Clone)]
pub struct LanguageUsage {
pub lang: String,
pub preferred: bool,
}
#[derive(Debug, Clone, Default)]
pub struct UserUsage {
pub user_type: Option<String>,
pub names: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PageElementSubtype {
HeaderFooter,
Foreground,
Background,
Logo,
}
impl PageElementSubtype {
fn from_name(name: &[u8]) -> Option<Self> {
match name {
b"HF" => Some(PageElementSubtype::HeaderFooter),
b"FG" => Some(PageElementSubtype::Foreground),
b"BG" => Some(PageElementSubtype::Background),
b"L" => Some(PageElementSubtype::Logo),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct CreatorInfo {
pub creator: String,
pub subtype: Option<String>,
}
pub fn parse_layers(resolver: &Resolver, sink: &WarningSink) -> Vec<Layer> {
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 off_set = collect_ocg_id_set(resolver, oc_dict, b"D", b"OFF");
let locked_set = collect_ocg_id_set(resolver, oc_dict, b"D", b"Locked");
let Some(ocgs_obj) = oc_dict.get(b"OCGs") else {
return Vec::new();
};
let ocgs_resolved = match resolver.deref(ocgs_obj) {
Ok(o) => o,
Err(_) => {
sink.record(
ParsePhase::Layers,
None,
Severity::Warning,
"/OCProperties /OCGs could not be resolved",
);
return Vec::new();
}
};
let Some(ocgs) = ocgs_resolved.as_array() else {
sink.record(
ParsePhase::Layers,
None,
Severity::Warning,
"/OCProperties /OCGs is not an array",
);
return Vec::new();
};
let mut layers = Vec::with_capacity(ocgs.len());
let mut seen = HashSet::new();
for entry in ocgs {
let Some((num, gen_num)) = entry.as_ref() else {
sink.record(
ParsePhase::Layers,
None,
Severity::Warning,
"OCG entry is not an indirect reference; skipped",
);
continue;
};
if !seen.insert(num) {
continue;
}
match resolver.resolve(num, gen_num) {
Ok(obj) => {
if let Some(dict) = obj.as_dict() {
if let Some(layer) =
parse_layer(resolver, dict, num, &off_set, &locked_set, sink)
{
layers.push(layer);
}
} else {
sink.record(
ParsePhase::Layers,
Some(LocationHint::Object {
obj_num: num,
gen_num,
}),
Severity::Warning,
"OCG object is not a dict; skipped",
);
}
}
Err(_) => {
sink.record(
ParsePhase::Layers,
Some(LocationHint::Object {
obj_num: num,
gen_num,
}),
Severity::Warning,
"OCG object could not be resolved; skipped",
);
}
}
}
layers
}
pub fn parse_layer(
resolver: &Resolver,
dict: &PdfDict,
ocg_id: u32,
off_set: &HashSet<u32>,
locked_set: &HashSet<u32>,
sink: &WarningSink,
) -> Option<Layer> {
if let Some(type_name) = dict.get_name(b"Type")
&& type_name != b"OCG"
{
sink.record(
ParsePhase::Layers,
Some(LocationHint::Object {
obj_num: ocg_id,
gen_num: 0,
}),
Severity::Warning,
format!(
"OCG dict has unexpected /Type {}; skipped",
String::from_utf8_lossy(type_name)
),
);
return None;
}
let name = dict
.get(b"Name")
.and_then(pdf_string_to_rust_pub)
.unwrap_or_default();
let intent = dict
.get(b"Intent")
.map(|obj| LayerIntent::from_obj(resolver, obj))
.unwrap_or(LayerIntent::View);
let usage = dict
.get(b"Usage")
.and_then(|obj| resolver.deref(obj).ok())
.and_then(|resolved| resolved.as_dict().cloned())
.map(|d| parse_usage_dict(resolver, &d))
.unwrap_or_default();
let creator_info = dict
.get(b"CreatorInfo")
.and_then(|obj| resolver.deref(obj).ok())
.and_then(|resolved| resolved.as_dict().cloned())
.and_then(|d| parse_creator_info(&d));
Some(Layer {
ocg_id,
name,
intent,
locked: locked_set.contains(&ocg_id),
usage,
creator_info,
default_visible: !off_set.contains(&ocg_id),
})
}
pub fn parse_usage_dict(resolver: &Resolver, dict: &PdfDict) -> LayerUsage {
LayerUsage {
creator_info: dict
.get(b"CreatorInfo")
.and_then(|obj| resolver.deref(obj).ok())
.and_then(|resolved| resolved.as_dict().cloned())
.and_then(|d| parse_creator_info(&d)),
language: dict
.get(b"Language")
.and_then(|obj| resolver.deref(obj).ok())
.and_then(|resolved| resolved.as_dict().cloned())
.and_then(|d| parse_language(&d)),
export: dict
.get(b"Export")
.and_then(|obj| resolver.deref(obj).ok())
.and_then(|resolved| resolved.as_dict().cloned())
.and_then(|d| parse_export(&d)),
zoom: dict
.get(b"Zoom")
.and_then(|obj| resolver.deref(obj).ok())
.and_then(|resolved| resolved.as_dict().cloned())
.map(|d| parse_zoom(&d)),
print: dict
.get(b"Print")
.and_then(|obj| resolver.deref(obj).ok())
.and_then(|resolved| resolved.as_dict().cloned())
.and_then(|d| parse_print(&d)),
view: dict
.get(b"View")
.and_then(|obj| resolver.deref(obj).ok())
.and_then(|resolved| resolved.as_dict().cloned())
.and_then(|d| parse_view(&d)),
user: dict
.get(b"User")
.and_then(|obj| resolver.deref(obj).ok())
.and_then(|resolved| resolved.as_dict().cloned())
.map(|d| parse_user(&d)),
page_element: dict
.get(b"PageElement")
.and_then(|obj| resolver.deref(obj).ok())
.and_then(|resolved| resolved.as_dict().cloned())
.and_then(|d| {
d.get_name(b"Subtype")
.and_then(PageElementSubtype::from_name)
}),
}
}
fn parse_creator_info(dict: &PdfDict) -> Option<CreatorInfo> {
let creator = dict.get(b"Creator").and_then(pdf_string_to_rust_pub)?;
let subtype = dict
.get_name(b"Subtype")
.map(|n| String::from_utf8_lossy(n).into_owned());
Some(CreatorInfo { creator, subtype })
}
fn parse_language(dict: &PdfDict) -> Option<LanguageUsage> {
let lang = dict.get(b"Lang").and_then(pdf_string_to_rust_pub)?;
let preferred = dict.get_name(b"Preferred") == Some(b"ON");
Some(LanguageUsage { lang, preferred })
}
fn parse_export(dict: &PdfDict) -> Option<ExportUsage> {
let state = dict
.get_name(b"ExportState")
.and_then(UsageState::from_name)?;
Some(ExportUsage { state })
}
fn parse_zoom(dict: &PdfDict) -> ZoomUsage {
ZoomUsage {
min: dict.get_f64(b"min"),
max: dict.get_f64(b"max"),
}
}
fn parse_print(dict: &PdfDict) -> Option<PrintUsage> {
let state = dict
.get_name(b"PrintState")
.and_then(UsageState::from_name)?;
let subtype = dict
.get_name(b"Subtype")
.map(|n| String::from_utf8_lossy(n).into_owned());
Some(PrintUsage { subtype, state })
}
fn parse_view(dict: &PdfDict) -> Option<ViewUsage> {
let state = dict
.get_name(b"ViewState")
.and_then(UsageState::from_name)?;
Some(ViewUsage { state })
}
fn parse_user(dict: &PdfDict) -> UserUsage {
let user_type = dict
.get_name(b"Type")
.map(|n| String::from_utf8_lossy(n).into_owned());
let names = match dict.get(b"Name") {
Some(PdfObj::Str(s)) => vec![decode_pdf_text_string_pub(s)],
Some(PdfObj::Array(items)) => items
.iter()
.filter_map(|o| match o {
PdfObj::Str(s) => Some(decode_pdf_text_string_pub(s)),
_ => None,
})
.collect(),
_ => Vec::new(),
};
UserUsage { user_type, names }
}
fn collect_ocg_id_set(
resolver: &Resolver,
oc_dict: &PdfDict,
config_key: &[u8],
set_key: &[u8],
) -> HashSet<u32> {
let mut ids = HashSet::new();
let Some(config_obj) = oc_dict.get(config_key) else {
return ids;
};
let Ok(config_resolved) = resolver.deref(config_obj) else {
return ids;
};
let Some(config_dict) = config_resolved.as_dict() else {
return ids;
};
let Some(set_obj) = config_dict.get(set_key) else {
return ids;
};
let Ok(set_resolved) = resolver.deref(set_obj) else {
return ids;
};
if let Some(arr) = set_resolved.as_array() {
for o in arr {
if let Some((num, _gen)) = o.as_ref() {
ids.insert(num);
}
}
}
ids
}
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 intent_from_single_name() {
assert_eq!(LayerIntent::from_single_name(b"View"), LayerIntent::View);
assert_eq!(
LayerIntent::from_single_name(b"Design"),
LayerIntent::Design
);
assert_eq!(
LayerIntent::from_single_name(b"Export"),
LayerIntent::Export
);
match LayerIntent::from_single_name(b"Custom") {
LayerIntent::Other(s) => assert_eq!(s, "Custom"),
other => panic!("expected Other, got {other:?}"),
}
}
#[test]
fn page_element_names() {
assert_eq!(
PageElementSubtype::from_name(b"HF"),
Some(PageElementSubtype::HeaderFooter)
);
assert_eq!(
PageElementSubtype::from_name(b"FG"),
Some(PageElementSubtype::Foreground)
);
assert_eq!(
PageElementSubtype::from_name(b"BG"),
Some(PageElementSubtype::Background)
);
assert_eq!(
PageElementSubtype::from_name(b"L"),
Some(PageElementSubtype::Logo)
);
assert_eq!(PageElementSubtype::from_name(b"Other"), None);
}
#[test]
fn usage_state_names() {
assert_eq!(UsageState::from_name(b"ON"), Some(UsageState::On));
assert_eq!(UsageState::from_name(b"OFF"), Some(UsageState::Off));
assert_eq!(UsageState::from_name(b"on"), None);
}
}