1use crate::objects::{PdfDict, PdfObj};
15use crate::resolver::Resolver;
16
17#[derive(Debug, Clone, PartialEq)]
25pub struct ViewerPreferences {
26 pub hide_toolbar: bool,
28 pub hide_menubar: bool,
30 pub hide_window_ui: bool,
32 pub fit_window: bool,
34 pub center_window: bool,
36 pub display_doc_title: bool,
38 pub non_full_screen_page_mode: PageMode,
40 pub direction: ReadingDirection,
42 pub page_layout: PageLayout,
44 pub page_mode: PageMode,
46 pub print_scaling: PrintScaling,
48 pub duplex: Option<Duplex>,
50 pub pick_tray_by_pdf_size: Option<bool>,
52 pub num_copies: Option<u32>,
54}
55
56impl Default for ViewerPreferences {
57 fn default() -> Self {
58 Self {
59 hide_toolbar: false,
60 hide_menubar: false,
61 hide_window_ui: false,
62 fit_window: false,
63 center_window: false,
64 display_doc_title: false,
65 non_full_screen_page_mode: PageMode::UseNone,
66 direction: ReadingDirection::L2R,
67 page_layout: PageLayout::SinglePage,
68 page_mode: PageMode::UseNone,
69 print_scaling: PrintScaling::AppDefault,
70 duplex: None,
71 pick_tray_by_pdf_size: None,
72 num_copies: None,
73 }
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79#[non_exhaustive]
80pub enum PageLayout {
81 SinglePage,
83 OneColumn,
85 TwoColumnLeft,
87 TwoColumnRight,
89 TwoPageLeft,
91 TwoPageRight,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97#[non_exhaustive]
98pub enum PageMode {
99 UseNone,
101 UseOutlines,
103 UseThumbs,
105 FullScreen,
107 UseOC,
109 UseAttachments,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115#[non_exhaustive]
116pub enum ReadingDirection {
117 L2R,
119 R2L,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125#[non_exhaustive]
126pub enum PrintScaling {
127 None,
129 AppDefault,
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135#[non_exhaustive]
136pub enum Duplex {
137 Simplex,
139 DuplexFlipShortEdge,
141 DuplexFlipLongEdge,
143}
144
145pub fn parse_viewer_preferences(resolver: &Resolver) -> ViewerPreferences {
151 let mut prefs = ViewerPreferences::default();
152
153 let Some(catalog) = catalog_dict(resolver) else {
154 return prefs;
155 };
156
157 if let Some(name) = catalog.get_name(b"PageLayout")
158 && let Some(layout) = parse_page_layout(name)
159 {
160 prefs.page_layout = layout;
161 }
162 if let Some(name) = catalog.get_name(b"PageMode")
163 && let Some(mode) = parse_page_mode(name)
164 {
165 prefs.page_mode = mode;
166 }
167
168 if let Some(vp_obj) = catalog.get(b"ViewerPreferences")
169 && let Ok(vp) = resolver.deref(vp_obj)
170 && let Some(vp_dict) = vp.as_dict()
171 {
172 fill_viewer_prefs(&mut prefs, vp_dict);
173 }
174
175 prefs
176}
177
178fn fill_viewer_prefs(prefs: &mut ViewerPreferences, dict: &PdfDict) {
179 if let Some(b) = dict.get(b"HideToolbar").and_then(as_bool) {
180 prefs.hide_toolbar = b;
181 }
182 if let Some(b) = dict.get(b"HideMenubar").and_then(as_bool) {
183 prefs.hide_menubar = b;
184 }
185 if let Some(b) = dict.get(b"HideWindowUI").and_then(as_bool) {
186 prefs.hide_window_ui = b;
187 }
188 if let Some(b) = dict.get(b"FitWindow").and_then(as_bool) {
189 prefs.fit_window = b;
190 }
191 if let Some(b) = dict.get(b"CenterWindow").and_then(as_bool) {
192 prefs.center_window = b;
193 }
194 if let Some(b) = dict.get(b"DisplayDocTitle").and_then(as_bool) {
195 prefs.display_doc_title = b;
196 }
197 if let Some(name) = dict.get_name(b"NonFullScreenPageMode")
198 && let Some(m) = parse_page_mode(name)
199 {
200 prefs.non_full_screen_page_mode = m;
201 }
202 if let Some(name) = dict.get_name(b"Direction")
203 && let Some(d) = parse_direction(name)
204 {
205 prefs.direction = d;
206 }
207 if let Some(name) = dict.get_name(b"PrintScaling")
208 && let Some(p) = parse_print_scaling(name)
209 {
210 prefs.print_scaling = p;
211 }
212 if let Some(name) = dict.get_name(b"Duplex") {
213 prefs.duplex = parse_duplex(name);
214 }
215 if let Some(b) = dict.get(b"PickTrayByPDFSize").and_then(as_bool) {
216 prefs.pick_tray_by_pdf_size = Some(b);
217 }
218 if let Some(n) = dict.get_int(b"NumCopies")
219 && (1..=10_000).contains(&n)
220 {
221 prefs.num_copies = Some(n as u32);
222 }
223}
224
225fn as_bool(obj: &PdfObj) -> Option<bool> {
226 match obj {
227 PdfObj::Bool(b) => Some(*b),
228 _ => None,
229 }
230}
231
232fn parse_page_layout(name: &[u8]) -> Option<PageLayout> {
233 match name {
234 b"SinglePage" => Some(PageLayout::SinglePage),
235 b"OneColumn" => Some(PageLayout::OneColumn),
236 b"TwoColumnLeft" => Some(PageLayout::TwoColumnLeft),
237 b"TwoColumnRight" => Some(PageLayout::TwoColumnRight),
238 b"TwoPageLeft" => Some(PageLayout::TwoPageLeft),
239 b"TwoPageRight" => Some(PageLayout::TwoPageRight),
240 _ => None,
241 }
242}
243
244fn parse_page_mode(name: &[u8]) -> Option<PageMode> {
245 match name {
246 b"UseNone" => Some(PageMode::UseNone),
247 b"UseOutlines" => Some(PageMode::UseOutlines),
248 b"UseThumbs" => Some(PageMode::UseThumbs),
249 b"FullScreen" => Some(PageMode::FullScreen),
250 b"UseOC" => Some(PageMode::UseOC),
251 b"UseAttachments" => Some(PageMode::UseAttachments),
252 _ => None,
253 }
254}
255
256fn parse_direction(name: &[u8]) -> Option<ReadingDirection> {
257 match name {
258 b"L2R" => Some(ReadingDirection::L2R),
259 b"R2L" => Some(ReadingDirection::R2L),
260 _ => None,
261 }
262}
263
264fn parse_print_scaling(name: &[u8]) -> Option<PrintScaling> {
265 match name {
266 b"None" => Some(PrintScaling::None),
267 b"AppDefault" => Some(PrintScaling::AppDefault),
268 _ => None,
269 }
270}
271
272fn parse_duplex(name: &[u8]) -> Option<Duplex> {
273 match name {
274 b"Simplex" => Some(Duplex::Simplex),
275 b"DuplexFlipShortEdge" => Some(Duplex::DuplexFlipShortEdge),
276 b"DuplexFlipLongEdge" => Some(Duplex::DuplexFlipLongEdge),
277 _ => None,
278 }
279}
280
281fn catalog_dict(resolver: &Resolver) -> Option<PdfDict> {
282 if let Some((num, gen_num)) = resolver.trailer().get_ref(b"Root")
283 && let Ok(obj) = resolver.resolve(num, gen_num)
284 && let Some(dict) = obj.as_dict()
285 && (dict.get_name(b"Type") == Some(b"Catalog")
286 || dict.get(b"Pages").is_some()
287 || dict.get(b"PageLayout").is_some()
288 || dict.get(b"PageMode").is_some()
289 || dict.get(b"ViewerPreferences").is_some())
290 {
291 return Some(dict.clone());
292 }
293 crate::find_catalog(resolver).and_then(|obj| obj.as_dict().cloned())
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
301 fn defaults_are_sane() {
302 let p = ViewerPreferences::default();
303 assert!(!p.hide_toolbar);
304 assert_eq!(p.page_layout, PageLayout::SinglePage);
305 assert_eq!(p.page_mode, PageMode::UseNone);
306 assert_eq!(p.print_scaling, PrintScaling::AppDefault);
307 assert!(p.duplex.is_none());
308 }
309
310 #[test]
311 fn fill_from_dict_sets_overrides() {
312 let mut dict = PdfDict::new();
313 dict.insert(b"HideToolbar".to_vec(), PdfObj::Bool(true));
314 dict.insert(b"FitWindow".to_vec(), PdfObj::Bool(true));
315 dict.insert(b"PrintScaling".to_vec(), PdfObj::Name(b"None".to_vec()));
316 dict.insert(
317 b"Duplex".to_vec(),
318 PdfObj::Name(b"DuplexFlipLongEdge".to_vec()),
319 );
320 dict.insert(b"NumCopies".to_vec(), PdfObj::Int(3));
321 dict.insert(
322 b"NonFullScreenPageMode".to_vec(),
323 PdfObj::Name(b"UseOutlines".to_vec()),
324 );
325
326 let mut prefs = ViewerPreferences::default();
327 fill_viewer_prefs(&mut prefs, &dict);
328
329 assert!(prefs.hide_toolbar);
330 assert!(prefs.fit_window);
331 assert_eq!(prefs.print_scaling, PrintScaling::None);
332 assert_eq!(prefs.duplex, Some(Duplex::DuplexFlipLongEdge));
333 assert_eq!(prefs.num_copies, Some(3));
334 assert_eq!(prefs.non_full_screen_page_mode, PageMode::UseOutlines);
335 }
336
337 #[test]
338 fn unknown_name_leaves_default() {
339 let mut dict = PdfDict::new();
340 dict.insert(b"PrintScaling".to_vec(), PdfObj::Name(b"Bogus".to_vec()));
341 let mut prefs = ViewerPreferences::default();
342 fill_viewer_prefs(&mut prefs, &dict);
343 assert_eq!(prefs.print_scaling, PrintScaling::AppDefault);
344 }
345
346 #[test]
347 fn num_copies_out_of_range_ignored() {
348 let mut dict = PdfDict::new();
349 dict.insert(b"NumCopies".to_vec(), PdfObj::Int(0));
350 let mut prefs = ViewerPreferences::default();
351 fill_viewer_prefs(&mut prefs, &dict);
352 assert!(prefs.num_copies.is_none());
353
354 let mut dict = PdfDict::new();
355 dict.insert(b"NumCopies".to_vec(), PdfObj::Int(99_999));
356 let mut prefs = ViewerPreferences::default();
357 fill_viewer_prefs(&mut prefs, &dict);
358 assert!(prefs.num_copies.is_none());
359 }
360
361 #[test]
362 fn page_layout_round_trip() {
363 for (name, expect) in [
364 (&b"SinglePage"[..], PageLayout::SinglePage),
365 (&b"OneColumn"[..], PageLayout::OneColumn),
366 (&b"TwoColumnLeft"[..], PageLayout::TwoColumnLeft),
367 (&b"TwoPageRight"[..], PageLayout::TwoPageRight),
368 ] {
369 assert_eq!(parse_page_layout(name), Some(expect));
370 }
371 assert!(parse_page_layout(b"unknown").is_none());
372 }
373}