Skip to main content

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}