1use crate::names::{name_tree_root_with, NameTree};
6use crate::object::{Dict, ObjRef, Object};
7use crate::source::AsyncObjectSource;
8use crate::tree::{self, resolved_dict};
9
10#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum DestinationPage {
15 Object(ObjRef),
17 Number(u32),
20}
21
22#[derive(Debug, Clone, Copy, PartialEq)]
28pub enum Fit {
29 Xyz {
32 left: Option<f32>,
33 top: Option<f32>,
34 zoom: Option<f32>,
35 },
36 Fit,
38 FitH { top: Option<f32> },
40 FitV { left: Option<f32> },
42 FitR {
44 left: f32,
45 bottom: f32,
46 right: f32,
47 top: f32,
48 },
49 FitB,
51 FitBH { top: Option<f32> },
53 FitBV { left: Option<f32> },
55}
56
57#[derive(Debug, Clone, Copy, PartialEq)]
61pub struct Destination {
62 pub page: DestinationPage,
63 pub fit: Fit,
64}
65
66impl Destination {
67 pub fn parse(array: &[Object]) -> Option<Destination> {
74 let page = match array.first()? {
75 Object::Ref(r) => DestinationPage::Object(*r),
76 Object::Int(n) => DestinationPage::Number(u32::try_from(*n).ok()?),
77 _ => return None,
78 };
79 let fit = array.get(1)?.as_name()?.0.as_str();
80 let optional = |i: usize| -> Option<Option<f32>> {
81 match array.get(i) {
82 None | Some(Object::Null) => Some(None),
83 Some(o) => Some(Some(o.as_f64()? as f32)),
84 }
85 };
86 let required = |i: usize| -> Option<f32> { Some(array.get(i)?.as_f64()? as f32) };
87 let fit = match fit {
88 "XYZ" => Fit::Xyz {
89 left: optional(2)?,
90 top: optional(3)?,
91 zoom: optional(4)?.filter(|z| *z != 0.0),
92 },
93 "Fit" => Fit::Fit,
94 "FitH" => Fit::FitH { top: optional(2)? },
95 "FitV" => Fit::FitV { left: optional(2)? },
96 "FitR" => Fit::FitR {
97 left: required(2)?,
98 bottom: required(3)?,
99 right: required(4)?,
100 top: required(5)?,
101 },
102 "FitB" => Fit::FitB,
103 "FitBH" => Fit::FitBH { top: optional(2)? },
104 "FitBV" => Fit::FitBV { left: optional(2)? },
105 _ => return None,
106 };
107 Some(Destination { page, fit })
108 }
109}
110
111pub async fn destination_with<S: AsyncObjectSource>(
117 src: &S,
118 object: &Object,
119) -> Option<Destination> {
120 let Ok(Object::Array(items)) = src.resolve(object).await else {
121 return None;
122 };
123 let mut direct = Vec::with_capacity(items.len());
124 for (i, item) in items.iter().enumerate() {
125 let resolved = match item {
128 Object::Ref(_) if i > 0 => src.resolve(item).await.ok()?,
129 other => other.clone(),
130 };
131 direct.push(resolved);
132 }
133 Destination::parse(&direct)
134}
135
136pub async fn named_destination_with<S: AsyncObjectSource>(
143 src: &S,
144 trailer: &Dict,
145 name: &[u8],
146) -> Option<Destination> {
147 if let Some(root) = name_tree_root_with(src, trailer, NameTree::Dests).await {
148 if let Some(value) = tree::lookup(src, &root, &name.to_vec()).await {
149 if let Some(found) = destination_entry_with(src, &value).await {
150 return Some(found);
151 }
152 }
153 }
154 let dests = catalog_dests_with(src, trailer).await?;
155 let key = std::str::from_utf8(name).ok()?;
156 destination_entry_with(src, dests.get(key)?).await
157}
158
159async fn catalog_dests_with<S: AsyncObjectSource>(src: &S, trailer: &Dict) -> Option<Dict> {
161 let catalog = resolved_dict(src, trailer.get("Root")?).await?;
162 resolved_dict(src, catalog.get("Dests")?).await
163}
164
165async fn destination_entry_with<S: AsyncObjectSource>(
168 src: &S,
169 value: &Object,
170) -> Option<Destination> {
171 match src.resolve(value).await.ok()? {
172 Object::Array(_) => destination_with(src, value).await,
173 Object::Dict(d) => destination_with(src, d.get("D")?).await,
174 _ => None,
175 }
176}
177
178pub async fn named_destinations_with<S: AsyncObjectSource>(
184 src: &S,
185 trailer: &Dict,
186) -> Vec<(Vec<u8>, Destination)> {
187 let mut found: Vec<(Vec<u8>, Destination)> = Vec::new();
188 if let Some(root) = name_tree_root_with(src, trailer, NameTree::Dests).await {
189 for (name, value) in tree::entries::<Vec<u8>, S>(src, &root).await {
190 if let Some(destination) = destination_entry_with(src, &value).await {
191 found.push((name, destination));
192 }
193 }
194 }
195 if let Some(dests) = catalog_dests_with(src, trailer).await {
196 for (name, value) in dests.iter() {
197 let key = name.0.as_bytes().to_vec();
198 if found.iter().any(|(known, _)| *known == key) {
199 continue;
200 }
201 if let Some(destination) = destination_entry_with(src, value).await {
202 found.push((key, destination));
203 }
204 }
205 }
206 found.sort_by(|a, b| a.0.cmp(&b.0));
207 found
208}
209
210pub async fn destination_value_with<S: AsyncObjectSource>(
216 src: &S,
217 trailer: &Dict,
218 value: &Object,
219) -> Option<Destination> {
220 let mut current = src.resolve(value).await.ok()?;
221 if let Object::Dict(d) = ¤t {
222 current = src.resolve(d.get("D")?).await.ok()?;
223 }
224 match current {
225 Object::Array(items) => destination_with(src, &Object::Array(items)).await,
226 Object::Name(name) => named_destination_with(src, trailer, name.0.as_bytes()).await,
227 Object::String(name) => named_destination_with(src, trailer, &name).await,
228 _ => None,
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use crate::object::Name;
236 use crate::{block_on, Document, Immediate};
237 use pdfboss_testkit::PdfBuilder;
238
239 fn page() -> Object {
240 Object::Ref(ObjRef { num: 3, gen: 0 })
241 }
242
243 fn name(n: &str) -> Object {
244 Object::Name(Name(n.into()))
245 }
246
247 fn parse(items: Vec<Object>) -> Option<Destination> {
248 Destination::parse(&items)
249 }
250
251 #[test]
253 fn every_table_151_form_parses() {
254 let page_ref = DestinationPage::Object(ObjRef { num: 3, gen: 0 });
255 let cases = [
256 (
257 vec![
258 page(),
259 name("XYZ"),
260 Object::Int(10),
261 Object::Real(700.5),
262 Object::Real(1.5),
263 ],
264 Fit::Xyz {
265 left: Some(10.0),
266 top: Some(700.5),
267 zoom: Some(1.5),
268 },
269 ),
270 (vec![page(), name("Fit")], Fit::Fit),
271 (
272 vec![page(), name("FitH"), Object::Int(700)],
273 Fit::FitH { top: Some(700.0) },
274 ),
275 (
276 vec![page(), name("FitV"), Object::Int(72)],
277 Fit::FitV { left: Some(72.0) },
278 ),
279 (
280 vec![
281 page(),
282 name("FitR"),
283 Object::Int(10),
284 Object::Int(20),
285 Object::Int(300),
286 Object::Int(400),
287 ],
288 Fit::FitR {
289 left: 10.0,
290 bottom: 20.0,
291 right: 300.0,
292 top: 400.0,
293 },
294 ),
295 (vec![page(), name("FitB")], Fit::FitB),
296 (
297 vec![page(), name("FitBH"), Object::Int(650)],
298 Fit::FitBH { top: Some(650.0) },
299 ),
300 (
301 vec![page(), name("FitBV"), Object::Int(36)],
302 Fit::FitBV { left: Some(36.0) },
303 ),
304 ];
305 for (array, fit) in cases {
306 assert_eq!(
307 parse(array.clone()),
308 Some(Destination {
309 page: page_ref,
310 fit
311 }),
312 "{array:?}"
313 );
314 }
315 }
316
317 #[test]
319 fn null_zero_zoom_and_missing_parameters_keep_the_current_value() {
320 let xyz = |left, top, zoom| {
321 Some(Destination {
322 page: DestinationPage::Object(ObjRef { num: 3, gen: 0 }),
323 fit: Fit::Xyz { left, top, zoom },
324 })
325 };
326 assert_eq!(
327 parse(vec![
328 page(),
329 name("XYZ"),
330 Object::Null,
331 Object::Null,
332 Object::Null
333 ]),
334 xyz(None, None, None)
335 );
336 assert_eq!(
337 parse(vec![
338 page(),
339 name("XYZ"),
340 Object::Int(0),
341 Object::Int(0),
342 Object::Int(0)
343 ]),
344 xyz(Some(0.0), Some(0.0), None),
345 "zoom 0 is null, a coordinate 0 is not"
346 );
347 assert_eq!(
349 parse(vec![page(), name("XYZ"), Object::Int(5)]),
350 xyz(Some(5.0), None, None)
351 );
352 assert_eq!(
353 parse(vec![page(), name("FitH")]),
354 Some(Destination {
355 page: DestinationPage::Object(ObjRef { num: 3, gen: 0 }),
356 fit: Fit::FitH { top: None },
357 })
358 );
359 }
360
361 #[test]
363 fn remote_destinations_carry_a_page_number() {
364 assert_eq!(
365 parse(vec![Object::Int(4), name("Fit")]),
366 Some(Destination {
367 page: DestinationPage::Number(4),
368 fit: Fit::Fit,
369 })
370 );
371 assert_eq!(parse(vec![Object::Int(-1), name("Fit")]), None);
372 }
373
374 #[test]
376 fn malformed_destinations_are_rejected() {
377 assert_eq!(parse(vec![]), None, "empty");
378 assert_eq!(parse(vec![page()]), None, "no fit");
379 assert_eq!(parse(vec![page(), name("FitAll")]), None, "unknown fit");
380 assert_eq!(
381 parse(vec![name("Fit"), page()]),
382 None,
383 "page and fit swapped"
384 );
385 assert_eq!(
386 parse(vec![Object::String(b"3".to_vec()), name("Fit")]),
387 None,
388 "page as a string"
389 );
390 assert_eq!(
391 parse(vec![
392 page(),
393 name("FitR"),
394 Object::Int(1),
395 Object::Int(2),
396 Object::Int(3)
397 ]),
398 None,
399 "FitR with three numbers"
400 );
401 assert_eq!(
402 parse(vec![
403 page(),
404 name("XYZ"),
405 name("left"),
406 Object::Int(1),
407 Object::Int(1)
408 ]),
409 None,
410 "a coordinate that is not a number or null"
411 );
412 }
413
414 #[test]
416 fn indirect_arrays_and_numbers_resolve() {
417 let mut b = PdfBuilder::new();
418 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
419 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
420 b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
421 b.object(10, "[3 0 R /XYZ 11 0 R null 12 0 R]");
422 b.object(11, "42");
423 b.object(12, "2.0");
424 b.object(13, "(not an array)");
425 let doc = Document::load(b.build(1)).unwrap();
426 let indirect = Object::Ref(ObjRef { num: 10, gen: 0 });
427 assert_eq!(
428 block_on(destination_with(&Immediate(&doc), &indirect)),
429 Some(Destination {
430 page: DestinationPage::Object(ObjRef { num: 3, gen: 0 }),
431 fit: Fit::Xyz {
432 left: Some(42.0),
433 top: None,
434 zoom: Some(2.0),
435 },
436 })
437 );
438 let direct = Object::Array(vec![page(), name("Fit")]);
439 assert_eq!(
440 block_on(destination_with(&Immediate(&doc), &direct)).map(|d| d.fit),
441 Some(Fit::Fit)
442 );
443 let string = Object::Ref(ObjRef { num: 13, gen: 0 });
444 assert_eq!(block_on(destination_with(&Immediate(&doc), &string)), None);
445 }
446
447 fn named_doc() -> Document {
453 let mut b = PdfBuilder::new();
454 b.object(
455 1,
456 "<< /Type /Catalog /Pages 2 0 R /Names << /Dests 10 0 R >> \
457 /Dests << /Intro [3 0 R /Fit] /Chapter1 [3 0 R /FitB] /Junk 7 >> >>",
458 );
459 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
460 b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
461 b.object(
462 10,
463 "<< /Names [(Broken) (nope) (Chapter1) [3 0 R /XYZ 0 700 null] (Chapter2) 11 0 R] >>",
464 );
465 b.object(11, "<< /D [3 0 R /FitH 500] /Extra true >>");
466 Document::load(b.build(1)).unwrap()
467 }
468
469 fn on_page(fit: Fit) -> Option<Destination> {
470 Some(Destination {
471 page: DestinationPage::Object(ObjRef { num: 3, gen: 0 }),
472 fit,
473 })
474 }
475
476 #[test]
478 fn named_destinations_come_from_the_name_tree_and_the_catalog_dictionary() {
479 let doc = named_doc();
480 let xyz = Fit::Xyz {
481 left: Some(0.0),
482 top: Some(700.0),
483 zoom: None,
484 };
485 assert_eq!(doc.named_destination(b"Chapter1"), on_page(xyz));
487 assert_eq!(
489 doc.named_destination(b"Chapter2"),
490 on_page(Fit::FitH { top: Some(500.0) })
491 );
492 assert_eq!(doc.named_destination(b"Intro"), on_page(Fit::Fit));
494 assert_eq!(doc.named_destination(b"Broken"), None, "a string value");
495 assert_eq!(doc.named_destination(b"Junk"), None, "a number value");
496 assert_eq!(doc.named_destination(b"Chapter9"), None, "absent");
497 let all: Vec<(String, Fit)> = doc
498 .named_destinations()
499 .into_iter()
500 .map(|(name, d)| (String::from_utf8(name).unwrap(), d.fit))
501 .collect();
502 assert_eq!(
503 all,
504 [
505 ("Chapter1".to_string(), xyz),
506 ("Chapter2".to_string(), Fit::FitH { top: Some(500.0) }),
507 ("Intro".to_string(), Fit::Fit),
508 ]
509 );
510 let mut b = PdfBuilder::new();
512 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
513 b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
514 let plain = Document::load(b.build(1)).unwrap();
515 assert_eq!(plain.named_destination(b"Intro"), None);
516 assert!(plain.named_destinations().is_empty());
517 }
518
519 #[test]
521 fn destination_values_follow_arrays_names_strings_and_d_dictionaries() {
522 let doc = named_doc();
523 let array = Object::Array(vec![page(), name("FitV"), Object::Int(9)]);
524 assert_eq!(
525 doc.destination(&array),
526 on_page(Fit::FitV { left: Some(9.0) })
527 );
528 assert_eq!(doc.destination(&name("Intro")), on_page(Fit::Fit));
529 assert_eq!(
530 doc.destination(&Object::String(b"Chapter2".to_vec())),
531 on_page(Fit::FitH { top: Some(500.0) })
532 );
533 let mut d = crate::object::Dict::default();
534 d.insert(Name("D".into()), Object::String(b"Intro".to_vec()));
535 assert_eq!(doc.destination(&Object::Dict(d)), on_page(Fit::Fit));
536 assert_eq!(doc.destination(&Object::Dict(Default::default())), None);
537 assert_eq!(doc.destination(&name("Chapter9")), None);
538 assert_eq!(doc.destination(&Object::Int(3)), None);
539 }
540}