1use kurbo::Affine;
17use pdfrum_object::{Dict, Name, Object, Resolve, Stream};
18
19use crate::annot::Annotation;
20use crate::geom;
21use crate::names;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43pub enum ApMode {
44 #[default]
46 Normal,
47 Rollover,
49 Down,
51}
52
53impl ApMode {
54 fn key(self) -> &'static Name {
56 match self {
57 ApMode::Normal => names::N,
58 ApMode::Rollover => names::R,
59 ApMode::Down => names::D,
60 }
61 }
62}
63
64#[must_use]
89pub fn annot_ap<R: Resolve>(
90 dict: &Dict,
91 mode: ApMode,
92 fallback_to_normal: bool,
93 r: &R,
94) -> Option<Stream> {
95 let ap = dict.dict(names::AP, r)?;
96 let mut entry = mode.key();
97 if fallback_to_normal && !ap.contains_key(entry) {
98 entry = names::N;
99 }
100 let sub = ap.get(entry, r)?.get().clone();
101 if let Object::Stream(stream) = sub {
102 return Some(*stream);
103 }
104 let states = sub.as_dict()?;
105
106 let mut state = dict.byte_string(names::AS, r).unwrap_or_default();
107 if state.is_empty() {
108 let mut value = dict.byte_string(names::V, r).unwrap_or_default();
111 if value.is_empty() {
112 value = dict
113 .dict(names::PARENT, r)
114 .and_then(|parent| parent.byte_string(names::V, r))
115 .unwrap_or_default();
116 }
117 state = if !value.is_empty() && states.contains_key(&Name::new(value.clone())) {
120 value
121 } else {
122 names::OFF.as_bytes().to_vec()
123 };
124 }
125 states.stream(&Name::new(state), r)
126}
127
128#[must_use]
152pub fn has_appearance<R: Resolve>(dict: &Dict, r: &R) -> bool {
153 dict.dict(names::AP, r)
154 .and_then(|ap| ap.dict(names::N, r))
155 .is_some()
156}
157
158#[must_use]
165pub(crate) fn annot_matrix(
166 annot: &Annotation,
167 form_dict: &Dict,
168 page_quarter_turns: u8,
169 user_to_device: Affine,
170 r: &impl Resolve,
171) -> Affine {
172 let form_matrix = form_dict.matrix(names::MATRIX, r);
173 let form_bbox = geom::transform_rect(form_matrix, form_dict.rect(names::BBOX, r));
174 let rect = geom::normalize(annot.rect);
175 let mut matrix = geom::match_rect(rect, form_bbox);
176
177 if annot.flags.no_rotate() && page_quarter_turns != 0 {
178 let (ox, oy) = (rect.x0, rect.y1);
179 let angle = std::f64::consts::FRAC_PI_2 * f64::from(page_quarter_turns);
180 matrix = matrix
181 * Affine::translate((-ox, -oy))
182 * Affine::rotate(angle)
183 * Affine::translate((ox, oy));
184 }
185 matrix * user_to_device
186}
187
188#[cfg(test)]
189mod tests {
190 use super::{ApMode, annot_ap, has_appearance};
191 use pdfrum_object::{ByteSpan, Dict, Name, NoResolve, Object, PdfString, Stream};
192
193 fn dict(pairs: &[(&str, Object)]) -> Dict {
194 Dict::from_pairs(
195 pairs
196 .iter()
197 .map(|(k, v)| (Name::from(*k), v.clone()))
198 .collect::<Vec<_>>(),
199 )
200 }
201
202 fn stream(marker: &[u8]) -> Object {
203 Object::Stream(Box::new(Stream::new(
204 Dict::new(),
205 ByteSpan::from(marker.to_vec()),
206 )))
207 }
208
209 fn found(annot: &Dict, mode: ApMode, fallback: bool) -> Option<Vec<u8>> {
210 annot_ap(annot, mode, fallback, &NoResolve).map(|s| s.data.as_bytes().to_vec())
211 }
212
213 #[test]
214 fn no_appearance_dictionary_finds_nothing() {
215 assert_eq!(found(&Dict::new(), ApMode::Normal, true), None);
216 assert_eq!(
217 found(&dict(&[("AP", Object::Int(4))]), ApMode::Normal, true),
218 None
219 );
220 }
221
222 #[test]
223 fn a_direct_stream_is_the_answer() {
224 let annot = dict(&[("AP", Object::Dict(dict(&[("N", stream(b"body"))])))]);
225 assert_eq!(found(&annot, ApMode::Normal, true), Some(b"body".to_vec()));
226 }
227
228 #[test]
229 fn the_mode_fallback_tests_presence_not_usability() {
230 let ap_missing_r = dict(&[("AP", Object::Dict(dict(&[("N", stream(b"n"))])))]);
231 assert_eq!(
232 found(&ap_missing_r, ApMode::Rollover, true),
233 Some(b"n".to_vec())
234 );
235 assert_eq!(found(&ap_missing_r, ApMode::Rollover, false), None);
236
237 let ap_null_r = dict(&[(
239 "AP",
240 Object::Dict(dict(&[("N", stream(b"n")), ("R", Object::Null)])),
241 )]);
242 assert_eq!(found(&ap_null_r, ApMode::Rollover, true), None);
243 }
244
245 #[test]
246 fn a_state_dictionary_is_chosen_by_the_appearance_state() {
247 let states = dict(&[("Yes", stream(b"on")), ("Off", stream(b"off"))]);
248 let with_as = dict(&[
249 (
250 "AP",
251 Object::Dict(dict(&[("N", Object::Dict(states.clone()))])),
252 ),
253 ("AS", Object::Name(Name::from("Yes"))),
254 ]);
255 assert_eq!(found(&with_as, ApMode::Normal, true), Some(b"on".to_vec()));
256
257 let with_v = dict(&[
260 (
261 "AP",
262 Object::Dict(dict(&[("N", Object::Dict(states.clone()))])),
263 ),
264 ("V", Object::Str(PdfString::literal(b"Yes"))),
265 ]);
266 assert_eq!(found(&with_v, ApMode::Normal, true), Some(b"on".to_vec()));
267
268 let wrong_v = dict(&[
271 (
272 "AP",
273 Object::Dict(dict(&[("N", Object::Dict(states.clone()))])),
274 ),
275 ("V", Object::Name(Name::from("Nope"))),
276 ]);
277 assert_eq!(found(&wrong_v, ApMode::Normal, true), Some(b"off".to_vec()));
278
279 let bare = dict(&[("AP", Object::Dict(dict(&[("N", Object::Dict(states))])))]);
281 assert_eq!(found(&bare, ApMode::Normal, true), Some(b"off".to_vec()));
282 }
283
284 #[test]
285 fn the_parent_is_consulted_one_level_only() {
286 let states = dict(&[("Yes", stream(b"on")), ("Off", stream(b"off"))]);
287 let grandparent = dict(&[("V", Object::Name(Name::from("Yes")))]);
288 let parent = dict(&[("Parent", Object::Dict(grandparent))]);
289 let annot = dict(&[
290 (
291 "AP",
292 Object::Dict(dict(&[("N", Object::Dict(states.clone()))])),
293 ),
294 ("Parent", Object::Dict(parent)),
295 ]);
296 assert_eq!(found(&annot, ApMode::Normal, true), Some(b"off".to_vec()));
298
299 let direct_parent = dict(&[("V", Object::Name(Name::from("Yes")))]);
300 let annot = dict(&[
301 ("AP", Object::Dict(dict(&[("N", Object::Dict(states))]))),
302 ("Parent", Object::Dict(direct_parent)),
303 ]);
304 assert_eq!(found(&annot, ApMode::Normal, true), Some(b"on".to_vec()));
305 }
306
307 #[test]
308 fn a_non_stream_state_value_yields_nothing() {
309 let states = dict(&[("Yes", Object::Int(5))]);
310 let annot = dict(&[
311 ("AP", Object::Dict(dict(&[("N", Object::Dict(states))]))),
312 ("AS", Object::Name(Name::from("Yes"))),
313 ]);
314 assert_eq!(found(&annot, ApMode::Normal, true), None);
315 }
316
317 #[test]
318 fn a_stream_normal_appearance_counts_as_having_one() {
319 let with_stream = dict(&[("AP", Object::Dict(dict(&[("N", stream(b"x"))])))]);
320 assert!(has_appearance(&with_stream, &NoResolve));
321
322 let with_states = dict(&[(
323 "AP",
324 Object::Dict(dict(&[("N", Object::Dict(dict(&[("Yes", stream(b"x"))])))])),
325 )]);
326 assert!(has_appearance(&with_states, &NoResolve));
327
328 let scalar = dict(&[("AP", Object::Dict(dict(&[("N", Object::Int(5))])))]);
330 assert!(!has_appearance(&scalar, &NoResolve));
331 assert!(!has_appearance(&Dict::new(), &NoResolve));
332 }
333}