use crate::names;
use pdfrum_common::{DiagKind, Diagnostics, Severity};
use pdfrum_object::{Array, Dict, Name, Object, Resolve};
use std::collections::HashMap;
pub const MAX_VE_DEPTH: u32 = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum UsageType {
#[default]
View,
Design,
Print,
Export,
}
impl UsageType {
#[must_use]
pub fn as_bytes(self) -> &'static [u8] {
match self {
Self::View => b"View",
Self::Design => b"Design",
Self::Print => b"Print",
Self::Export => b"Export",
}
}
#[must_use]
pub fn state_key(self) -> Name {
let mut key = self.as_bytes().to_vec();
key.extend_from_slice(b"State");
Name::new(key)
}
}
#[derive(Debug)]
pub struct OcContext {
usage: UsageType,
properties: Option<Dict>,
cache: HashMap<pdfrum_object::ObjRef, bool>,
}
impl OcContext {
#[must_use]
pub fn new(properties: Option<Dict>, usage: UsageType) -> Self {
Self {
usage,
properties,
cache: HashMap::new(),
}
}
#[must_use]
pub fn permissive() -> Self {
Self::new(None, UsageType::View)
}
pub fn content_visible<R: Resolve>(
&mut self,
dict: Option<&Dict>,
r: &R,
diags: &mut Diagnostics,
) -> bool {
let Some(dict) = dict else {
return true;
};
if dict
.name(names::TYPE)
.is_none_or(|t| t.as_bytes() == b"OCG")
{
return self.group_visible(Some(dict), r);
}
self.membership_visible(dict, r, diags)
}
pub fn group_visible<R: Resolve>(&mut self, dict: Option<&Dict>, r: &R) -> bool {
let Some(dict) = dict else {
return false;
};
let key = dict.reference(&Name::from("__self"));
if let Some(id) = key
&& let Some(hit) = self.cache.get(&id)
{
return *hit;
}
let answer = self.load_group_state(dict, r);
if let Some(id) = key {
self.cache.insert(id, answer);
}
answer
}
#[must_use]
pub fn memoized(&self) -> usize {
self.cache.len()
}
fn membership_visible<R: Resolve>(
&mut self,
ocmd: &Dict,
r: &R,
diags: &mut Diagnostics,
) -> bool {
if let Some(ve) = ocmd.array(names::VE, r) {
return self.eval_expression(&ve, r, 0);
}
let policy = ocmd
.byte_string(names::P, r)
.unwrap_or_else(|| b"AnyOn".to_vec());
let Some(ocgs) = ocmd.get(names::OCGS, r) else {
return true;
};
match &*ocgs {
Object::Dict(d) => self.group_visible(Some(d), r),
Object::Array(array) => {
let state = policy == b"AllOn" || policy == b"AllOff";
let mut seen_valid = false;
for element in array.iter() {
let Some(d) = element.resolve(r).ok().and_then(|o| o.as_dict().cloned()) else {
continue;
};
seen_valid = true;
let visible = self.group_visible(Some(&d), r);
if (policy == b"AnyOn" && visible) || (policy == b"AnyOff" && !visible) {
return true;
}
if (policy == b"AllOn" && !visible) || (policy == b"AllOff" && visible) {
return false;
}
}
if !seen_valid {
return true;
}
if !matches!(
policy.as_slice(),
b"AnyOn" | b"AllOn" | b"AnyOff" | b"AllOff"
) {
diags.record(
Severity::Suspicious,
DiagKind::OptionalContentPolicyUnknown,
None,
);
}
state
}
_ => true,
}
}
fn eval_expression<R: Resolve>(&mut self, expr: &Array, r: &R, depth: u32) -> bool {
if depth > MAX_VE_DEPTH {
return false;
}
let operator = expr.byte_string_at(0).unwrap_or_default();
match operator.as_slice() {
b"Not" => match expr.get(1, r).as_deref() {
Some(Object::Dict(d)) => !self.group_visible(Some(d), r),
Some(Object::Array(a)) => !self.eval_expression(a, r, depth + 1),
_ => false,
},
b"Or" | b"And" => {
let and = operator == b"And";
let mut value = false;
for i in 1..expr.len() {
let operand = expr.get(i, r);
let Some(operand) = operand else {
continue;
};
let result = match &*operand {
Object::Dict(d) => self.group_visible(Some(d), r),
Object::Array(a) => self.eval_expression(a, r, depth + 1),
_ => false,
};
if i == 1 {
value = result;
} else if and {
value = value && result;
} else {
value = value || result;
}
}
value
}
_ => false,
}
}
fn load_group_state<R: Resolve>(&mut self, ocg: &Dict, r: &R) -> bool {
if !has_intent(ocg, b"View", b"View", r) {
return true;
}
if let Some(usage) = ocg.dict(names::USAGE, r) {
let state_key = self.usage.state_key();
if let Some(entry) = usage.dict(&Name::new(self.usage.as_bytes()), r)
&& entry.contains_key(&state_key)
{
return entry.byte_string(&state_key, r).as_deref() != Some(b"OFF");
}
if self.usage != UsageType::View
&& let Some(entry) = usage.dict(&Name::from("View"), r)
&& entry.contains_key(&Name::from("ViewState"))
{
return entry.byte_string(&Name::from("ViewState"), r).as_deref() != Some(b"OFF");
}
}
self.state_from_config(ocg, r)
}
fn state_from_config<R: Resolve>(&mut self, ocg: &Dict, r: &R) -> bool {
let Some(config) = self.select_config(ocg, r) else {
return true;
};
let mut on = config.byte_string(names::BASE_STATE, r).as_deref() != Some(b"OFF");
if let Some(array) = config.array(names::ON, r)
&& contains_dict(&array, ocg, r)
{
on = true;
}
if let Some(array) = config.array(names::OFF, r)
&& contains_dict(&array, ocg, r)
{
on = false;
}
if let Some(entries) = config.array(names::AS, r) {
for element in entries.iter() {
let Some(entry) = element.resolve(r).ok().and_then(|o| o.as_dict().cloned()) else {
continue;
};
let event = entry
.byte_string(names::EVENT, r)
.unwrap_or_else(|| b"View".to_vec());
if event != self.usage.as_bytes() {
continue;
}
let Some(groups) = entry.array(names::OCGS, r) else {
continue;
};
if !contains_dict(&groups, ocg, r) {
continue;
}
let state_key = self.usage.state_key();
if let Some(sub) = entry.dict(&Name::new(self.usage.as_bytes()), r) {
on = sub.byte_string(&state_key, r).as_deref() != Some(b"OFF");
}
}
}
on
}
fn select_config<R: Resolve>(&self, ocg: &Dict, r: &R) -> Option<Dict> {
let properties = self.properties.as_ref()?;
let all = properties.array(names::OCGS, r)?;
if !contains_dict(&all, ocg, r) {
return None;
}
if let Some(configs) = properties.array(names::CONFIGS, r) {
for element in configs.iter() {
let Some(config) = element.resolve(r).ok().and_then(|o| o.as_dict().cloned())
else {
continue;
};
if has_intent(&config, b"View", b"", r) {
return Some(config);
}
}
}
properties.dict(names::D_CONFIG, r)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Visibility {
nodes: Vec<Node>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
struct Node {
visible: bool,
children: Visibility,
}
impl Visibility {
#[must_use]
pub fn all_visible() -> Self {
Self::default()
}
#[must_use]
pub fn shows_everything(&self) -> bool {
self.nodes.is_empty()
}
#[must_use]
pub fn visible(&self, index: usize) -> bool {
self.nodes.get(index).is_none_or(|n| n.visible)
}
#[must_use]
pub fn children(&self, index: usize) -> Self {
self.nodes
.get(index)
.map(|n| n.children.clone())
.unwrap_or_default()
}
fn collapsed(self) -> Self {
if self
.nodes
.iter()
.all(|n| n.visible && n.children.shows_everything())
{
Self::default()
} else {
self
}
}
}
#[must_use]
pub fn page_visibility<R: Resolve>(
page: &crate::Page,
oc: &mut OcContext,
r: &R,
diags: &mut Diagnostics,
) -> Visibility {
object_visibility(&page.objects, oc, r, diags)
}
fn object_visibility<R: Resolve>(
objects: &[crate::PageObject],
oc: &mut OcContext,
r: &R,
diags: &mut Diagnostics,
) -> Visibility {
let nodes = objects
.iter()
.map(|object| {
let visible = object_visible(object, oc, r, diags);
let children = match object {
crate::PageObject::Form(f) if visible => {
object_visibility(&f.object.objects, oc, r, diags)
}
_ => Visibility::default(),
};
Node { visible, children }
})
.collect();
Visibility { nodes }.collapsed()
}
fn object_visible<R: Resolve>(
object: &crate::PageObject,
oc: &mut OcContext,
r: &R,
diags: &mut Diagnostics,
) -> bool {
if !object
.marks()
.optional_content_all()
.into_iter()
.all(|d| oc.content_visible(Some(d), r, diags))
{
return false;
}
let own = match object {
crate::PageObject::Form(f) => f.object.oc.as_deref(),
crate::PageObject::Image(i) => i.object.oc.as_deref(),
crate::PageObject::Path(_) | crate::PageObject::Text(_) | crate::PageObject::Shading(_) => {
None
}
};
oc.content_visible(own, r, diags)
}
fn has_intent(dict: &Dict, element: &[u8], default: &[u8], r: &impl Resolve) -> bool {
let Some(intent) = dict.get(names::INTENT, r) else {
return element == default;
};
match &*intent {
Object::Array(array) => array.iter().any(|o| {
let s = o.to_byte_string();
s == b"All" || s == element
}),
other => {
let s = other.to_byte_string();
s == b"All" || s == element
}
}
}
fn contains_dict(array: &Array, target: &Dict, r: &impl Resolve) -> bool {
array.iter().any(|o| {
o.resolve(r)
.ok()
.and_then(|res| res.as_dict().cloned())
.as_ref()
== Some(target)
})
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unreadable_literal,
clippy::float_cmp,
clippy::indexing_slicing,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
reason = "test fixtures quote oracle vectors verbatim and compare exactly"
)]
use super::{MAX_VE_DEPTH, OcContext, UsageType};
use pdfrum_common::{DiagKind, Diagnostics};
use pdfrum_object::{Array, Dict, Name, NoResolve, Object};
fn ocg(name: &str) -> Dict {
Dict::from_pairs([
(Name::from("Type"), Object::Name(Name::from("OCG"))),
(Name::from("Name"), Object::Name(Name::from(name))),
])
}
fn ocmd(pairs: Vec<(Name, Object)>) -> Dict {
let mut d = Dict::from_pairs([(Name::from("Type"), Object::Name(Name::from("OCMD")))]);
for (k, v) in pairs {
d.push(k, v);
}
d
}
#[test]
fn the_null_asymmetry_between_content_and_group_checks() {
let mut ctx = OcContext::permissive();
let mut diags = Diagnostics::default();
assert!(ctx.content_visible(None, &NoResolve, &mut diags));
assert!(!ctx.group_visible(None, &NoResolve));
}
#[test]
fn a_group_with_no_configuration_is_visible() {
let mut ctx = OcContext::permissive();
let mut diags = Diagnostics::default();
assert!(ctx.content_visible(Some(&ocg("Layer")), &NoResolve, &mut diags));
}
#[test]
fn the_four_membership_policies() {
let on = ocg("On");
let mut diags = Diagnostics::default();
for (policy, want) in [
("AnyOn", true),
("AllOn", true),
("AnyOff", false),
("AllOff", false),
] {
let mut ctx = OcContext::permissive();
let d = ocmd(vec![
(Name::from("P"), Object::Name(Name::from(policy))),
(
Name::from("OCGs"),
Object::Array(Array::of([Object::Dict(on.clone())])),
),
]);
assert_eq!(
ctx.content_visible(Some(&d), &NoResolve, &mut diags),
want,
"policy {policy}"
);
}
}
#[test]
fn an_unknown_policy_with_a_valid_group_is_invisible() {
let mut ctx = OcContext::permissive();
let mut diags = Diagnostics::default();
let d = ocmd(vec![
(Name::from("P"), Object::Name(Name::from("SomeOn"))),
(
Name::from("OCGs"),
Object::Array(Array::of([Object::Dict(ocg("On"))])),
),
]);
assert!(!ctx.content_visible(Some(&d), &NoResolve, &mut diags));
assert!(diags.contains(&DiagKind::OptionalContentPolicyUnknown));
}
#[test]
fn a_membership_with_no_valid_groups_is_visible() {
let mut ctx = OcContext::permissive();
let mut diags = Diagnostics::default();
let d = ocmd(vec![
(Name::from("P"), Object::Name(Name::from("AllOn"))),
(
Name::from("OCGs"),
Object::Array(Array::of([Object::Int(7), Object::Null])),
),
]);
assert!(ctx.content_visible(Some(&d), &NoResolve, &mut diags));
}
#[test]
fn a_single_group_dictionary_ignores_the_policy() {
let mut ctx = OcContext::permissive();
let mut diags = Diagnostics::default();
let d = ocmd(vec![
(Name::from("P"), Object::Name(Name::from("AllOff"))),
(Name::from("OCGs"), Object::Dict(ocg("On"))),
]);
assert!(ctx.content_visible(Some(&d), &NoResolve, &mut diags));
}
#[test]
fn a_visibility_expression_takes_precedence_over_the_policy() {
let mut ctx = OcContext::permissive();
let mut diags = Diagnostics::default();
let d = ocmd(vec![
(Name::from("P"), Object::Name(Name::from("AnyOn"))),
(
Name::from("VE"),
Object::Array(Array::of([
Object::Name(Name::from("Not")),
Object::Dict(ocg("On")),
])),
),
(
Name::from("OCGs"),
Object::Array(Array::of([Object::Dict(ocg("On"))])),
),
]);
assert!(!ctx.content_visible(Some(&d), &NoResolve, &mut diags));
}
#[test]
fn an_unknown_expression_operator_is_invisible() {
let mut ctx = OcContext::permissive();
let mut diags = Diagnostics::default();
let d = ocmd(vec![(
Name::from("VE"),
Object::Array(Array::of([
Object::Name(Name::from("Nand")),
Object::Dict(ocg("On")),
])),
)]);
assert!(!ctx.content_visible(Some(&d), &NoResolve, &mut diags));
}
#[test]
fn an_and_whose_first_operand_is_missing_is_false() {
let mut ctx = OcContext::permissive();
let mut diags = Diagnostics::default();
let d = ocmd(vec![(
Name::from("VE"),
Object::Array(Array::of([
Object::Name(Name::from("And")),
Object::Null,
Object::Dict(ocg("On")),
])),
)]);
assert!(!ctx.content_visible(Some(&d), &NoResolve, &mut diags));
let d = ocmd(vec![(
Name::from("VE"),
Object::Array(Array::of([
Object::Name(Name::from("Or")),
Object::Null,
Object::Dict(ocg("On")),
])),
)]);
assert!(ctx.content_visible(Some(&d), &NoResolve, &mut diags));
}
#[test]
fn expressions_deeper_than_the_cap_are_invisible() {
let mut ctx = OcContext::permissive();
let mut diags = Diagnostics::default();
let mut expr = Object::Dict(ocg("On"));
for _ in 0..=MAX_VE_DEPTH + 1 {
expr = Object::Array(Array::of([Object::Name(Name::from("Not")), expr]));
}
let d = ocmd(vec![(Name::from("VE"), expr)]);
let _ = ctx.content_visible(Some(&d), &NoResolve, &mut diags);
}
#[test]
fn usage_state_keys_are_built_from_the_usage_name() {
assert_eq!(UsageType::View.state_key().as_bytes(), b"ViewState");
assert_eq!(UsageType::Print.state_key().as_bytes(), b"PrintState");
assert_eq!(UsageType::Export.as_bytes(), b"Export");
}
struct Store(std::collections::HashMap<u32, std::sync::Arc<Object>>);
impl pdfrum_object::Resolve for Store {
fn fetch(
&self,
r: pdfrum_object::ObjRef,
) -> Result<std::sync::Arc<Object>, pdfrum_object::Error> {
self.0
.get(&r.num)
.map(std::sync::Arc::clone)
.ok_or(pdfrum_object::Error::UnresolvedRef(r))
}
}
#[test]
fn a_self_referencing_visibility_expression_terminates() {
let selfref = Object::Array(Array::of([
Object::Name(Name::from("Not")),
Object::Ref(pdfrum_object::ObjRef {
num: 1,
generation: 0,
}),
]));
let mut objects = std::collections::HashMap::new();
objects.insert(1u32, std::sync::Arc::new(selfref.clone()));
let store = Store(objects);
let mut ctx = OcContext::permissive();
let mut diags = Diagnostics::default();
let d = ocmd(vec![(Name::from("VE"), selfref)]);
let _ = ctx.content_visible(Some(&d), &store, &mut diags);
let mut objects = std::collections::HashMap::new();
objects.insert(
1u32,
std::sync::Arc::new(Object::Array(Array::of([
Object::Name(Name::from("Not")),
Object::Ref(pdfrum_object::ObjRef {
num: 2,
generation: 0,
}),
]))),
);
objects.insert(
2u32,
std::sync::Arc::new(Object::Array(Array::of([
Object::Name(Name::from("Not")),
Object::Ref(pdfrum_object::ObjRef {
num: 1,
generation: 0,
}),
]))),
);
let store = Store(objects);
let mut ctx = OcContext::permissive();
let d = ocmd(vec![(
Name::from("VE"),
Object::Ref(pdfrum_object::ObjRef {
num: 1,
generation: 0,
}),
)]);
let _ = ctx.content_visible(Some(&d), &store, &mut diags);
}
use crate::ops::MarkProperties;
use crate::state::ContentMarks;
use crate::{Content, PageObject, PathObject};
fn off_group() -> Dict {
Dict::from_pairs([
(Name::from("Type"), Object::Name(Name::from("OCG"))),
(Name::from("Name"), Object::Name(Name::from("Hidden"))),
])
}
fn context_hiding(off: &Dict) -> OcContext {
let properties = Dict::from_pairs([
(
Name::from("OCGs"),
Object::Array(Array::of([Object::Dict(off.clone())])),
),
(
Name::from("D"),
Object::Dict(Dict::from_pairs([(
Name::from("OFF"),
Object::Array(Array::of([Object::Dict(off.clone())])),
)])),
),
]);
OcContext::new(Some(properties), UsageType::View)
}
fn marked(oc: Option<&Dict>, from_resources: bool) -> ContentMarks {
let mut marks = ContentMarks::new();
if let Some(d) = oc {
push_oc(&mut marks, d, from_resources);
}
marks
}
fn push_oc(marks: &mut ContentMarks, dict: &Dict, from_resources: bool) {
let properties = if from_resources {
MarkProperties::Named(Name::from("MC0"))
} else {
MarkProperties::Inline(Box::new(dict.clone()))
};
marks.push_with_properties(Name::from("OC"), &properties, |_| Some(dict.clone()));
}
fn path_with(marks: ContentMarks) -> PageObject {
PageObject::Path(Box::new(Content {
object: PathObject {
path: kurbo::BezPath::new(),
matrix: kurbo::Affine::IDENTITY,
fill_rule: crate::FillRule::Winding,
stroke: false,
},
state: crate::GraphicsState::default(),
marks,
content_stream: Some(0),
dirty: false,
active: true,
}))
}
fn page_of(objects: Vec<PageObject>) -> crate::Page {
crate::Page {
objects,
..crate::Page::empty()
}
}
#[test]
fn a_page_with_no_optional_content_produces_an_empty_tree() {
let page = page_of(vec![path_with(ContentMarks::new()); 3]);
let mut ctx = OcContext::permissive();
let mut diags = Diagnostics::default();
let v = super::page_visibility(&page, &mut ctx, &NoResolve, &mut diags);
assert!(
v.shows_everything(),
"an all-visible page collapses to nothing, so a renderer can skip \
the descent entirely"
);
assert!(v.visible(0));
assert!(v.visible(99));
}
#[test]
fn an_off_group_hides_the_object_its_mark_encloses() {
let off = off_group();
let page = page_of(vec![
path_with(marked(None, false)),
path_with(marked(Some(&off), true)),
path_with(marked(None, false)),
]);
let mut ctx = context_hiding(&off);
let mut diags = Diagnostics::default();
let v = super::page_visibility(&page, &mut ctx, &NoResolve, &mut diags);
assert!(!v.shows_everything());
assert!(v.visible(0));
assert!(!v.visible(1), "the marked object is hidden");
assert!(v.visible(2));
}
#[test]
fn an_inline_property_list_never_hides_anything() {
let off = off_group();
let page = page_of(vec![path_with(marked(Some(&off), false))]);
let mut ctx = context_hiding(&off);
let mut diags = Diagnostics::default();
let v = super::page_visibility(&page, &mut ctx, &NoResolve, &mut diags);
assert!(v.shows_everything());
}
#[test]
fn every_enclosing_mark_gets_a_veto_not_just_the_innermost() {
let off = off_group();
let mut marks = marked(Some(&off), true);
push_oc(&mut marks, &ocg("Shown"), true);
let page = page_of(vec![path_with(marks)]);
let mut ctx = context_hiding(&off);
let mut diags = Diagnostics::default();
let v = super::page_visibility(&page, &mut ctx, &NoResolve, &mut diags);
assert!(!v.visible(0));
}
#[test]
fn a_hidden_form_says_nothing_about_children_nobody_reaches() {
let off = off_group();
let form = PageObject::Form(Box::new(Content {
object: crate::FormObject {
objects: vec![path_with(ContentMarks::new())],
matrix: kurbo::Affine::IDENTITY,
bbox: None,
transparency: crate::Transparency::default(),
oc: Some(std::sync::Arc::new(off.clone())),
source: None,
live_edit: false,
},
state: crate::GraphicsState::default(),
marks: ContentMarks::new(),
content_stream: Some(0),
dirty: false,
active: true,
}));
let page = page_of(vec![form]);
let mut ctx = context_hiding(&off);
let mut diags = Diagnostics::default();
let v = super::page_visibility(&page, &mut ctx, &NoResolve, &mut diags);
assert!(!v.visible(0), "the form's own `/OC` hides it");
assert!(
v.children(0).shows_everything(),
"and its children are not walked, because nothing reaches them"
);
}
#[test]
fn a_visible_forms_children_are_answered_in_their_own_frame() {
let off = off_group();
let form = PageObject::Form(Box::new(Content {
object: crate::FormObject {
objects: vec![
path_with(ContentMarks::new()),
path_with(marked(Some(&off), true)),
],
matrix: kurbo::Affine::IDENTITY,
bbox: None,
transparency: crate::Transparency::default(),
oc: None,
source: None,
live_edit: false,
},
state: crate::GraphicsState::default(),
marks: ContentMarks::new(),
content_stream: Some(0),
dirty: false,
active: true,
}));
let page = page_of(vec![form]);
let mut ctx = context_hiding(&off);
let mut diags = Diagnostics::default();
let v = super::page_visibility(&page, &mut ctx, &NoResolve, &mut diags);
assert!(v.visible(0), "the form itself is drawn");
let inner = v.children(0);
assert!(inner.visible(0));
assert!(!inner.visible(1), "but its second child is not");
}
}