summaryrefslogtreecommitdiff
path: root/crates/typst-pdf/src/named_destination.rs
blob: 7ae2c5e6fc186f62d7aeb1d65e35fe2ef134d50b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
use std::collections::{HashMap, HashSet};

use pdf_writer::writers::Destination;
use pdf_writer::{Ref, Str};
use typst_library::diag::SourceResult;
use typst_library::foundations::{Label, NativeElement};
use typst_library::introspection::Location;
use typst_library::layout::Abs;
use typst_library::model::HeadingElem;

use crate::{AbsExt, PdfChunk, Renumber, StrExt, WithGlobalRefs};

/// A list of destinations in the PDF document (a specific point on a specific
/// page), that have a name associated with them.
///
/// Typst creates a named destination for each heading in the document, that
/// will then be written in the document catalog. PDF readers can then display
/// them to show a clickable outline of the document.
#[derive(Default)]
pub struct NamedDestinations {
    /// A map between elements and their associated labels
    pub loc_to_dest: HashMap<Location, Label>,
    /// A sorted list of all named destinations.
    pub dests: Vec<(Label, Ref)>,
}

impl Renumber for NamedDestinations {
    fn renumber(&mut self, offset: i32) {
        for (_, reference) in &mut self.dests {
            reference.renumber(offset);
        }
    }
}

/// Fills in the map and vector for named destinations and writes the indirect
/// destination objects.
pub fn write_named_destinations(
    context: &WithGlobalRefs,
) -> SourceResult<(PdfChunk, NamedDestinations)> {
    let mut chunk = PdfChunk::new();
    let mut out = NamedDestinations::default();
    let mut seen = HashSet::new();

    // Find all headings that have a label and are the first among other
    // headings with the same label.
    let mut matches: Vec<_> = context
        .document
        .introspector
        .query(&HeadingElem::elem().select())
        .iter()
        .filter_map(|elem| elem.location().zip(elem.label()))
        .filter(|&(_, label)| seen.insert(label))
        .collect();

    // Named destinations must be sorted by key.
    matches.sort_by_key(|&(_, label)| label.resolve());

    for (loc, label) in matches {
        // Don't encode named destinations that would exceed the limit. Those
        // will instead be encoded as normal links.
        if label.resolve().len() > Str::PDFA_LIMIT {
            continue;
        }

        let pos = context.document.introspector.position(loc);
        let index = pos.page.get() - 1;
        let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero());

        if let Some((Some(page), Some(page_ref))) =
            context.pages.get(index).zip(context.globals.pages.get(index))
        {
            let dest_ref = chunk.alloc();
            let x = pos.point.x.to_f32();
            let y = (page.content.size.y - y).to_f32();
            out.dests.push((label, dest_ref));
            out.loc_to_dest.insert(loc, label);
            chunk
                .indirect(dest_ref)
                .start::<Destination>()
                .page(*page_ref)
                .xyz(x, y, None);
        }
    }

    Ok((chunk, out))
}