1use std::collections::HashMap;
7
8use rpdfium_core::{Name, PdfSource};
9use rpdfium_parser::object::Object;
10use rpdfium_parser::store::ObjectStore;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum ReadingDirection {
15 L2R,
17 R2L,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26pub enum DuplexMode {
27 #[default]
29 Simplex,
30 DuplexFlipShortEdge,
32 DuplexFlipLongEdge,
34}
35
36#[derive(Debug, Clone)]
38pub struct ViewerPreferences {
39 pub hide_toolbar: bool,
41 pub hide_menubar: bool,
43 pub hide_window_ui: bool,
45 pub fit_window: bool,
47 pub center_window: bool,
49 pub display_doc_title: bool,
51 pub direction: ReadingDirection,
53 pub print_scaling: Option<String>,
55 pub num_copies: Option<u32>,
57 pub duplex: Option<String>,
59 pub print_page_range: Option<Vec<i64>>,
61}
62
63impl Default for ViewerPreferences {
64 fn default() -> Self {
65 Self {
66 hide_toolbar: false,
67 hide_menubar: false,
68 hide_window_ui: false,
69 fit_window: false,
70 center_window: false,
71 display_doc_title: false,
72 direction: ReadingDirection::L2R,
73 print_scaling: None,
74 num_copies: None,
75 duplex: None,
76 print_page_range: None,
77 }
78 }
79}
80
81impl ViewerPreferences {
82 pub fn from_dict<S: PdfSource>(dict: &HashMap<Name, Object>, store: &ObjectStore<S>) -> Self {
84 let get_bool = |name: &Name| -> bool {
85 dict.get(name)
86 .and_then(|o| store.deep_resolve(o).ok())
87 .and_then(|o| o.as_bool())
88 .unwrap_or(false)
89 };
90
91 let get_name_string = |name: &Name| -> Option<String> {
92 dict.get(name)
93 .and_then(|o| store.deep_resolve(o).ok())
94 .and_then(|o| o.as_name().map(|n| n.as_str().into_owned()))
95 };
96
97 let get_string = |name: &Name| -> Option<String> {
98 dict.get(name)
99 .and_then(|o| store.deep_resolve(o).ok())
100 .and_then(|o| {
101 if let Some(s) = o.as_string() {
102 Some(s.to_string_lossy())
103 } else {
104 o.as_name().map(|n| n.as_str().into_owned())
105 }
106 })
107 };
108
109 let direction = match get_name_string(&Name::direction()).as_deref() {
110 Some("R2L") => ReadingDirection::R2L,
111 _ => ReadingDirection::L2R,
112 };
113
114 let num_copies = dict
115 .get(&Name::num_copies())
116 .and_then(|o| store.deep_resolve(o).ok())
117 .and_then(|o| o.as_i64())
118 .map(|n| n.max(1) as u32);
119
120 let print_page_range = dict
122 .get(&Name::print_page_range())
123 .and_then(|o| store.deep_resolve(o).ok())
124 .and_then(|o| {
125 o.as_array().map(|arr| {
126 arr.iter()
127 .filter_map(|item| item.as_i64())
128 .collect::<Vec<i64>>()
129 })
130 })
131 .filter(|v| !v.is_empty());
132
133 Self {
134 hide_toolbar: get_bool(&Name::hide_toolbar()),
135 hide_menubar: get_bool(&Name::hide_menubar()),
136 hide_window_ui: get_bool(&Name::hide_window_ui()),
137 fit_window: get_bool(&Name::fit_window()),
138 center_window: get_bool(&Name::center_window()),
139 display_doc_title: get_bool(&Name::display_doc_title()),
140 direction,
141 print_scaling: get_string(&Name::print_scaling()),
142 num_copies,
143 duplex: get_string(&Name::duplex()),
144 print_page_range,
145 }
146 }
147
148 pub fn is_direction_r2l(&self) -> bool {
152 self.direction == ReadingDirection::R2L
153 }
154
155 pub fn print_scaling(&self) -> bool {
162 self.print_scaling.as_deref() != Some("None")
163 }
164
165 pub fn is_print_scaling_suppressed(&self) -> bool {
170 !self.print_scaling()
171 }
172
173 pub fn num_copies(&self) -> Option<u32> {
178 self.num_copies
179 }
180
181 #[deprecated(since = "0.1.0", note = "use num_copies() instead")]
184 #[inline]
185 pub fn get_num_copies(&self) -> Option<u32> {
186 self.num_copies()
187 }
188
189 pub fn print_page_range(&self) -> Option<&[i64]> {
196 self.print_page_range.as_deref()
197 }
198
199 pub fn duplex_mode(&self) -> DuplexMode {
206 match self.duplex.as_deref() {
207 Some("DuplexFlipShortEdge") => DuplexMode::DuplexFlipShortEdge,
208 Some("DuplexFlipLongEdge") => DuplexMode::DuplexFlipLongEdge,
209 _ => DuplexMode::Simplex,
210 }
211 }
212
213 #[inline]
217 pub fn duplex(&self) -> DuplexMode {
218 self.duplex_mode()
219 }
220
221 pub fn generic_name(&self, key: &str) -> Option<&str> {
230 match key {
231 "HideToolbar" => self.hide_toolbar.then_some("true"),
232 "HideMenubar" => self.hide_menubar.then_some("true"),
233 "HideWindowUI" => self.hide_window_ui.then_some("true"),
234 "FitWindow" => self.fit_window.then_some("true"),
235 "CenterWindow" => self.center_window.then_some("true"),
236 "DisplayDocTitle" => self.display_doc_title.then_some("true"),
237 "Direction" => match self.direction {
238 ReadingDirection::R2L => Some("R2L"),
239 ReadingDirection::L2R => Some("L2R"),
240 },
241 "PrintScaling" => self.print_scaling.as_deref(),
242 "Duplex" => self.duplex.as_deref(),
243 _ => None,
244 }
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 fn build_store() -> ObjectStore<Vec<u8>> {
253 let pdf = build_minimal_pdf();
254 ObjectStore::open(pdf, rpdfium_core::ParsingMode::Lenient).unwrap()
255 }
256
257 fn build_minimal_pdf() -> Vec<u8> {
258 let mut pdf = Vec::new();
259 pdf.extend_from_slice(b"%PDF-1.4\n");
260 let obj1_offset = pdf.len();
261 pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
262 let obj2_offset = pdf.len();
263 pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
264 let xref_offset = pdf.len();
265 pdf.extend_from_slice(b"xref\n0 3\n");
266 pdf.extend_from_slice(b"0000000000 65535 f \r\n");
267 pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj1_offset).as_bytes());
268 pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj2_offset).as_bytes());
269 pdf.extend_from_slice(b"trailer\n<< /Size 3 /Root 1 0 R >>\n");
270 pdf.extend_from_slice(format!("startxref\n{}\n%%EOF", xref_offset).as_bytes());
271 pdf
272 }
273
274 #[test]
275 fn test_default_preferences() {
276 let prefs = ViewerPreferences::default();
277 assert!(!prefs.hide_toolbar);
278 assert!(!prefs.hide_menubar);
279 assert!(!prefs.hide_window_ui);
280 assert!(!prefs.fit_window);
281 assert!(!prefs.center_window);
282 assert!(!prefs.display_doc_title);
283 assert_eq!(prefs.direction, ReadingDirection::L2R);
284 assert!(prefs.print_scaling.is_none());
285 assert!(prefs.num_copies.is_none());
286 assert!(prefs.duplex.is_none());
287 assert!(prefs.print_page_range.is_none());
288 }
289
290 #[test]
291 fn test_parse_all_fields() {
292 let store = build_store();
293 let mut dict = HashMap::new();
294 dict.insert(Name::hide_toolbar(), Object::Boolean(true));
295 dict.insert(Name::hide_menubar(), Object::Boolean(true));
296 dict.insert(Name::hide_window_ui(), Object::Boolean(true));
297 dict.insert(Name::fit_window(), Object::Boolean(true));
298 dict.insert(Name::center_window(), Object::Boolean(true));
299 dict.insert(Name::display_doc_title(), Object::Boolean(true));
300 dict.insert(Name::direction(), Object::Name(Name::from("R2L")));
301 dict.insert(Name::print_scaling(), Object::Name(Name::from("None")));
302 dict.insert(Name::num_copies(), Object::Integer(3));
303 dict.insert(
304 Name::duplex(),
305 Object::Name(Name::from("DuplexFlipLongEdge")),
306 );
307
308 let prefs = ViewerPreferences::from_dict(&dict, &store);
309 assert!(prefs.hide_toolbar);
310 assert!(prefs.hide_menubar);
311 assert!(prefs.hide_window_ui);
312 assert!(prefs.fit_window);
313 assert!(prefs.center_window);
314 assert!(prefs.display_doc_title);
315 assert_eq!(prefs.direction, ReadingDirection::R2L);
316 assert_eq!(prefs.print_scaling.as_deref(), Some("None"));
317 assert_eq!(prefs.num_copies, Some(3));
318 assert_eq!(prefs.duplex.as_deref(), Some("DuplexFlipLongEdge"));
319 }
320
321 #[test]
322 fn test_parse_empty_dict() {
323 let store = build_store();
324 let dict = HashMap::new();
325 let prefs = ViewerPreferences::from_dict(&dict, &store);
326 assert!(!prefs.hide_toolbar);
327 assert_eq!(prefs.direction, ReadingDirection::L2R);
328 assert!(prefs.print_scaling.is_none());
329 }
330
331 #[test]
332 fn test_print_page_range_parsed() {
333 let store = build_store();
334 let mut dict = HashMap::new();
335 dict.insert(
336 Name::print_page_range(),
337 Object::Array(vec![
338 Object::Integer(0),
339 Object::Integer(3),
340 Object::Integer(5),
341 Object::Integer(7),
342 ]),
343 );
344 let prefs = ViewerPreferences::from_dict(&dict, &store);
345 let range = prefs.print_page_range.unwrap();
346 assert_eq!(range, vec![0, 3, 5, 7]);
347 }
348
349 #[test]
350 fn test_print_page_range_empty_is_none() {
351 let store = build_store();
352 let mut dict = HashMap::new();
353 dict.insert(Name::print_page_range(), Object::Array(vec![]));
354 let prefs = ViewerPreferences::from_dict(&dict, &store);
355 assert!(prefs.print_page_range.is_none());
356 }
357
358 #[test]
359 fn test_direction_default_l2r() {
360 let store = build_store();
361 let mut dict = HashMap::new();
362 dict.insert(Name::direction(), Object::Name(Name::from("L2R")));
363 let prefs = ViewerPreferences::from_dict(&dict, &store);
364 assert_eq!(prefs.direction, ReadingDirection::L2R);
365 }
366
367 #[test]
368 fn test_direction_r2l() {
369 let store = build_store();
370 let mut dict = HashMap::new();
371 dict.insert(Name::direction(), Object::Name(Name::from("R2L")));
372 let prefs = ViewerPreferences::from_dict(&dict, &store);
373 assert_eq!(prefs.direction, ReadingDirection::R2L);
374 }
375
376 #[test]
377 fn test_is_print_scaling_suppressed_none_value() {
378 let store = build_store();
379 let mut dict = HashMap::new();
380 dict.insert(Name::print_scaling(), Object::Name(Name::from("None")));
381 let prefs = ViewerPreferences::from_dict(&dict, &store);
382 assert!(prefs.is_print_scaling_suppressed());
383 }
384
385 #[test]
386 fn test_is_print_scaling_suppressed_app_default() {
387 let store = build_store();
388 let mut dict = HashMap::new();
389 dict.insert(
390 Name::print_scaling(),
391 Object::Name(Name::from("AppDefault")),
392 );
393 let prefs = ViewerPreferences::from_dict(&dict, &store);
394 assert!(!prefs.is_print_scaling_suppressed());
395 }
396
397 #[test]
398 fn test_is_print_scaling_suppressed_absent() {
399 let store = build_store();
400 let prefs = ViewerPreferences::from_dict(&HashMap::new(), &store);
401 assert!(!prefs.is_print_scaling_suppressed());
402 }
403
404 #[test]
405 fn test_generic_name_direction() {
406 let store = build_store();
407 let mut dict = HashMap::new();
408 dict.insert(Name::direction(), Object::Name(Name::from("R2L")));
409 let prefs = ViewerPreferences::from_dict(&dict, &store);
410 assert_eq!(prefs.generic_name("Direction"), Some("R2L"));
411 }
412
413 #[test]
414 fn test_generic_name_print_scaling() {
415 let store = build_store();
416 let mut dict = HashMap::new();
417 dict.insert(Name::print_scaling(), Object::Name(Name::from("None")));
418 let prefs = ViewerPreferences::from_dict(&dict, &store);
419 assert_eq!(prefs.generic_name("PrintScaling"), Some("None"));
420 }
421
422 #[test]
423 fn test_generic_name_unknown_key_is_none() {
424 let prefs = ViewerPreferences::default();
425 assert_eq!(prefs.generic_name("SomeUnknownKey"), None);
426 }
427
428 #[test]
429 fn test_generic_name_duplex() {
430 let store = build_store();
431 let mut dict = HashMap::new();
432 dict.insert(
433 Name::duplex(),
434 Object::Name(Name::from("DuplexFlipShortEdge")),
435 );
436 let prefs = ViewerPreferences::from_dict(&dict, &store);
437 assert_eq!(prefs.generic_name("Duplex"), Some("DuplexFlipShortEdge"));
438 }
439
440 #[test]
441 fn test_duplex_mode_simplex_by_default() {
442 let prefs = ViewerPreferences::default();
443 assert_eq!(prefs.duplex_mode(), DuplexMode::Simplex);
444 }
445
446 #[test]
447 fn test_duplex_mode_flip_short_edge() {
448 let store = build_store();
449 let mut dict = HashMap::new();
450 dict.insert(
451 Name::duplex(),
452 Object::Name(Name::from("DuplexFlipShortEdge")),
453 );
454 let prefs = ViewerPreferences::from_dict(&dict, &store);
455 assert_eq!(prefs.duplex_mode(), DuplexMode::DuplexFlipShortEdge);
456 }
457
458 #[test]
459 fn test_duplex_mode_flip_long_edge() {
460 let store = build_store();
461 let mut dict = HashMap::new();
462 dict.insert(
463 Name::duplex(),
464 Object::Name(Name::from("DuplexFlipLongEdge")),
465 );
466 let prefs = ViewerPreferences::from_dict(&dict, &store);
467 assert_eq!(prefs.duplex_mode(), DuplexMode::DuplexFlipLongEdge);
468 }
469
470 #[test]
471 fn test_duplex_mode_unknown_is_simplex() {
472 let store = build_store();
473 let mut dict = HashMap::new();
474 dict.insert(Name::duplex(), Object::Name(Name::from("Unknown")));
475 let prefs = ViewerPreferences::from_dict(&dict, &store);
476 assert_eq!(prefs.duplex_mode(), DuplexMode::Simplex);
477 }
478}