Skip to main content

pdfboss_core/
names.rs

1//! The document's name dictionary (ISO 32000-1 §7.7.4): the catalog's
2//! `/Names` entry, whose entries are the roots of name trees, one per
3//! category of object that can be referred to by name.
4
5use crate::object::{Dict, Object};
6use crate::source::AsyncObjectSource;
7use crate::tree::{self, resolved_dict};
8
9/// The name trees of Table 31, named by their key in the name dictionary.
10///
11/// Covers ISO 32000-1 §7.7.4.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum NameTree {
14    /// `/Dests`: name strings to destinations (§12.3.2.3).
15    Dests,
16    /// `/AP`: name strings to annotation appearance streams (§12.5.5).
17    Ap,
18    /// `/JavaScript`: name strings to document-level JavaScript actions.
19    JavaScript,
20    /// `/Pages`: name strings to visible pages used as templates.
21    Pages,
22    /// `/Templates`: name strings to invisible template pages.
23    Templates,
24    /// `/IDS`: digital identifiers to Web Capture content sets.
25    Ids,
26    /// `/URLS`: URLs to Web Capture content sets.
27    Urls,
28    /// `/EmbeddedFiles`: name strings to file specifications (§7.11.4).
29    EmbeddedFiles,
30    /// `/AlternatePresentations`: name strings to alternate presentations.
31    AlternatePresentations,
32    /// `/Renditions`: name strings to rendition objects (§13.2.3).
33    Renditions,
34}
35
36impl NameTree {
37    /// Every tree of Table 31, in the table's order.
38    pub const ALL: [NameTree; 10] = [
39        NameTree::Dests,
40        NameTree::Ap,
41        NameTree::JavaScript,
42        NameTree::Pages,
43        NameTree::Templates,
44        NameTree::Ids,
45        NameTree::Urls,
46        NameTree::EmbeddedFiles,
47        NameTree::AlternatePresentations,
48        NameTree::Renditions,
49    ];
50
51    /// The tree's key in the name dictionary.
52    pub fn key(self) -> &'static str {
53        match self {
54            NameTree::Dests => "Dests",
55            NameTree::Ap => "AP",
56            NameTree::JavaScript => "JavaScript",
57            NameTree::Pages => "Pages",
58            NameTree::Templates => "Templates",
59            NameTree::Ids => "IDS",
60            NameTree::Urls => "URLS",
61            NameTree::EmbeddedFiles => "EmbeddedFiles",
62            NameTree::AlternatePresentations => "AlternatePresentations",
63            NameTree::Renditions => "Renditions",
64        }
65    }
66}
67
68/// The root node of `tree` in the catalog's name dictionary, or `None`
69/// when the catalog, its `/Names` entry or the tree's entry is missing or
70/// not a dictionary.
71///
72/// Covers ISO 32000-1 §7.7.4.
73pub async fn name_tree_root_with<S: AsyncObjectSource>(
74    src: &S,
75    trailer: &Dict,
76    tree: NameTree,
77) -> Option<Dict> {
78    let catalog = resolved_dict(src, trailer.get("Root")?).await?;
79    let names = resolved_dict(src, catalog.get("Names")?).await?;
80    resolved_dict(src, names.get(tree.key())?).await
81}
82
83/// The object that `key` names in `tree`, resolved; `None` when the tree
84/// or the name is absent.
85///
86/// Covers ISO 32000-1 §7.7.4.
87pub async fn named_with<S: AsyncObjectSource>(
88    src: &S,
89    trailer: &Dict,
90    tree: NameTree,
91    key: &[u8],
92) -> Option<Object> {
93    let root = name_tree_root_with(src, trailer, tree).await?;
94    let value = tree::lookup(src, &root, &key.to_vec()).await?;
95    src.resolve(&value).await.ok()
96}
97
98/// Every name in `tree` with the object it names, unresolved, in tree
99/// order; empty when the tree is absent.
100///
101/// Covers ISO 32000-1 §7.7.4.
102pub async fn names_with<S: AsyncObjectSource>(
103    src: &S,
104    trailer: &Dict,
105    tree: NameTree,
106) -> Vec<(Vec<u8>, Object)> {
107    match name_tree_root_with(src, trailer, tree).await {
108        Some(root) => tree::entries(src, &root).await,
109        None => Vec::new(),
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use crate::object::{Name, ObjRef};
117    use crate::Document;
118    use pdfboss_testkit::PdfBuilder;
119
120    /// A one-page document whose catalog carries `catalog_extra`; `objects`
121    /// supplies anything numbered 10 and up.
122    fn doc(catalog_extra: &str, objects: &[(u32, &str)]) -> Document {
123        let mut b = PdfBuilder::new();
124        b.object(
125            1,
126            &format!("<< /Type /Catalog /Pages 2 0 R {catalog_extra} >>"),
127        );
128        b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
129        b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
130        for (num, body) in objects {
131            b.object(*num, body);
132        }
133        Document::load(b.build(1)).expect("load")
134    }
135
136    fn keys(pairs: &[(Vec<u8>, Object)]) -> Vec<String> {
137        pairs
138            .iter()
139            .map(|(k, _)| String::from_utf8(k.clone()).unwrap())
140            .collect()
141    }
142
143    // Covers ISO 32000-1 §7.7.4.
144    #[test]
145    fn named_objects_come_from_the_catalog_name_dictionary() {
146        // /Dests is an indirect two-level tree, /EmbeddedFiles a direct
147        // single-leaf root inside the name dictionary itself.
148        let doc = doc(
149            "/Names 10 0 R",
150            &[
151                (
152                    10,
153                    "<< /Dests 11 0 R /EmbeddedFiles << /Names [(a.txt) 14 0 R] >> >>",
154                ),
155                (11, "<< /Kids [12 0 R 13 0 R] >>"),
156                (
157                    12,
158                    "<< /Limits [(Chapter1) (Chapter2)] /Names [(Chapter1) [3 0 R /Fit] (Chapter2) 15 0 R] >>",
159                ),
160                (
161                    13,
162                    "<< /Limits [(Index) (Index)] /Names [(Index) [3 0 R /XYZ 0 0 null]] >>",
163                ),
164                (14, "<< /Type /Filespec /F (a.txt) >>"),
165                (15, "[3 0 R /FitH 700]"),
166            ],
167        );
168        let page = Object::Ref(ObjRef { num: 3, gen: 0 });
169        assert_eq!(
170            doc.named(NameTree::Dests, b"Chapter1"),
171            Some(Object::Array(vec![
172                page.clone(),
173                Object::Name(Name("Fit".into()))
174            ]))
175        );
176        // An indirect value comes back resolved.
177        assert_eq!(
178            doc.named(NameTree::Dests, b"Chapter2"),
179            Some(Object::Array(vec![
180                page,
181                Object::Name(Name("FitH".into())),
182                Object::Int(700)
183            ]))
184        );
185        assert_eq!(doc.named(NameTree::Dests, b"Chapter3"), None);
186        let spec = doc.named(NameTree::EmbeddedFiles, b"a.txt").unwrap();
187        assert_eq!(
188            spec.as_dict().unwrap().get("F"),
189            Some(&Object::String(b"a.txt".to_vec()))
190        );
191        assert_eq!(
192            keys(&doc.names(NameTree::Dests)),
193            ["Chapter1", "Chapter2", "Index"]
194        );
195        assert_eq!(keys(&doc.names(NameTree::EmbeddedFiles)), ["a.txt"]);
196        // A tree the dictionary does not carry.
197        assert_eq!(doc.named(NameTree::JavaScript, b"init"), None);
198        assert!(doc.names(NameTree::JavaScript).is_empty());
199    }
200
201    // Covers ISO 32000-1 §7.7.4.
202    #[test]
203    fn documents_without_a_name_dictionary_have_no_names() {
204        let plain = doc("", &[]);
205        assert_eq!(plain.named(NameTree::Dests, b"x"), None);
206        assert!(plain.names(NameTree::Dests).is_empty());
207        for tree in NameTree::ALL {
208            assert!(plain.names(tree).is_empty(), "{tree:?}");
209        }
210        // A /Names entry that is not a dictionary, and a tree entry that
211        // is not a dictionary, read as absent.
212        let broken = doc("/Names 10 0 R", &[(10, "[1 2 3]")]);
213        assert_eq!(broken.named(NameTree::Dests, b"x"), None);
214        let broken = doc("/Names << /Dests (nope) >>", &[]);
215        assert!(broken.names(NameTree::Dests).is_empty());
216    }
217
218    // Covers ISO 32000-1 §7.7.4.
219    #[test]
220    fn every_table_31_tree_has_its_dictionary_key() {
221        let keys: Vec<&str> = NameTree::ALL.iter().map(|t| t.key()).collect();
222        assert_eq!(
223            keys,
224            [
225                "Dests",
226                "AP",
227                "JavaScript",
228                "Pages",
229                "Templates",
230                "IDS",
231                "URLS",
232                "EmbeddedFiles",
233                "AlternatePresentations",
234                "Renditions"
235            ]
236        );
237    }
238}