pdfrum_doc/prefs.rs
1//! Viewer preferences (ISO 32000-1 ยง12.2): how a document asks to be
2//! displayed and printed.
3//!
4//! Each accessor has its own default for a document with no
5//! `/ViewerPreferences` at all, and two of those defaults are **not** the
6//! value a present-but-empty dictionary produces: `num_copies` answers one
7//! with no dictionary and zero with an empty one.
8
9use pdfrum_object::{Array, Dict, Name, Object, Resolve};
10
11use crate::names;
12
13/// A document's viewer preferences.
14///
15/// ```
16/// use pdfrum_doc::ViewerPrefs;
17/// use pdfrum_object::{Dict, Name, NoResolve, Object};
18///
19/// let catalog = Dict::from_pairs([(
20/// Name::from("ViewerPreferences"),
21/// Object::Dict(Dict::from_pairs([
22/// (Name::from("Direction"), Object::Name(Name::from("R2L"))),
23/// (Name::from("NumCopies"), Object::Int(3)),
24/// ])),
25/// )]);
26/// let prefs = ViewerPrefs::read(&catalog, &NoResolve);
27///
28/// assert!(prefs.is_direction_r2l(&NoResolve));
29/// // A catalog with no `/ViewerPreferences` has no dictionary at all,
30/// // which is not the same as an empty one.
31/// assert!(ViewerPrefs::read(&Dict::default(), &NoResolve).dict.is_none());
32/// ```
33#[derive(Debug, Clone, Default, PartialEq)]
34pub struct ViewerPrefs {
35 /// The `/ViewerPreferences` dictionary, absent when the catalog has none.
36 pub dict: Option<Dict>,
37}
38
39impl ViewerPrefs {
40 /// Reads the catalog's preferences.
41 ///
42 /// ```
43 /// use pdfrum_doc::ViewerPrefs;
44 /// use pdfrum_object::{Dict, Name, NoResolve, Object};
45 ///
46 /// let catalog = Dict::from_pairs([(
47 /// Name::from("ViewerPreferences"),
48 /// Object::Dict(Dict::from_pairs([
49 /// (Name::from("Direction"), Object::Name(Name::from("R2L"))),
50 /// (Name::from("NumCopies"), Object::Int(3)),
51 /// ])),
52 /// )]);
53 /// let prefs = ViewerPrefs::read(&catalog, &NoResolve);
54 ///
55 /// assert!(prefs.dict.is_some());
56 /// ```
57 #[must_use]
58 pub fn read<R: Resolve>(catalog: &Dict, r: &R) -> ViewerPrefs {
59 ViewerPrefs {
60 dict: catalog.dict(names::VIEWER_PREFERENCES, r),
61 }
62 }
63
64 /// Whether the reading order is right-to-left.
65 ///
66 /// ```
67 /// use pdfrum_doc::ViewerPrefs;
68 /// use pdfrum_object::{Dict, Name, NoResolve, Object};
69 ///
70 /// let catalog = Dict::from_pairs([(
71 /// Name::from("ViewerPreferences"),
72 /// Object::Dict(Dict::from_pairs([
73 /// (Name::from("Direction"), Object::Name(Name::from("R2L"))),
74 /// (Name::from("NumCopies"), Object::Int(3)),
75 /// ])),
76 /// )]);
77 /// let prefs = ViewerPrefs::read(&catalog, &NoResolve);
78 ///
79 /// assert!(prefs.is_direction_r2l(&NoResolve));
80 /// assert!(!ViewerPrefs::default().is_direction_r2l(&NoResolve));
81 /// ```
82 #[must_use]
83 pub fn is_direction_r2l<R: Resolve>(&self, r: &R) -> bool {
84 self.dict
85 .as_ref()
86 .and_then(|dict| dict.byte_string(names::DIRECTION, r))
87 .as_deref()
88 == Some(b"R2L")
89 }
90
91 /// Whether the print dialog offers page scaling. **True** with no
92 /// dictionary.
93 ///
94 /// ```
95 /// use pdfrum_doc::ViewerPrefs;
96 /// use pdfrum_object::{Dict, Name, NoResolve, Object};
97 ///
98 /// let catalog = Dict::from_pairs([(
99 /// Name::from("ViewerPreferences"),
100 /// Object::Dict(Dict::from_pairs([
101 /// (Name::from("Direction"), Object::Name(Name::from("R2L"))),
102 /// (Name::from("NumCopies"), Object::Int(3)),
103 /// ])),
104 /// )]);
105 /// let prefs = ViewerPrefs::read(&catalog, &NoResolve);
106 ///
107 /// // True with no dictionary, and true unless the key says `None`.
108 /// assert!(prefs.print_scaling(&NoResolve));
109 /// assert!(ViewerPrefs::default().print_scaling(&NoResolve));
110 /// ```
111 #[must_use]
112 pub fn print_scaling<R: Resolve>(&self, r: &R) -> bool {
113 self.dict
114 .as_ref()
115 .and_then(|dict| dict.byte_string(names::PRINT_SCALING, r))
116 .as_deref()
117 != Some(b"None")
118 }
119
120 /// The default number of copies.
121 ///
122 /// One with no dictionary, **zero** with a dictionary that omits the key
123 /// โ the two paths genuinely differ.
124 ///
125 /// ```
126 /// use pdfrum_doc::ViewerPrefs;
127 /// use pdfrum_object::{Dict, Name, NoResolve, Object};
128 ///
129 /// let catalog = Dict::from_pairs([(
130 /// Name::from("ViewerPreferences"),
131 /// Object::Dict(Dict::from_pairs([
132 /// (Name::from("Direction"), Object::Name(Name::from("R2L"))),
133 /// (Name::from("NumCopies"), Object::Int(3)),
134 /// ])),
135 /// )]);
136 /// let prefs = ViewerPrefs::read(&catalog, &NoResolve);
137 ///
138 /// assert_eq!(prefs.num_copies(&NoResolve), 3);
139 /// // One with no dictionary; a dictionary omitting the key answers zero.
140 /// assert_eq!(ViewerPrefs::default().num_copies(&NoResolve), 1);
141 /// ```
142 #[must_use]
143 pub fn num_copies<R: Resolve>(&self, r: &R) -> i64 {
144 match &self.dict {
145 None => 1,
146 Some(dict) => dict.int(names::NUM_COPIES, r).unwrap_or(0),
147 }
148 }
149
150 /// The default printed page range, as raw index pairs. Duplicates are
151 /// kept.
152 ///
153 /// ```
154 /// use pdfrum_doc::ViewerPrefs;
155 /// use pdfrum_object::{Dict, Name, NoResolve, Object};
156 ///
157 /// let catalog = Dict::from_pairs([(
158 /// Name::from("ViewerPreferences"),
159 /// Object::Dict(Dict::from_pairs([
160 /// (Name::from("Direction"), Object::Name(Name::from("R2L"))),
161 /// (Name::from("NumCopies"), Object::Int(3)),
162 /// ])),
163 /// )]);
164 /// let prefs = ViewerPrefs::read(&catalog, &NoResolve);
165 ///
166 /// assert!(prefs.print_page_range(&NoResolve).is_none());
167 /// ```
168 #[must_use]
169 pub fn print_page_range<R: Resolve>(&self, r: &R) -> Option<Array> {
170 self.dict
171 .as_ref()
172 .and_then(|dict| dict.array(names::PRINT_PAGE_RANGE, r))
173 }
174
175 /// The duplex handling. `None` โ the *string* โ with no dictionary.
176 ///
177 /// ```
178 /// use pdfrum_doc::ViewerPrefs;
179 /// use pdfrum_object::{Dict, Name, NoResolve, Object};
180 ///
181 /// let catalog = Dict::from_pairs([(
182 /// Name::from("ViewerPreferences"),
183 /// Object::Dict(Dict::from_pairs([
184 /// (Name::from("Direction"), Object::Name(Name::from("R2L"))),
185 /// (Name::from("NumCopies"), Object::Int(3)),
186 /// ])),
187 /// )]);
188 /// let prefs = ViewerPrefs::read(&catalog, &NoResolve);
189 ///
190 /// // The *string* `None` is the default, not an absent value.
191 /// assert_eq!(prefs.duplex(&NoResolve), b"None");
192 /// ```
193 #[must_use]
194 pub fn duplex<R: Resolve>(&self, r: &R) -> Vec<u8> {
195 self.dict
196 .as_ref()
197 .and_then(|dict| dict.byte_string(names::DUPLEX, r))
198 .unwrap_or_else(|| b"None".to_vec())
199 }
200
201 /// Any preference whose value is a **name**.
202 ///
203 /// The type filter is the point: a Boolean `/HideToolbar` and an integer
204 /// `/NumCopies` both answer nothing here even when present. Keys are
205 /// case-sensitive.
206 ///
207 /// ```
208 /// use pdfrum_doc::ViewerPrefs;
209 /// use pdfrum_object::{Dict, Name, NoResolve, Object};
210 ///
211 /// let catalog = Dict::from_pairs([(
212 /// Name::from("ViewerPreferences"),
213 /// Object::Dict(Dict::from_pairs([
214 /// (Name::from("Direction"), Object::Name(Name::from("R2L"))),
215 /// (Name::from("NumCopies"), Object::Int(3)),
216 /// ])),
217 /// )]);
218 /// let prefs = ViewerPrefs::read(&catalog, &NoResolve);
219 ///
220 /// assert_eq!(prefs.generic_name(&Name::from("Direction")), Some(b"R2L".to_vec()));
221 /// // Name-typed: the integer `/NumCopies` answers nothing here.
222 /// assert_eq!(prefs.generic_name(&Name::from("NumCopies")), None);
223 /// ```
224 #[must_use]
225 pub fn generic_name(&self, key: &Name) -> Option<Vec<u8>> {
226 self.dict
227 .as_ref()?
228 .raw(key)
229 .and_then(Object::as_name)
230 .map(|name| name.as_bytes().to_vec())
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::ViewerPrefs;
237 use pdfrum_object::{Array, Dict, Name, NoResolve, Object};
238
239 fn prefs(pairs: &[(&str, Object)]) -> ViewerPrefs {
240 ViewerPrefs {
241 dict: Some(Dict::from_pairs(
242 pairs
243 .iter()
244 .map(|(k, v)| (Name::from(*k), v.clone()))
245 .collect::<Vec<_>>(),
246 )),
247 }
248 }
249
250 #[test]
251 fn a_document_with_no_preferences_uses_the_no_dictionary_defaults() {
252 let none = ViewerPrefs::default();
253 assert!(!none.is_direction_r2l(&NoResolve));
254 assert!(none.print_scaling(&NoResolve));
255 assert_eq!(none.num_copies(&NoResolve), 1);
256 assert_eq!(none.duplex(&NoResolve), b"None");
257 assert!(none.print_page_range(&NoResolve).is_none());
258 }
259
260 #[test]
261 fn an_empty_dictionary_answers_zero_copies_not_one() {
262 assert_eq!(prefs(&[]).num_copies(&NoResolve), 0);
263 }
264
265 #[test]
266 fn the_generic_accessor_is_the_only_type_filtered_one() {
267 let p = prefs(&[
268 ("NumCopies", Object::Int(5)),
269 ("Direction", Object::Name(Name::from("R2L"))),
270 ("ViewArea", Object::Name(Name::from("CropBox"))),
271 ("HideToolbar", Object::Bool(true)),
272 ("Foo", Object::Name(Name::from("foo"))),
273 ]);
274 assert_eq!(p.num_copies(&NoResolve), 5);
275 assert!(p.is_direction_r2l(&NoResolve));
276 assert_eq!(
277 p.generic_name(&Name::from("ViewArea")),
278 Some(b"CropBox".to_vec())
279 );
280 // A Boolean and an integer are not names.
281 assert_eq!(p.generic_name(&Name::from("HideToolbar")), None);
282 assert_eq!(p.generic_name(&Name::from("NumCopies")), None);
283 // Keys are case-sensitive.
284 assert_eq!(p.generic_name(&Name::from("Foo")), Some(b"foo".to_vec()));
285 assert_eq!(p.generic_name(&Name::from("foo")), None);
286 }
287
288 #[test]
289 fn a_print_page_range_keeps_its_duplicates() {
290 let p = prefs(&[(
291 "PrintPageRange",
292 Object::Array(Array::of([0, 2, 4, 4].map(Object::from))),
293 )]);
294 let range = p.print_page_range(&NoResolve).expect("present");
295 assert_eq!(range.len(), 4);
296 assert_eq!(range.int_at(3), Some(4));
297 }
298}