use kurbo::Affine;
use pdfrum_object::{Dict, Name, Object, Resolve, Stream};
use crate::annot::Annotation;
use crate::geom;
use crate::names;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ApMode {
#[default]
Normal,
Rollover,
Down,
}
impl ApMode {
fn key(self) -> &'static Name {
match self {
ApMode::Normal => names::N,
ApMode::Rollover => names::R,
ApMode::Down => names::D,
}
}
}
#[must_use]
pub fn annot_ap<R: Resolve>(
dict: &Dict,
mode: ApMode,
fallback_to_normal: bool,
r: &R,
) -> Option<Stream> {
let ap = dict.dict(names::AP, r)?;
let mut entry = mode.key();
if fallback_to_normal && !ap.contains_key(entry) {
entry = names::N;
}
let sub = ap.get(entry, r)?.get().clone();
if let Object::Stream(stream) = sub {
return Some(*stream);
}
let states = sub.as_dict()?;
let mut state = dict.byte_string(names::AS, r).unwrap_or_default();
if state.is_empty() {
let mut value = dict.byte_string(names::V, r).unwrap_or_default();
if value.is_empty() {
value = dict
.dict(names::PARENT, r)
.and_then(|parent| parent.byte_string(names::V, r))
.unwrap_or_default();
}
state = if !value.is_empty() && states.contains_key(&Name::new(value.clone())) {
value
} else {
names::OFF.as_bytes().to_vec()
};
}
states.stream(&Name::new(state), r)
}
#[must_use]
pub fn has_appearance<R: Resolve>(dict: &Dict, r: &R) -> bool {
dict.dict(names::AP, r)
.and_then(|ap| ap.dict(names::N, r))
.is_some()
}
#[must_use]
pub(crate) fn annot_matrix(
annot: &Annotation,
form_dict: &Dict,
page_quarter_turns: u8,
user_to_device: Affine,
r: &impl Resolve,
) -> Affine {
let form_matrix = form_dict.matrix(names::MATRIX, r);
let form_bbox = geom::transform_rect(form_matrix, form_dict.rect(names::BBOX, r));
let rect = geom::normalize(annot.rect);
let mut matrix = geom::match_rect(rect, form_bbox);
if annot.flags.no_rotate() && page_quarter_turns != 0 {
let (ox, oy) = (rect.x0, rect.y1);
let angle = std::f64::consts::FRAC_PI_2 * f64::from(page_quarter_turns);
matrix = matrix
* Affine::translate((-ox, -oy))
* Affine::rotate(angle)
* Affine::translate((ox, oy));
}
matrix * user_to_device
}
#[cfg(test)]
mod tests {
use super::{ApMode, annot_ap, has_appearance};
use pdfrum_object::{ByteSpan, Dict, Name, NoResolve, Object, PdfString, Stream};
fn dict(pairs: &[(&str, Object)]) -> Dict {
Dict::from_pairs(
pairs
.iter()
.map(|(k, v)| (Name::from(*k), v.clone()))
.collect::<Vec<_>>(),
)
}
fn stream(marker: &[u8]) -> Object {
Object::Stream(Box::new(Stream::new(
Dict::new(),
ByteSpan::from(marker.to_vec()),
)))
}
fn found(annot: &Dict, mode: ApMode, fallback: bool) -> Option<Vec<u8>> {
annot_ap(annot, mode, fallback, &NoResolve).map(|s| s.data.as_bytes().to_vec())
}
#[test]
fn no_appearance_dictionary_finds_nothing() {
assert_eq!(found(&Dict::new(), ApMode::Normal, true), None);
assert_eq!(
found(&dict(&[("AP", Object::Int(4))]), ApMode::Normal, true),
None
);
}
#[test]
fn a_direct_stream_is_the_answer() {
let annot = dict(&[("AP", Object::Dict(dict(&[("N", stream(b"body"))])))]);
assert_eq!(found(&annot, ApMode::Normal, true), Some(b"body".to_vec()));
}
#[test]
fn the_mode_fallback_tests_presence_not_usability() {
let ap_missing_r = dict(&[("AP", Object::Dict(dict(&[("N", stream(b"n"))])))]);
assert_eq!(
found(&ap_missing_r, ApMode::Rollover, true),
Some(b"n".to_vec())
);
assert_eq!(found(&ap_missing_r, ApMode::Rollover, false), None);
let ap_null_r = dict(&[(
"AP",
Object::Dict(dict(&[("N", stream(b"n")), ("R", Object::Null)])),
)]);
assert_eq!(found(&ap_null_r, ApMode::Rollover, true), None);
}
#[test]
fn a_state_dictionary_is_chosen_by_the_appearance_state() {
let states = dict(&[("Yes", stream(b"on")), ("Off", stream(b"off"))]);
let with_as = dict(&[
(
"AP",
Object::Dict(dict(&[("N", Object::Dict(states.clone()))])),
),
("AS", Object::Name(Name::from("Yes"))),
]);
assert_eq!(found(&with_as, ApMode::Normal, true), Some(b"on".to_vec()));
let with_v = dict(&[
(
"AP",
Object::Dict(dict(&[("N", Object::Dict(states.clone()))])),
),
("V", Object::Str(PdfString::literal(b"Yes"))),
]);
assert_eq!(found(&with_v, ApMode::Normal, true), Some(b"on".to_vec()));
let wrong_v = dict(&[
(
"AP",
Object::Dict(dict(&[("N", Object::Dict(states.clone()))])),
),
("V", Object::Name(Name::from("Nope"))),
]);
assert_eq!(found(&wrong_v, ApMode::Normal, true), Some(b"off".to_vec()));
let bare = dict(&[("AP", Object::Dict(dict(&[("N", Object::Dict(states))])))]);
assert_eq!(found(&bare, ApMode::Normal, true), Some(b"off".to_vec()));
}
#[test]
fn the_parent_is_consulted_one_level_only() {
let states = dict(&[("Yes", stream(b"on")), ("Off", stream(b"off"))]);
let grandparent = dict(&[("V", Object::Name(Name::from("Yes")))]);
let parent = dict(&[("Parent", Object::Dict(grandparent))]);
let annot = dict(&[
(
"AP",
Object::Dict(dict(&[("N", Object::Dict(states.clone()))])),
),
("Parent", Object::Dict(parent)),
]);
assert_eq!(found(&annot, ApMode::Normal, true), Some(b"off".to_vec()));
let direct_parent = dict(&[("V", Object::Name(Name::from("Yes")))]);
let annot = dict(&[
("AP", Object::Dict(dict(&[("N", Object::Dict(states))]))),
("Parent", Object::Dict(direct_parent)),
]);
assert_eq!(found(&annot, ApMode::Normal, true), Some(b"on".to_vec()));
}
#[test]
fn a_non_stream_state_value_yields_nothing() {
let states = dict(&[("Yes", Object::Int(5))]);
let annot = dict(&[
("AP", Object::Dict(dict(&[("N", Object::Dict(states))]))),
("AS", Object::Name(Name::from("Yes"))),
]);
assert_eq!(found(&annot, ApMode::Normal, true), None);
}
#[test]
fn a_stream_normal_appearance_counts_as_having_one() {
let with_stream = dict(&[("AP", Object::Dict(dict(&[("N", stream(b"x"))])))]);
assert!(has_appearance(&with_stream, &NoResolve));
let with_states = dict(&[(
"AP",
Object::Dict(dict(&[("N", Object::Dict(dict(&[("Yes", stream(b"x"))])))])),
)]);
assert!(has_appearance(&with_states, &NoResolve));
let scalar = dict(&[("AP", Object::Dict(dict(&[("N", Object::Int(5))])))]);
assert!(!has_appearance(&scalar, &NoResolve));
assert!(!has_appearance(&Dict::new(), &NoResolve));
}
}