1use crate::names;
18use pdfrum_object::{Dict, Name, Object, Resolve};
19
20#[derive(Debug, Clone, PartialEq, Default)]
22pub struct Resources {
23 pub chosen: Option<Dict>,
25 pub page: Option<Dict>,
27}
28
29impl Resources {
30 #[must_use]
35 pub fn choose(own: Option<Dict>, parent: Option<Dict>, page: Option<Dict>) -> Self {
36 let chosen = own.or(parent).or_else(|| page.clone());
37 Self { chosen, page }
38 }
39
40 #[must_use]
43 pub fn for_page(page: Option<Dict>) -> Self {
44 Self {
45 chosen: page.clone(),
46 page,
47 }
48 }
49
50 #[must_use]
52 pub fn holder<R: Resolve>(&self, category: &Name, r: &R) -> Option<Dict> {
53 if let Some(chosen) = &self.chosen
54 && let Some(found) = chosen.dict(category, r)
55 {
56 return Some(found);
59 }
60 let page = self.page.as_ref()?;
63 if self.chosen.as_ref() == Some(page) {
64 return None;
65 }
66 page.dict(category, r)
67 }
68
69 #[must_use]
71 pub fn find<R: Resolve>(&self, category: &Name, name: &Name, r: &R) -> Option<Object> {
72 let holder = self.holder(category, r)?;
73 Some(holder.get(name, r)?.into_owned())
74 }
75
76 #[must_use]
84 pub fn find_ref<R: Resolve>(
85 &self,
86 category: &Name,
87 name: &Name,
88 r: &R,
89 ) -> Option<pdfrum_object::ObjRef> {
90 match self.holder(category, r)?.raw(name)? {
91 Object::Ref(reference) => Some(*reference),
92 _ => None,
93 }
94 }
95
96 #[must_use]
99 pub fn color_spaces<R: Resolve>(&self, r: &R) -> Option<Dict> {
100 self.holder(names::COLOR_SPACE, r)
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 #![allow(
110 clippy::unreadable_literal,
111 clippy::float_cmp,
112 clippy::indexing_slicing,
113 clippy::cast_precision_loss,
114 clippy::cast_possible_truncation,
115 reason = "test fixtures quote oracle vectors verbatim and compare exactly"
116 )]
117
118 use super::Resources;
119 use crate::names;
120 use pdfrum_object::{Dict, Name, NoResolve, Object};
121
122 fn category(key: &str, entry: &str) -> Dict {
123 Dict::from_pairs([(
124 Name::from(key),
125 Object::Dict(Dict::from_pairs([(
126 Name::from(entry),
127 Object::Name(Name::from(entry)),
128 )])),
129 )])
130 }
131
132 #[test]
133 fn the_first_non_null_dictionary_wins_whole() {
134 let own = category("Font", "F1");
135 let page = category("XObject", "X1");
136 let res = Resources::choose(Some(own), None, Some(page));
137 assert!(
139 res.find(names::FONT, &Name::from("F1"), &NoResolve)
140 .is_some()
141 );
142 assert!(
145 res.find(names::XOBJECT, &Name::from("X1"), &NoResolve)
146 .is_some()
147 );
148 }
149
150 #[test]
151 fn a_present_category_lacking_the_name_does_not_fall_back() {
152 let mut own = category("Font", "F1");
153 own.push(
155 Name::from("XObject"),
156 Object::Dict(Dict::from_pairs([(
157 Name::from("Other"),
158 Object::Name(Name::from("Other")),
159 )])),
160 );
161 let page = category("XObject", "X1");
162 let res = Resources::choose(Some(own), None, Some(page));
163 assert!(
164 res.find(names::XOBJECT, &Name::from("X1"), &NoResolve)
165 .is_none(),
166 "a present-but-incomplete category must not fall back"
167 );
168 }
169
170 #[test]
171 fn the_parent_is_consulted_before_the_page() {
172 let parent = category("Font", "PARENT");
173 let page = category("Font", "PAGE");
174 let res = Resources::choose(None, Some(parent), Some(page));
175 assert!(
176 res.find(names::FONT, &Name::from("PARENT"), &NoResolve)
177 .is_some()
178 );
179 assert!(
180 res.find(names::FONT, &Name::from("PAGE"), &NoResolve)
181 .is_none()
182 );
183 }
184
185 #[test]
186 fn a_page_looks_up_only_in_itself() {
187 let page = category("Font", "F1");
188 let res = Resources::for_page(Some(page));
189 assert!(
190 res.find(names::FONT, &Name::from("F1"), &NoResolve)
191 .is_some()
192 );
193 assert!(
194 res.find(names::XOBJECT, &Name::from("X1"), &NoResolve)
195 .is_none()
196 );
197 }
198
199 #[test]
200 fn no_resources_at_all_finds_nothing() {
201 let res = Resources::default();
202 assert!(
203 res.find(names::FONT, &Name::from("F1"), &NoResolve)
204 .is_none()
205 );
206 assert!(res.color_spaces(&NoResolve).is_none());
207 }
208}