1use crate::object::{Dict, Object};
6use crate::source::AsyncObjectSource;
7use crate::tree::{self, resolved_dict};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum NameTree {
14 Dests,
16 Ap,
18 JavaScript,
20 Pages,
22 Templates,
24 Ids,
26 Urls,
28 EmbeddedFiles,
30 AlternatePresentations,
32 Renditions,
34}
35
36impl NameTree {
37 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 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
68pub 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
83pub 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
98pub 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 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 #[test]
145 fn named_objects_come_from_the_catalog_name_dictionary() {
146 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 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 assert_eq!(doc.named(NameTree::JavaScript, b"init"), None);
198 assert!(doc.names(NameTree::JavaScript).is_empty());
199 }
200
201 #[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 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 #[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}