use std::sync::Arc;
use crate::hash::FastSet;
use crate::object::{decode_text_string, Dict, ObjRef, Object};
use crate::source::AsyncObjectSource;
use crate::tree::{resolved_dict, Entries};
const MAX_VE_DEPTH: u32 = 8;
const DEFAULT_INTENT: &str = "View";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OcEvent {
View,
Print,
Export,
}
impl OcEvent {
pub fn from_name(name: &str) -> Option<OcEvent> {
match name {
"View" => Some(OcEvent::View),
"Print" => Some(OcEvent::Print),
"Export" => Some(OcEvent::Export),
_ => None,
}
}
pub fn as_name(self) -> &'static str {
match self {
OcEvent::View => "View",
OcEvent::Print => "Print",
OcEvent::Export => "Export",
}
}
fn state_key(self) -> &'static str {
match self {
OcEvent::View => "ViewState",
OcEvent::Print => "PrintState",
OcEvent::Export => "ExportState",
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct OcState {
off: FastSet<ObjRef>,
}
impl OcState {
pub async fn load_with<S: AsyncObjectSource>(src: &S, trailer: &Dict) -> Option<OcState> {
OcState::load_for_with(src, trailer, Some(OcEvent::View)).await
}
pub async fn load_for_with<S: AsyncObjectSource>(
src: &S,
trailer: &Dict,
event: Option<OcEvent>,
) -> Option<OcState> {
let props = properties(src, trailer).await?;
Some(OcState::from_properties(src, &props, event).await)
}
async fn from_properties<S: AsyncObjectSource>(
src: &S,
props: &Dict,
event: Option<OcEvent>,
) -> OcState {
let config = match props.get("D") {
Some(o) => resolved_dict(src, o).await,
None => None,
};
let mut off = configured_off(src, props, config.as_ref()).await;
if let (Some(config), Some(event)) = (config.as_ref(), event) {
apply_usage(src, config, event, &mut off).await;
}
retain_matching_intents(src, config.as_ref(), &mut off).await;
OcState { off }
}
pub fn hidden(&self, group: ObjRef) -> bool {
self.off.contains(&group)
}
pub async fn visible_with<S: AsyncObjectSource>(&self, src: &S, value: &Object) -> bool {
match value {
Object::Ref(r) => {
let Ok(resolved) = src.resolve(value).await else {
return true;
};
let Some(dict) = resolved.as_dict() else {
return true;
};
if is_ocmd(dict) {
return self.ocmd_visible(src, dict).await;
}
!self.hidden(*r)
}
Object::Dict(dict) => {
if is_ocmd(dict) {
return self.ocmd_visible(src, dict).await;
}
true
}
_ => true,
}
}
pub async fn props_visible_with<S: AsyncObjectSource>(
&self,
src: &S,
chain: &[Arc<Dict>],
props: &Object,
) -> bool {
let named;
let value = match props {
Object::Name(name) => {
named = properties_value(src, chain, &name.0).await;
match &named {
Some(value) => value,
None => return true,
}
}
other => other,
};
self.visible_with(src, value).await
}
async fn ocmd_visible<S: AsyncObjectSource>(&self, src: &S, dict: &Dict) -> bool {
if let Some(ve) = dict.get("VE") {
let Ok(Object::Array(expr)) = src.resolve(ve).await else {
return true;
};
return self.expression_visible(src, expr).await.unwrap_or(true);
}
let groups: Vec<ObjRef> = match dict.get("OCGs") {
None => return true,
Some(indirect @ Object::Ref(r)) => match src.resolve(indirect).await {
Ok(Object::Array(items)) => items.iter().filter_map(Object::as_ref).collect(),
Ok(Object::Dict(_)) => vec![*r],
_ => Vec::new(),
},
Some(Object::Array(items)) => items.iter().filter_map(Object::as_ref).collect(),
Some(_) => Vec::new(),
};
if groups.is_empty() {
return true;
}
let policy = match dict.get("P") {
Some(o) => src
.resolve(o)
.await
.ok()
.and_then(|o| o.as_name().map(|n| n.0.clone())),
None => None,
};
match policy.as_deref() {
Some("AllOn") => groups.iter().all(|g| !self.hidden(*g)),
Some("AnyOff") => groups.iter().any(|g| self.hidden(*g)),
Some("AllOff") => groups.iter().all(|g| self.hidden(*g)),
_ => groups.iter().any(|g| !self.hidden(*g)),
}
}
async fn expression_visible<S: AsyncObjectSource>(
&self,
src: &S,
expr: Vec<Object>,
) -> Option<bool> {
let mut stack = vec![VeFrame::new(src, expr).await?];
loop {
let top = stack.len() - 1;
let Some(operand) = stack[top].operands.get(stack[top].next).cloned() else {
let done = stack.pop()?;
let value = match done.operator.as_str() {
"And" => done.all,
"Or" => done.any,
_ => !done.any,
};
let Some(parent) = stack.last_mut() else {
return Some(value);
};
parent.fold(value);
continue;
};
stack[top].next += 1;
let value = match operand {
Object::Array(items) => {
if stack.len() > MAX_VE_DEPTH as usize {
return None;
}
stack.push(VeFrame::new(src, items).await?);
continue;
}
Object::Ref(group) => match src.resolve(&operand).await.ok()? {
Object::Array(items) => {
if stack.len() > MAX_VE_DEPTH as usize {
return None;
}
stack.push(VeFrame::new(src, items).await?);
continue;
}
Object::Dict(_) => !self.hidden(group),
_ => return None,
},
_ => return None,
};
stack[top].fold(value);
}
}
}
struct VeFrame {
operator: String,
operands: Vec<Object>,
next: usize,
all: bool,
any: bool,
}
impl VeFrame {
async fn new<S: AsyncObjectSource>(src: &S, mut expr: Vec<Object>) -> Option<VeFrame> {
let operator = match expr.first()? {
Object::Name(n) => n.0.clone(),
other => src.resolve(other).await.ok()?.as_name()?.0.clone(),
};
if !matches!(operator.as_str(), "And" | "Or" | "Not") {
return None;
}
let operands = expr.split_off(1);
if operands.is_empty() || (operator == "Not" && operands.len() != 1) {
return None;
}
Some(VeFrame {
operator,
operands,
next: 0,
all: true,
any: false,
})
}
fn fold(&mut self, value: bool) {
self.all &= value;
self.any |= value;
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct OcGroup {
pub reference: ObjRef,
pub name: Option<String>,
pub intent: Vec<String>,
pub usage: OcUsage,
pub visible: bool,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct OcUsage {
pub view: Option<bool>,
pub print: Option<bool>,
pub print_subtype: Option<String>,
pub export: Option<bool>,
pub zoom_min: Option<f64>,
pub zoom_max: Option<f64>,
pub language: Option<String>,
pub language_preferred: bool,
pub page_element: Option<String>,
pub creator: Option<String>,
pub creator_subtype: Option<String>,
pub user_type: Option<String>,
pub user_names: Vec<String>,
}
pub async fn optional_content_groups_with<S: AsyncObjectSource>(
src: &S,
trailer: &Dict,
event: Option<OcEvent>,
) -> Vec<OcGroup> {
let Some(props) = properties(src, trailer).await else {
return Vec::new();
};
let state = OcState::from_properties(src, &props, event).await;
let mut groups = Vec::new();
for reference in group_refs(src, props.get("OCGs")).await {
let Some(dict) = resolved_dict(src, &Object::Ref(reference)).await else {
continue;
};
let usage = match sub_dict(src, &dict, "Usage").await {
Some(usage) => read_usage(src, &usage).await,
None => OcUsage::default(),
};
groups.push(OcGroup {
reference,
name: Entries { src, dict: &dict }.text("Name").await,
intent: intent_names(src, &dict).await,
usage,
visible: !state.hidden(reference),
});
}
groups
}
async fn properties<S: AsyncObjectSource>(src: &S, trailer: &Dict) -> Option<Dict> {
let catalog = resolved_dict(src, trailer.get("Root")?).await?;
resolved_dict(src, catalog.get("OCProperties")?).await
}
async fn configured_off<S: AsyncObjectSource>(
src: &S,
props: &Dict,
config: Option<&Dict>,
) -> FastSet<ObjRef> {
let mut off: FastSet<ObjRef> = FastSet::default();
let Some(config) = config else {
return off;
};
let base = Entries { src, dict: config }.value("BaseState").await;
if on_off(base.as_ref()) == Some(false) {
off.extend(group_refs(src, props.get("OCGs")).await);
}
for group in group_refs(src, config.get("ON")).await {
off.remove(&group);
}
off.extend(group_refs(src, config.get("OFF")).await);
off
}
async fn apply_usage<S: AsyncObjectSource>(
src: &S,
config: &Dict,
event: OcEvent,
off: &mut FastSet<ObjRef>,
) {
let Some(apps) = config.get("AS") else {
return;
};
let Ok(Object::Array(apps)) = src.resolve(apps).await else {
return;
};
for app in &apps {
let Some(app) = resolved_dict(src, app).await else {
continue;
};
let entries = Entries { src, dict: &app };
if entries.named("Event", OcEvent::from_name).await != Some(event) {
continue;
}
let categories: Vec<OcEvent> = names(src, app.get("Category"))
.await
.unwrap_or_default()
.iter()
.filter_map(|name| OcEvent::from_name(name))
.collect();
if categories.is_empty() {
continue;
}
for group in group_refs(src, app.get("OCGs")).await {
let Some(dict) = resolved_dict(src, &Object::Ref(group)).await else {
continue;
};
let Some(usage) = sub_dict(src, &dict, "Usage").await else {
continue;
};
for category in &categories {
match usage_state(src, &usage, *category).await {
Some(true) => {
off.remove(&group);
}
Some(false) => {
off.insert(group);
}
None => {}
}
}
}
}
}
async fn retain_matching_intents<S: AsyncObjectSource>(
src: &S,
config: Option<&Dict>,
off: &mut FastSet<ObjRef>,
) {
if off.is_empty() {
return;
}
let config_intents = match config {
Some(config) => intent_names(src, config).await,
None => vec![DEFAULT_INTENT.to_string()],
};
if config_intents.iter().any(|name| name == "All") {
return;
}
let groups: Vec<ObjRef> = off.iter().copied().collect();
for group in groups {
let group_intents = match resolved_dict(src, &Object::Ref(group)).await {
Some(dict) => intent_names(src, &dict).await,
None => vec![DEFAULT_INTENT.to_string()],
};
if group_intents
.iter()
.any(|name| config_intents.contains(name))
{
continue;
}
off.remove(&group);
}
}
async fn intent_names<S: AsyncObjectSource>(src: &S, dict: &Dict) -> Vec<String> {
names(src, dict.get("Intent"))
.await
.unwrap_or_else(|| vec![DEFAULT_INTENT.to_string()])
}
async fn names<S: AsyncObjectSource>(src: &S, value: Option<&Object>) -> Option<Vec<String>> {
match src.resolve(value?).await.ok()? {
Object::Name(name) => Some(vec![name.0]),
Object::Array(items) => Some(
items
.iter()
.filter_map(|item| item.as_name().map(|name| name.0.clone()))
.collect(),
),
_ => None,
}
}
async fn sub_dict<S: AsyncObjectSource>(src: &S, dict: &Dict, key: &str) -> Option<Dict> {
resolved_dict(src, dict.get(key)?).await
}
async fn usage_state<S: AsyncObjectSource>(
src: &S,
usage: &Dict,
category: OcEvent,
) -> Option<bool> {
let entry = sub_dict(src, usage, category.as_name()).await?;
on_off(
Entries { src, dict: &entry }
.value(category.state_key())
.await
.as_ref(),
)
}
fn on_off(value: Option<&Object>) -> Option<bool> {
match value?.as_name()?.0.as_str() {
"ON" => Some(true),
"OFF" => Some(false),
_ => None,
}
}
async fn read_usage<S: AsyncObjectSource>(src: &S, usage: &Dict) -> OcUsage {
let mut out = OcUsage {
view: usage_state(src, usage, OcEvent::View).await,
print: usage_state(src, usage, OcEvent::Print).await,
export: usage_state(src, usage, OcEvent::Export).await,
..OcUsage::default()
};
if let Some(print) = sub_dict(src, usage, "Print").await {
out.print_subtype = name_entry(src, &print, "Subtype").await;
}
if let Some(zoom) = sub_dict(src, usage, "Zoom").await {
let entries = Entries { src, dict: &zoom };
out.zoom_min = entries.value("min").await.and_then(|o| o.as_f64());
out.zoom_max = entries.value("max").await.and_then(|o| o.as_f64());
}
if let Some(language) = sub_dict(src, usage, "Language").await {
let entries = Entries {
src,
dict: &language,
};
out.language = entries.text("Lang").await;
out.language_preferred = on_off(entries.value("Preferred").await.as_ref()) == Some(true);
}
if let Some(element) = sub_dict(src, usage, "PageElement").await {
out.page_element = name_entry(src, &element, "Subtype").await;
}
if let Some(creator) = sub_dict(src, usage, "CreatorInfo").await {
let entries = Entries {
src,
dict: &creator,
};
out.creator = entries.text("Creator").await;
out.creator_subtype = name_entry(src, &creator, "Subtype").await;
}
if let Some(user) = sub_dict(src, usage, "User").await {
let entries = Entries { src, dict: &user };
out.user_type = name_entry(src, &user, "Type").await;
out.user_names = match entries.value("Name").await {
Some(Object::Array(items)) => items
.iter()
.filter_map(|item| item.as_str_bytes().map(decode_text_string))
.collect(),
Some(single) => single
.as_str_bytes()
.map(decode_text_string)
.into_iter()
.collect(),
None => Vec::new(),
};
}
out
}
async fn name_entry<S: AsyncObjectSource>(src: &S, dict: &Dict, key: &str) -> Option<String> {
Entries { src, dict }
.value(key)
.await
.and_then(|o| o.as_name().map(|name| name.0.clone()))
}
fn is_ocmd(dict: &Dict) -> bool {
match dict.get_name("Type") {
Some(n) => n.0 == "OCMD",
None => dict.get("OCGs").is_some() || dict.get("VE").is_some(),
}
}
async fn group_refs<S: AsyncObjectSource>(src: &S, value: Option<&Object>) -> Vec<ObjRef> {
let Some(value) = value else {
return Vec::new();
};
let Ok(Object::Array(items)) = src.resolve(value).await else {
return Vec::new();
};
items.iter().filter_map(Object::as_ref).collect()
}
async fn properties_value<S: AsyncObjectSource>(
src: &S,
chain: &[Arc<Dict>],
name: &str,
) -> Option<Object> {
for res in chain {
let Some(cat) = res.get("Properties") else {
continue;
};
let Ok(Object::Dict(dict)) = src.resolve(cat).await else {
continue;
};
let Some(value) = dict.get(name) else {
continue;
};
return Some(value.clone());
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{block_on, Document, Immediate, Name};
use pdfboss_testkit::PdfBuilder;
fn gref(num: u32) -> ObjRef {
ObjRef { num, gen: 0 }
}
fn doc_with_oc(oc_props: &str, extra: impl FnOnce(&mut PdfBuilder)) -> Document {
let mut b = PdfBuilder::new();
let oc = if oc_props.is_empty() {
String::new()
} else {
format!(" /OCProperties {oc_props}")
};
b.object(1, &format!("<< /Type /Catalog /Pages 2 0 R{oc} >>"));
b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
b.object(10, "<< /Type /OCG /Name (one) >>");
b.object(11, "<< /Type /OCG /Name (two) >>");
extra(&mut b);
Document::load(b.build(1)).expect("load")
}
fn visible(doc: &Document, state: &OcState, value: &Object) -> bool {
block_on(state.visible_with(&Immediate(doc), value))
}
fn state_for(doc: &Document, event: Option<OcEvent>) -> OcState {
doc.oc_state_for(event).expect("state")
}
#[test]
fn no_ocproperties_means_no_state() {
let doc = doc_with_oc("", |_| {});
assert_eq!(doc.oc_state(), None);
}
#[test]
fn base_state_defaults_on_and_off_hides() {
let doc = doc_with_oc("<< /OCGs [10 0 R 11 0 R] /D << /OFF [11 0 R] >> >>", |_| {});
let state = doc.oc_state().expect("state");
assert!(!state.hidden(gref(10)));
assert!(state.hidden(gref(11)));
}
#[test]
fn base_state_off_hides_all_but_on() {
let doc = doc_with_oc(
"<< /OCGs [10 0 R 11 0 R] /D << /BaseState /OFF /ON [10 0 R] >> >>",
|_| {},
);
let state = doc.oc_state().expect("state");
assert!(!state.hidden(gref(10)));
assert!(state.hidden(gref(11)));
}
#[test]
fn off_is_applied_after_on() {
let doc = doc_with_oc(
"<< /OCGs [10 0 R] /D << /ON [10 0 R] /OFF [10 0 R] >> >>",
|_| {},
);
let state = doc.oc_state().expect("state");
assert!(state.hidden(gref(10)));
}
#[test]
fn configuration_resolves_indirection() {
let doc = doc_with_oc("<< /OCGs 20 0 R /D 21 0 R >>", |b| {
b.object(20, "[10 0 R 11 0 R]");
b.object(21, "<< /BaseState 22 0 R /ON 23 0 R >>");
b.object(22, "/OFF");
b.object(23, "[10 0 R]");
});
let state = doc.oc_state().expect("state");
assert!(!state.hidden(gref(10)));
assert!(state.hidden(gref(11)));
}
fn split_state() -> (Document, OcState) {
let doc = doc_with_oc("<< /OCGs [10 0 R 11 0 R] /D << /OFF [11 0 R] >> >>", |b| {
b.object(30, "<< /Type /OCMD /OCGs [10 0 R 11 0 R] >>");
b.object(31, "<< /Type /OCMD /OCGs [10 0 R 11 0 R] /P /AllOn >>");
b.object(32, "<< /Type /OCMD /OCGs [10 0 R 11 0 R] /P /AnyOff >>");
b.object(33, "<< /Type /OCMD /OCGs [10 0 R 11 0 R] /P /AllOff >>");
b.object(34, "<< /Type /OCMD /OCGs [11 0 R] >>");
b.object(35, "<< /Type /OCMD /OCGs [] /P /AllOff >>");
b.object(36, "<< /Type /OCMD /OCGs [null null] /P /AllOff >>");
b.object(37, "<< /Type /OCMD /OCGs 11 0 R /P /AnyOn >>");
b.object(38, "<< /Type /OCMD /OCGs [11 0 R] /P /Bogus >>");
});
let state = doc.oc_state().expect("state");
(doc, state)
}
#[test]
fn group_reference_visibility_follows_the_off_set() {
let (doc, state) = split_state();
assert!(visible(&doc, &state, &Object::Ref(gref(10))));
assert!(!visible(&doc, &state, &Object::Ref(gref(11))));
}
#[test]
fn membership_policies_follow_the_specification() {
let (doc, state) = split_state();
let ocmd = |num| Object::Ref(gref(num));
assert!(visible(&doc, &state, &ocmd(30)), "AnyOn default: 10 is on");
assert!(!visible(&doc, &state, &ocmd(31)), "AllOn: 11 is off");
assert!(visible(&doc, &state, &ocmd(32)), "AnyOff: 11 is off");
assert!(!visible(&doc, &state, &ocmd(33)), "AllOff: 10 is on");
assert!(
!visible(&doc, &state, &ocmd(34)),
"AnyOn over one off group"
);
assert!(visible(&doc, &state, &ocmd(35)), "empty /OCGs is visible");
assert!(visible(&doc, &state, &ocmd(36)), "nulls are ignored");
assert!(
!visible(&doc, &state, &ocmd(37)),
"single group by reference"
);
assert!(
!visible(&doc, &state, &ocmd(38)),
"unknown policy reads AnyOn"
);
}
#[test]
fn malformed_values_stay_visible() {
let (doc, state) = split_state();
assert!(visible(&doc, &state, &Object::Null));
assert!(visible(&doc, &state, &Object::Int(3)));
assert!(visible(&doc, &state, &Object::Ref(gref(999))), "dangling");
let direct_group = Object::Dict({
let mut d = Dict::new();
d.insert(Name("Type".into()), Object::Name(Name("OCG".into())));
d
});
assert!(
visible(&doc, &state, &direct_group),
"a direct group dictionary has no identity to be off"
);
}
fn ve_doc(ve: &str) -> (Document, OcState) {
let doc = doc_with_oc("<< /OCGs [10 0 R 11 0 R] /D << /OFF [11 0 R] >> >>", |b| {
b.object(40, &format!("<< /Type /OCMD /OCGs [10 0 R] /VE {ve} >>"));
});
let state = doc.oc_state().expect("state");
(doc, state)
}
fn ve_visible(ve: &str) -> bool {
let (doc, state) = ve_doc(ve);
visible(&doc, &state, &Object::Ref(gref(40)))
}
#[test]
fn visibility_expressions_evaluate() {
assert!(ve_visible("[/Not 11 0 R]"), "Not of an off group");
assert!(!ve_visible("[/Not 10 0 R]"), "Not of an on group");
assert!(!ve_visible("[/And 10 0 R 11 0 R]"));
assert!(ve_visible("[/Or 10 0 R 11 0 R]"));
assert!(!ve_visible("[/Or 11 0 R 11 0 R]"));
assert!(
ve_visible("[/Or 11 0 R [/Not 11 0 R]]"),
"nested expression"
);
assert!(
!ve_visible("[/And 10 0 R [/Not [/Not 11 0 R]]]"),
"double negation"
);
}
#[test]
fn expression_takes_precedence_over_policy() {
assert!(!ve_visible("[/And 11 0 R]"));
}
#[test]
fn malformed_expressions_are_visible() {
assert!(ve_visible("[]"), "no operator");
assert!(ve_visible("[/And]"), "no operands");
assert!(ve_visible("[/Not 11 0 R 11 0 R]"), "Not takes one operand");
assert!(ve_visible("[/Xor 11 0 R]"), "unknown operator");
assert!(ve_visible("[/And 11 0 R (text)]"), "non-group operand");
}
#[test]
fn expression_depth_is_capped() {
let mut ve = "11 0 R".to_string();
for _ in 0..(MAX_VE_DEPTH + 2) {
ve = format!("[/Not {ve}]");
}
assert!(ve_visible(&ve), "past the cap reads as visible");
}
#[test]
fn named_properties_keep_group_identity() {
let (doc, state) = split_state();
let mut properties = Dict::new();
properties.insert(Name("On".into()), Object::Ref(gref(10)));
properties.insert(Name("Off".into()), Object::Ref(gref(11)));
let mut res = Dict::new();
res.insert(Name("Properties".into()), Object::Dict(properties));
let chain = vec![Arc::new(res)];
let src = Immediate(&doc);
let named = |name: &str| Object::Name(Name(name.into()));
assert!(block_on(state.props_visible_with(
&src,
&chain,
&named("On")
)));
assert!(!block_on(state.props_visible_with(
&src,
&chain,
&named("Off")
)));
assert!(
block_on(state.props_visible_with(&src, &chain, &named("Nope"))),
"an unresolvable name stays visible"
);
}
fn usage_doc(config: &str) -> Document {
doc_with_oc(
&format!("<< /OCGs [10 0 R 11 0 R 12 0 R 13 0 R] /D {config} >>"),
|b| {
b.object(
12,
"<< /Type /OCG /Name (print only) /Usage << /View << /ViewState /OFF >> \
/Print << /PrintState /ON /Subtype /Watermark >> >> >>",
);
b.object(
13,
"<< /Type /OCG /Name (screen only) /Usage << /Export << /ExportState /OFF >> \
/Zoom << /min 2 >> >> >>",
);
},
)
}
#[test]
fn view_usage_application_hides_a_view_state_off_group() {
let doc = usage_doc(
"<< /AS [ << /Event /View /OCGs [12 0 R 13 0 R] /Category [/View] >> \
<< /Event /Export /OCGs [13 0 R] /Category [/Export] >> ] >>",
);
let state = doc.oc_state().expect("state");
assert!(state.hidden(gref(12)), "ViewState OFF under the View event");
assert!(
!state.hidden(gref(13)),
"the Export dictionary is not applied"
);
assert!(
!state.hidden(gref(10)),
"a group without /Usage is unchanged"
);
}
#[test]
fn each_event_applies_its_own_dictionaries() {
let doc = usage_doc(
"<< /OFF [12 0 R] /AS [ << /Event /View /OCGs [12 0 R 13 0 R] /Category [/View] >> \
<< /Event /Print /OCGs [12 0 R] /Category [/Print] >> \
<< /Event /Export /OCGs [13 0 R] /Category [/Export] >> ] >>",
);
let print = state_for(&doc, Some(OcEvent::Print));
assert!(
!print.hidden(gref(12)),
"PrintState ON turns the /OFF group on"
);
assert!(!print.hidden(gref(13)));
let export = state_for(&doc, Some(OcEvent::Export));
assert!(export.hidden(gref(12)), "the /OFF array still applies");
assert!(
export.hidden(gref(13)),
"ExportState OFF under the Export event"
);
let none = state_for(&doc, None);
assert!(none.hidden(gref(12)));
assert!(!none.hidden(gref(13)), "no usage application at all");
}
#[test]
fn usage_without_application_or_category_changes_nothing() {
let doc = usage_doc("<< >>");
assert!(!doc.oc_state().expect("state").hidden(gref(12)), "no /AS");
let doc =
usage_doc("<< /AS [ << /Event /View /OCGs [12 0 R] /Category [/Export /Zoom] >> ] >>");
assert!(
!doc.oc_state().expect("state").hidden(gref(12)),
"categories the group has no usage entry for"
);
let doc = usage_doc(
"<< /AS [ << /Event /View /OCGs [12 0 R] /Category [/View] >> \
<< /Event /View /OCGs [12 0 R] /Category [/Print] >> ] >>",
);
assert!(
!doc.oc_state().expect("state").hidden(gref(12)),
"the later dictionary's Print category (ON) wins"
);
let doc = usage_doc("<< /AS [ << /Event /View /Category [/View] >> ] >>");
assert!(
!doc.oc_state().expect("state").hidden(gref(12)),
"no /OCGs means no groups are affected"
);
}
fn intent_doc(config: &str) -> Document {
doc_with_oc(
&format!("<< /OCGs [10 0 R 11 0 R 14 0 R 15 0 R] /D {config} >>"),
|b| {
b.object(14, "<< /Type /OCG /Name (guides) /Intent /Design >>");
b.object(15, "<< /Type /OCG /Name (both) /Intent [/Design /View] >>");
},
)
}
#[test]
fn a_group_of_another_intent_never_hides_content() {
let doc = intent_doc("<< /OFF [11 0 R 14 0 R 15 0 R] >>");
let state = doc.oc_state().expect("state");
assert!(state.hidden(gref(11)), "default intents match");
assert!(
!state.hidden(gref(14)),
"Design against a View configuration"
);
assert!(state.hidden(gref(15)), "[/Design /View] shares View");
assert!(
visible(&doc, &state, &Object::Ref(gref(14))),
"content in the Design group paints"
);
}
#[test]
fn the_configuration_intent_selects_the_groups_it_controls() {
let design = intent_doc("<< /Intent /Design /OFF [11 0 R 14 0 R 15 0 R] >>");
let state = design.oc_state().expect("state");
assert!(
!state.hidden(gref(11)),
"a View group under a Design configuration"
);
assert!(state.hidden(gref(14)));
assert!(state.hidden(gref(15)));
let all = intent_doc("<< /Intent [/All] /OFF [11 0 R 14 0 R] >>");
let state = all.oc_state().expect("state");
assert!(state.hidden(gref(11)));
assert!(state.hidden(gref(14)));
let none = intent_doc("<< /Intent [] /OFF [11 0 R 14 0 R] >>");
let state = none.oc_state().expect("state");
assert!(!state.hidden(gref(11)), "an empty intent controls nothing");
assert!(!state.hidden(gref(14)));
}
#[test]
fn the_intent_rule_applies_after_usage() {
let doc = doc_with_oc(
"<< /OCGs [10 0 R 16 0 R] /D << /AS [ << /Event /View /OCGs [16 0 R] /Category [/View] >> ] >> >>",
|b| {
b.object(
16,
"<< /Type /OCG /Name (design, view off) /Intent /Design \
/Usage << /View << /ViewState /OFF >> >> >>",
);
},
);
assert!(!doc.oc_state().expect("state").hidden(gref(16)));
}
#[test]
fn groups_read_as_data() {
let doc = doc_with_oc(
"<< /OCGs [10 0 R 17 0 R 999 0 R] /D << /OFF [17 0 R] \
/AS [ << /Event /Print /OCGs [17 0 R] /Category [/Print] >> ] >> >>",
|b| {
b.object(
17,
"<< /Type /OCG /Name <FEFF00450073> /Intent [/Design /View] /Usage << \
/CreatorInfo << /Creator (CAD) /Subtype /Technical >> \
/Language << /Lang (es-MX) /Preferred /ON >> \
/Export << /ExportState /OFF >> \
/Zoom << /min 0.5 /max 4 >> \
/Print << /Subtype /Watermark /PrintState /ON >> \
/View << /ViewState /OFF >> \
/User << /Type /Org /Name [(Acme) (Globex)] >> \
/PageElement << /Subtype /HF >> >> >>",
);
},
);
let groups = doc.optional_content_groups(Some(OcEvent::View));
assert_eq!(groups.len(), 2, "a dangling reference is skipped");
assert_eq!(
groups[0],
OcGroup {
reference: gref(10),
name: Some("one".to_string()),
intent: vec!["View".to_string()],
usage: OcUsage::default(),
visible: true,
}
);
assert_eq!(
groups[1],
OcGroup {
reference: gref(17),
name: Some("Es".to_string()),
intent: vec!["Design".to_string(), "View".to_string()],
usage: OcUsage {
view: Some(false),
print: Some(true),
print_subtype: Some("Watermark".to_string()),
export: Some(false),
zoom_min: Some(0.5),
zoom_max: Some(4.0),
language: Some("es-MX".to_string()),
language_preferred: true,
page_element: Some("HF".to_string()),
creator: Some("CAD".to_string()),
creator_subtype: Some("Technical".to_string()),
user_type: Some("Org".to_string()),
user_names: vec!["Acme".to_string(), "Globex".to_string()],
},
visible: false,
}
);
let printed = doc.optional_content_groups(Some(OcEvent::Print));
assert!(printed[1].visible, "PrintState ON under the Print event");
assert!(
doc_with_oc("", |_| {})
.optional_content_groups(None)
.is_empty(),
"no /OCProperties, no groups"
);
}
}