1use kurbo::Affine;
17use pdfrum_object::{Dict, Name, Object, Resolve, Stream};
18
19use crate::annot::{Annotation, Subtype};
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
158fn keeps_upright(annot: &Annotation) -> bool {
166 annot.flags.no_rotate() || annot.subtype == Subtype::Text
167}
168
169#[must_use]
189pub(crate) fn annot_matrix(
190 annot: &Annotation,
191 form_dict: &Dict,
192 page_quarter_turns: u8,
193 user_to_device: Affine,
194 r: &impl Resolve,
195) -> Affine {
196 let form_matrix = form_dict.matrix(names::MATRIX, r);
197 let form_bbox = geom::transform_rect(form_matrix, form_dict.rect(names::BBOX, r));
198 let rect = geom::normalize(annot.rect);
199 let mut matrix = geom::match_rect(rect, form_bbox);
200
201 if keeps_upright(annot) && page_quarter_turns != 0 {
202 let (ox, oy) = (rect.x0, rect.y1);
203 let angle = std::f64::consts::FRAC_PI_2 * f64::from(page_quarter_turns);
204 matrix = Affine::translate((ox, oy))
205 * Affine::rotate(angle)
206 * Affine::translate((-ox, -oy))
207 * matrix;
208 }
209 user_to_device * matrix
210}
211
212#[cfg(test)]
213mod tests {
214 use super::{ApMode, annot_ap, has_appearance};
215 use pdfrum_object::{ByteSpan, Dict, Name, NoResolve, Object, PdfString, Stream};
216
217 fn dict(pairs: &[(&str, Object)]) -> Dict {
218 Dict::from_pairs(
219 pairs
220 .iter()
221 .map(|(k, v)| (Name::from(*k), v.clone()))
222 .collect::<Vec<_>>(),
223 )
224 }
225
226 fn stream(marker: &[u8]) -> Object {
227 Object::Stream(Box::new(Stream::new(
228 Dict::new(),
229 ByteSpan::from(marker.to_vec()),
230 )))
231 }
232
233 fn found(annot: &Dict, mode: ApMode, fallback: bool) -> Option<Vec<u8>> {
234 annot_ap(annot, mode, fallback, &NoResolve).map(|s| s.data.as_bytes().to_vec())
235 }
236
237 #[test]
238 fn no_appearance_dictionary_finds_nothing() {
239 assert_eq!(found(&Dict::new(), ApMode::Normal, true), None);
240 assert_eq!(
241 found(&dict(&[("AP", Object::Int(4))]), ApMode::Normal, true),
242 None
243 );
244 }
245
246 #[test]
247 fn a_direct_stream_is_the_answer() {
248 let annot = dict(&[("AP", Object::Dict(dict(&[("N", stream(b"body"))])))]);
249 assert_eq!(found(&annot, ApMode::Normal, true), Some(b"body".to_vec()));
250 }
251
252 #[test]
253 fn the_mode_fallback_tests_presence_not_usability() {
254 let ap_missing_r = dict(&[("AP", Object::Dict(dict(&[("N", stream(b"n"))])))]);
255 assert_eq!(
256 found(&ap_missing_r, ApMode::Rollover, true),
257 Some(b"n".to_vec())
258 );
259 assert_eq!(found(&ap_missing_r, ApMode::Rollover, false), None);
260
261 let ap_null_r = dict(&[(
263 "AP",
264 Object::Dict(dict(&[("N", stream(b"n")), ("R", Object::Null)])),
265 )]);
266 assert_eq!(found(&ap_null_r, ApMode::Rollover, true), None);
267 }
268
269 #[test]
270 fn a_state_dictionary_is_chosen_by_the_appearance_state() {
271 let states = dict(&[("Yes", stream(b"on")), ("Off", stream(b"off"))]);
272 let with_as = dict(&[
273 (
274 "AP",
275 Object::Dict(dict(&[("N", Object::Dict(states.clone()))])),
276 ),
277 ("AS", Object::Name(Name::from("Yes"))),
278 ]);
279 assert_eq!(found(&with_as, ApMode::Normal, true), Some(b"on".to_vec()));
280
281 let with_v = dict(&[
284 (
285 "AP",
286 Object::Dict(dict(&[("N", Object::Dict(states.clone()))])),
287 ),
288 ("V", Object::Str(PdfString::literal(b"Yes"))),
289 ]);
290 assert_eq!(found(&with_v, ApMode::Normal, true), Some(b"on".to_vec()));
291
292 let wrong_v = dict(&[
295 (
296 "AP",
297 Object::Dict(dict(&[("N", Object::Dict(states.clone()))])),
298 ),
299 ("V", Object::Name(Name::from("Nope"))),
300 ]);
301 assert_eq!(found(&wrong_v, ApMode::Normal, true), Some(b"off".to_vec()));
302
303 let bare = dict(&[("AP", Object::Dict(dict(&[("N", Object::Dict(states))])))]);
305 assert_eq!(found(&bare, ApMode::Normal, true), Some(b"off".to_vec()));
306 }
307
308 #[test]
309 fn the_parent_is_consulted_one_level_only() {
310 let states = dict(&[("Yes", stream(b"on")), ("Off", stream(b"off"))]);
311 let grandparent = dict(&[("V", Object::Name(Name::from("Yes")))]);
312 let parent = dict(&[("Parent", Object::Dict(grandparent))]);
313 let annot = dict(&[
314 (
315 "AP",
316 Object::Dict(dict(&[("N", Object::Dict(states.clone()))])),
317 ),
318 ("Parent", Object::Dict(parent)),
319 ]);
320 assert_eq!(found(&annot, ApMode::Normal, true), Some(b"off".to_vec()));
322
323 let direct_parent = dict(&[("V", Object::Name(Name::from("Yes")))]);
324 let annot = dict(&[
325 ("AP", Object::Dict(dict(&[("N", Object::Dict(states))]))),
326 ("Parent", Object::Dict(direct_parent)),
327 ]);
328 assert_eq!(found(&annot, ApMode::Normal, true), Some(b"on".to_vec()));
329 }
330
331 #[test]
332 fn a_non_stream_state_value_yields_nothing() {
333 let states = dict(&[("Yes", Object::Int(5))]);
334 let annot = dict(&[
335 ("AP", Object::Dict(dict(&[("N", Object::Dict(states))]))),
336 ("AS", Object::Name(Name::from("Yes"))),
337 ]);
338 assert_eq!(found(&annot, ApMode::Normal, true), None);
339 }
340
341 #[test]
342 fn a_stream_normal_appearance_counts_as_having_one() {
343 let with_stream = dict(&[("AP", Object::Dict(dict(&[("N", stream(b"x"))])))]);
344 assert!(has_appearance(&with_stream, &NoResolve));
345
346 let with_states = dict(&[(
347 "AP",
348 Object::Dict(dict(&[("N", Object::Dict(dict(&[("Yes", stream(b"x"))])))])),
349 )]);
350 assert!(has_appearance(&with_states, &NoResolve));
351
352 let scalar = dict(&[("AP", Object::Dict(dict(&[("N", Object::Int(5))])))]);
354 assert!(!has_appearance(&scalar, &NoResolve));
355 assert!(!has_appearance(&Dict::new(), &NoResolve));
356 }
357
358 #[test]
362 fn no_rotate_counter_rotates_about_the_top_left_corner() {
363 use super::annot_matrix;
364 use crate::annot::Annotation;
365 use kurbo::{Affine, Point};
366
367 let ints = |values: &[i64]| {
368 Object::Array(pdfrum_object::Array::of(
369 values.iter().copied().map(Object::Int),
370 ))
371 };
372 let annot = Annotation::read(
373 &dict(&[
374 ("Subtype", Object::Name(Name::from("Stamp"))),
375 ("Rect", ints(&[100, 200, 140, 260])),
376 ("F", Object::Int(16)),
377 ]),
378 &NoResolve,
379 );
380 let form = dict(&[("BBox", ints(&[0, 0, 40, 60]))]);
381
382 let upright = annot_matrix(&annot, &form, 0, Affine::IDENTITY, &NoResolve);
383 let turned = annot_matrix(&annot, &form, 1, Affine::IDENTITY, &NoResolve);
384
385 let corner = Point::new(100.0, 260.0);
387 let top_left = Point::new(0.0, 60.0);
388 for matrix in [upright, turned] {
389 let got = matrix * top_left;
390 assert!(got.distance(corner) < 1e-9, "{got:?}");
391 }
392 let far = turned * Point::new(40.0, 0.0);
394 assert!(
395 far.distance(Point::new(100.0 + 60.0, 260.0 + 40.0)) < 1e-9,
396 "{far:?}"
397 );
398
399 let plain = Annotation::read(&dict(&[("Rect", ints(&[100, 200, 140, 260]))]), &NoResolve);
401 let plain = annot_matrix(&plain, &form, 1, Affine::IDENTITY, &NoResolve);
402 let unturned = upright * Point::new(40.0, 0.0);
403 assert!((plain * Point::new(40.0, 0.0)).distance(unturned) < 1e-9);
404 assert!(
405 far.distance(unturned) > 1.0,
406 "the turn moved the far corner"
407 );
408 }
409}