use pdfrum_common::{Diagnostics, Limits};
use pdfrum_object::{Dict, Object, Resolve, decode_text};
use crate::annot::AnnotFlags;
use crate::form::Form;
use crate::names;
use crate::nav::{Action, ActionKind};
const CLEARED_BY_HIDE: AnnotFlags = AnnotFlags::INVISIBLE.with(AnnotFlags::NO_VIEW);
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Hidden {
touched: Vec<(Dict, AnnotFlags)>,
}
impl Hidden {
#[must_use]
pub fn is_empty(&self) -> bool {
self.touched.is_empty()
}
#[must_use]
pub fn flags<R: Resolve>(&self, dict: &Dict, r: &R) -> AnnotFlags {
self.touched
.iter()
.find(|(touched, _)| touched == dict)
.map_or_else(
|| AnnotFlags::from_bits(dict.int(names::F, r).unwrap_or(0)),
|(_, f)| *f,
)
}
}
#[must_use]
pub fn hidden_by_open_action<R: Resolve>(
catalog: &Dict,
r: &R,
limits: &Limits,
diags: &mut Diagnostics,
) -> Hidden {
let mut hidden = Hidden::default();
let Some(dict) = catalog.dict(names::OPEN_ACTION, r) else {
return hidden;
};
let root = Action::new(dict);
let mut form: Option<Option<Form>> = None;
for action in std::iter::once(root.clone()).chain(root.chain(r, limits, diags)) {
if action.kind() != ActionKind::Hide {
continue;
}
let form = form.get_or_insert_with(|| Form::load(catalog, r, limits, diags));
let Some(form) = form.as_ref() else {
continue;
};
apply_hide(&action, form, &mut hidden, r);
}
hidden
}
fn apply_hide<R: Resolve>(action: &Action, form: &Form, hidden: &mut Hidden, r: &R) {
let hide = action.hide_status();
for named in action.fields(r) {
let Object::Str(text) = named else {
continue;
};
let name = decode_text(&text.bytes).into_owned();
let Some(field) = form.field(&name) else {
continue;
};
for widget in &field.widgets {
let flags = hidden.flags(&widget.dict, r).without(CLEARED_BY_HIDE);
let flags = if hide {
flags.with(AnnotFlags::HIDDEN)
} else {
flags.without(AnnotFlags::HIDDEN)
};
set(hidden, &widget.dict, flags);
}
}
}
fn set(hidden: &mut Hidden, dict: &Dict, flags: AnnotFlags) {
match hidden
.touched
.iter_mut()
.find(|(touched, _)| touched == dict)
{
Some((_, slot)) => *slot = flags,
None => hidden.touched.push((dict.clone(), flags)),
}
}
#[cfg(test)]
mod tests {
use super::{Hidden, hidden_by_open_action};
use crate::annot::AnnotFlags;
use pdfrum_common::{Diagnostics, Limits};
use pdfrum_object::{Array, Dict, Name, NoResolve, Object, PdfString};
fn dict(pairs: &[(&str, Object)]) -> Dict {
Dict::from_pairs(
pairs
.iter()
.map(|(k, v)| (Name::from(*k), v.clone()))
.collect::<Vec<_>>(),
)
}
fn catalog(widget: Dict, open_action: Option<Object>) -> (Dict, Dict) {
let mut pairs = vec![(
"AcroForm",
Object::Dict(dict(&[(
"Fields",
Object::Array(Array::of([Object::Dict(widget.clone())])),
)])),
)];
if let Some(action) = open_action {
pairs.push(("OpenAction", action));
}
(dict(&pairs), widget)
}
fn field_widget(flags: i64) -> Dict {
dict(&[
("Type", Object::Name(Name::from("Annot"))),
("Subtype", Object::Name(Name::from("Widget"))),
("FT", Object::Name(Name::from("Tx"))),
("T", Object::Str(PdfString::literal(b"f"))),
("F", Object::Int(flags)),
])
}
fn hide_action(names: &[&str], h: Option<bool>) -> Object {
let mut pairs = vec![
("Type", Object::Name(Name::from("Action"))),
("S", Object::Name(Name::from("Hide"))),
(
"T",
Object::Array(Array::of(
names
.iter()
.map(|n| Object::Str(PdfString::literal(n.as_bytes()))),
)),
),
];
if let Some(h) = h {
pairs.push(("H", Object::Bool(h)));
}
Object::Dict(dict(&pairs))
}
fn run(catalog: &Dict) -> Hidden {
let (limits, mut diags) = (Limits::default(), Diagnostics::default());
hidden_by_open_action(catalog, &NoResolve, &limits, &mut diags)
}
#[test]
fn a_document_with_no_open_action_hides_nothing() {
let (cat, widget) = catalog(field_widget(4), None);
let hidden = run(&cat);
assert!(hidden.is_empty());
assert_eq!(hidden.flags(&widget, &NoResolve), AnnotFlags::from_bits(4));
}
#[test]
fn a_hide_action_with_no_h_key_hides() {
let (cat, widget) = catalog(field_widget(4), Some(hide_action(&["f"], None)));
let hidden = run(&cat);
assert_eq!(
hidden.flags(&widget, &NoResolve),
AnnotFlags::from_bits(4 | 2)
);
}
#[test]
fn showing_clears_the_hidden_bit_and_the_two_others() {
let (cat, widget) = catalog(
field_widget(1 | 2 | 32),
Some(hide_action(&["f"], Some(false))),
);
let hidden = run(&cat);
assert_eq!(hidden.flags(&widget, &NoResolve), AnnotFlags::from_bits(0));
}
#[test]
fn hiding_also_clears_invisible_and_no_view() {
let (cat, widget) = catalog(
field_widget(1 | 32 | 4),
Some(hide_action(&["f"], Some(true))),
);
let hidden = run(&cat);
assert_eq!(
hidden.flags(&widget, &NoResolve),
AnnotFlags::from_bits(4 | 2)
);
}
#[test]
fn a_name_no_field_carries_touches_nothing() {
let (cat, _) = catalog(field_widget(4), Some(hide_action(&["other"], None)));
assert!(run(&cat).is_empty());
}
#[test]
fn only_a_string_names_a_field() {
let widget = field_widget(4);
let action = Object::Dict(dict(&[
("Type", Object::Name(Name::from("Action"))),
("S", Object::Name(Name::from("Hide"))),
(
"T",
Object::Array(Array::of([Object::Dict(widget.clone())])),
),
]));
let (cat, _) = catalog(widget, Some(action));
assert!(run(&cat).is_empty());
}
#[test]
fn an_open_action_that_is_not_a_hide_leaves_every_flag_alone() {
let goto = Object::Dict(dict(&[
("Type", Object::Name(Name::from("Action"))),
("S", Object::Name(Name::from("GoTo"))),
]));
let (cat, _) = catalog(field_widget(4), Some(goto));
assert!(run(&cat).is_empty());
}
#[test]
fn a_hide_behind_a_next_chain_still_runs() {
let goto = Object::Dict(dict(&[
("Type", Object::Name(Name::from("Action"))),
("S", Object::Name(Name::from("GoTo"))),
("Next", hide_action(&["f"], None)),
]));
let (cat, widget) = catalog(field_widget(0), Some(goto));
assert_eq!(
run(&cat).flags(&widget, &NoResolve),
AnnotFlags::from_bits(2)
);
}
#[test]
fn a_destination_array_open_action_is_not_an_action() {
let (cat, _) = catalog(
field_widget(4),
Some(Object::Array(Array::of([Object::Int(0)]))),
);
assert!(run(&cat).is_empty());
}
}