1use std::collections::{BTreeMap, BTreeSet};
33
34use pdfrum_object::{Dict, Name, ObjRef, Object};
35
36pub(crate) const CATEGORIES: [&str; 3] = ["ExtGState", "Font", "XObject"];
41
42#[derive(Debug, Clone, Default)]
48pub struct ResourceTable {
49 entries: BTreeMap<String, Dict>,
51 parked: BTreeMap<String, BTreeMap<Name, Object>>,
53}
54
55impl ResourceTable {
56 #[must_use]
61 pub fn load(resources: &Dict, r: &impl pdfrum_object::Resolve) -> Self {
62 let mut entries = BTreeMap::new();
63 for category in CATEGORIES {
64 let key = Name::from(category);
65 if let Some(dict) = resources.dict(&key, r) {
66 entries.insert(category.to_owned(), dict);
67 }
68 }
69 Self {
70 entries,
71 parked: BTreeMap::new(),
72 }
73 }
74
75 pub fn realize(&mut self, category: &str, object: ObjRef) -> Name {
83 let dict = self.entries.entry(category.to_owned()).or_default();
84 for (name, held) in dict.iter() {
85 if held.as_ref_id() == Some(object) {
86 return name.clone();
87 }
88 }
89 let name = self.free_name(category);
90 let dict = self.entries.entry(category.to_owned()).or_default();
91 dict.push(name.clone(), Object::Ref(object));
92 name
93 }
94
95 #[must_use]
102 pub fn name_of(&self, category: &str, object: ObjRef) -> Option<Name> {
103 self.entries
104 .get(category)?
105 .iter()
106 .find(|(_, held)| held.as_ref_id() == Some(object))
107 .map(|(name, _)| name.clone())
108 }
109
110 #[must_use]
113 pub fn name_of_dict(&self, category: &str, value: &Dict) -> Option<Name> {
114 self.entries
115 .get(category)?
116 .iter()
117 .find(|(_, held)| matches!(held, Object::Dict(d) if d == value))
118 .map(|(name, _)| name.clone())
119 }
120
121 pub fn realize_dict(&mut self, category: &str, value: &Dict) -> Name {
124 let existing = self
125 .entries
126 .get(category)
127 .and_then(|dict| {
128 dict.iter().find(|(_, held)| match held {
129 Object::Dict(d) => d == value,
130 _ => false,
131 })
132 })
133 .map(|(name, _)| name.clone());
134 existing.unwrap_or_else(|| {
135 let name = self.free_name(category);
136 let dict = self.entries.entry(category.to_owned()).or_default();
137 dict.push(name.clone(), Object::Dict(value.clone()));
138 name
139 })
140 }
141
142 fn free_name(&self, category: &str) -> Name {
147 let letter = category.chars().next().unwrap_or('X');
148 let live = self.entries.get(category);
149 let parked = self.parked.get(category);
150 for id in 1u32.. {
151 let candidate = Name::from(format!("FX{letter}{id}").as_str());
152 let taken = live.is_some_and(|d| d.contains_key(&candidate))
153 || parked.is_some_and(|p| p.contains_key(&candidate));
154 if !taken {
155 return candidate;
156 }
157 }
158 Name::from("FXX1")
161 }
162
163 pub fn sweep(&mut self, used: &BTreeMap<String, BTreeSet<Name>>) {
172 for category in CATEGORIES {
173 let wanted: BTreeSet<Name> =
174 used.get(category).into_iter().flatten().cloned().collect();
175
176 if let Some(dict) = self.entries.get_mut(category) {
177 let mut kept = Dict::new();
178 let parked = self.parked.entry(category.to_owned()).or_default();
179 for (name, value) in dict.iter() {
180 if wanted.contains(name) {
181 kept.push(name.clone(), value.clone());
182 } else {
183 parked.insert(name.clone(), value.clone());
184 }
185 }
186 *dict = kept;
187 }
188
189 let restorable: Vec<(Name, Object)> = self
191 .parked
192 .get(category)
193 .into_iter()
194 .flatten()
195 .filter(|(name, _)| wanted.contains(*name))
196 .map(|(name, value)| (name.clone(), value.clone()))
197 .collect();
198 if restorable.is_empty() {
199 continue;
200 }
201 let dict = self.entries.entry(category.to_owned()).or_default();
202 let parked = self.parked.entry(category.to_owned()).or_default();
203 for (name, value) in restorable {
204 if !dict.contains_key(&name) {
205 dict.push(name.clone(), value);
206 }
207 parked.remove(&name);
208 }
209 }
210 }
211
212 #[must_use]
215 pub fn to_dict(&self, base: &Dict) -> Dict {
216 let mut out = Dict::new();
217 for (key, value) in base.iter() {
218 if CATEGORIES.iter().any(|c| key.as_bytes() == c.as_bytes()) {
219 continue;
220 }
221 out.push(key.clone(), value.clone());
222 }
223 for category in CATEGORIES {
224 let Some(dict) = self.entries.get(category) else {
225 continue;
226 };
227 if dict.is_empty() {
230 continue;
231 }
232 out.push(Name::from(category), Object::Dict(dict.clone()));
233 }
234 out
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::{CATEGORIES, ResourceTable};
241 use pdfrum_object::{Dict, Name, NoResolve, ObjRef, Object};
242 use std::collections::{BTreeMap, BTreeSet};
243
244 fn used(pairs: &[(&str, &[&str])]) -> BTreeMap<String, BTreeSet<Name>> {
245 pairs
246 .iter()
247 .map(|(category, names)| {
248 (
249 (*category).to_owned(),
250 names.iter().map(|n| Name::from(*n)).collect(),
251 )
252 })
253 .collect()
254 }
255
256 fn names_of(table: &ResourceTable, category: &str) -> Vec<String> {
257 let dict = table.to_dict(&Dict::new());
258 let Some(Object::Dict(sub)) = dict.raw(&Name::from(category)) else {
259 return Vec::new();
260 };
261 sub.keys()
262 .map(|k| String::from_utf8_lossy(k.as_bytes()).into_owned())
263 .collect()
264 }
265
266 #[test]
268 fn exactly_three_categories_are_maintained() {
269 assert_eq!(CATEGORIES, ["ExtGState", "Font", "XObject"]);
270 }
271
272 #[test]
275 fn a_fresh_name_is_fx_plus_the_categorys_letter_plus_one() {
276 let mut table = ResourceTable::default();
277 assert_eq!(table.realize("Font", ObjRef::new(4, 0)), Name::from("FXF1"));
278 assert_eq!(
279 table.realize("XObject", ObjRef::new(5, 0)),
280 Name::from("FXX1")
281 );
282 assert_eq!(
283 table.realize("ExtGState", ObjRef::new(6, 0)),
284 Name::from("FXE1")
285 );
286 }
287
288 #[test]
289 fn successive_objects_take_successive_numbers() {
290 let mut table = ResourceTable::default();
291 let first = table.realize("XObject", ObjRef::new(1, 0));
292 let second = table.realize("XObject", ObjRef::new(2, 0));
293 assert_eq!(first, Name::from("FXX1"));
294 assert_eq!(second, Name::from("FXX2"));
295 }
296
297 #[test]
299 fn an_object_already_in_the_dictionary_keeps_its_name() {
300 let existing = Dict::from_pairs([(Name::from("F1"), Object::Ref(ObjRef::new(9, 0)))]);
301 let resources = Dict::from_pairs([(Name::from("Font"), Object::Dict(existing))]);
302 let mut table = ResourceTable::load(&resources, &NoResolve);
303 assert_eq!(table.realize("Font", ObjRef::new(9, 0)), Name::from("F1"));
304 assert_eq!(names_of(&table, "Font"), vec!["F1".to_owned()]);
305 }
306
307 #[test]
309 fn an_occupied_name_is_skipped() {
310 let existing = Dict::from_pairs([(Name::from("FXF1"), Object::Int(0))]);
311 let resources = Dict::from_pairs([(Name::from("Font"), Object::Dict(existing))]);
312 let mut table = ResourceTable::load(&resources, &NoResolve);
313 assert_eq!(table.realize("Font", ObjRef::new(3, 0)), Name::from("FXF2"));
314 }
315
316 #[test]
320 fn a_swept_name_is_parked_and_still_reserved() {
321 let mut table = ResourceTable::default();
322 let first = table.realize("ExtGState", ObjRef::new(1, 0));
323 let second = table.realize("ExtGState", ObjRef::new(2, 0));
324 assert_eq!(
325 (first.clone(), second.clone()),
326 (Name::from("FXE1"), Name::from("FXE2"))
327 );
328
329 table.sweep(&used(&[("ExtGState", &["FXE1"])]));
331 assert_eq!(names_of(&table, "ExtGState"), vec!["FXE1".to_owned()]);
332
333 let third = table.realize("ExtGState", ObjRef::new(3, 0));
335 assert_eq!(third, Name::from("FXE3"));
336 }
337
338 #[test]
341 fn a_parked_entry_wanted_again_is_restored() {
342 let existing = Dict::from_pairs([
343 (Name::from("A"), Object::Int(1)),
344 (Name::from("B"), Object::Int(2)),
345 ]);
346 let resources = Dict::from_pairs([(Name::from("Font"), Object::Dict(existing))]);
347 let mut table = ResourceTable::load(&resources, &NoResolve);
348
349 table.sweep(&used(&[("Font", &["A"])]));
350 assert_eq!(names_of(&table, "Font"), vec!["A".to_owned()]);
351
352 table.sweep(&used(&[("Font", &["A", "B"])]));
353 assert_eq!(
354 names_of(&table, "Font"),
355 vec!["A".to_owned(), "B".to_owned()]
356 );
357 }
358
359 #[test]
361 fn an_emptied_category_loses_its_key() {
362 let existing = Dict::from_pairs([(Name::from("F1"), Object::Int(1))]);
363 let resources = Dict::from_pairs([(Name::from("Font"), Object::Dict(existing))]);
364 let mut table = ResourceTable::load(&resources, &NoResolve);
365 table.sweep(&BTreeMap::new());
366 let out = table.to_dict(&Dict::new());
367 assert!(!out.contains_key(&Name::from("Font")));
368 }
369
370 #[test]
373 fn an_unmaintained_category_is_carried_through() {
374 let base = Dict::from_pairs([
375 (Name::from("ColorSpace"), Object::Int(7)),
376 (Name::from("Font"), Object::Int(0)),
377 ]);
378 let table = ResourceTable::default();
379 let out = table.to_dict(&base);
380 assert_eq!(out.raw(&Name::from("ColorSpace")), Some(&Object::Int(7)));
381 assert!(!out.contains_key(&Name::from("Font")));
384 }
385
386 #[test]
388 fn an_equal_direct_dictionary_is_reused() {
389 let mut table = ResourceTable::default();
390 let gs = Dict::from_pairs([(Name::from("ca"), Object::Real(0.5))]);
391 let first = table.realize_dict("ExtGState", &gs);
392 let second = table.realize_dict("ExtGState", &gs);
393 assert_eq!(first, second);
394 assert_eq!(names_of(&table, "ExtGState"), vec!["FXE1".to_owned()]);
395 }
396
397 #[test]
401 fn a_name_the_caller_did_not_list_is_swept() {
402 let mut table = ResourceTable::default();
403 let _ = table.realize("XObject", ObjRef::new(1, 0));
404 table.sweep(&BTreeMap::new());
405 assert!(names_of(&table, "XObject").is_empty());
406 }
407}