Skip to main content

oxidize_pdf/
viewer_preferences.rs

1//! Viewer preferences control how the PDF document is displayed in the viewer
2//!
3//! These preferences are defined in ISO 32000-1:2008 and provide control over
4//! the user interface and display behavior when the document is opened.
5
6use crate::objects::{Dictionary, Object};
7
8/// Page layout modes
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub enum PageLayout {
11    /// Display one page at a time
12    SinglePage,
13    /// Display pages in one column
14    OneColumn,
15    /// Display pages in two columns, odd-numbered pages on the left
16    TwoColumnLeft,
17    /// Display pages in two columns, odd-numbered pages on the right  
18    TwoColumnRight,
19    /// Display pages in two columns, cover page displayed alone
20    TwoPageLeft,
21    /// Display pages in two columns, cover page displayed alone
22    TwoPageRight,
23}
24
25impl PageLayout {
26    /// Convert to PDF name
27    pub fn to_pdf_name(&self) -> &'static str {
28        match self {
29            PageLayout::SinglePage => "SinglePage",
30            PageLayout::OneColumn => "OneColumn",
31            PageLayout::TwoColumnLeft => "TwoColumnLeft",
32            PageLayout::TwoColumnRight => "TwoColumnRight",
33            PageLayout::TwoPageLeft => "TwoPageLeft",
34            PageLayout::TwoPageRight => "TwoPageRight",
35        }
36    }
37}
38
39/// Page mode - how to display the document when opened
40#[derive(Debug, Clone, Copy, PartialEq)]
41pub enum PageMode {
42    /// Neither document outline nor thumbnail images visible
43    UseNone,
44    /// Document outline visible
45    UseOutlines,
46    /// Thumbnail images visible
47    UseThumbs,
48    /// Full-screen mode, hiding all menu bars, window controls, etc.
49    FullScreen,
50    /// Optional content group panel visible
51    UseOC,
52    /// Attachments panel visible
53    UseAttachments,
54}
55
56impl PageMode {
57    /// Convert to PDF name
58    pub fn to_pdf_name(&self) -> &'static str {
59        match self {
60            PageMode::UseNone => "UseNone",
61            PageMode::UseOutlines => "UseOutlines",
62            PageMode::UseThumbs => "UseThumbs",
63            PageMode::FullScreen => "FullScreen",
64            PageMode::UseOC => "UseOC",
65            PageMode::UseAttachments => "UseAttachments",
66        }
67    }
68}
69
70/// Non-full-screen page mode
71#[derive(Debug, Clone, Copy, PartialEq)]
72pub enum NonFullScreenPageMode {
73    /// Neither document outline nor thumbnail images visible
74    UseNone,
75    /// Document outline visible
76    UseOutlines,
77    /// Thumbnail images visible
78    UseThumbs,
79    /// Optional content group panel visible
80    UseOC,
81}
82
83impl NonFullScreenPageMode {
84    /// Convert to PDF name
85    pub fn to_pdf_name(&self) -> &'static str {
86        match self {
87            NonFullScreenPageMode::UseNone => "UseNone",
88            NonFullScreenPageMode::UseOutlines => "UseOutlines",
89            NonFullScreenPageMode::UseThumbs => "UseThumbs",
90            NonFullScreenPageMode::UseOC => "UseOC",
91        }
92    }
93}
94
95/// Direction for reading order
96#[derive(Debug, Clone, Copy, PartialEq)]
97pub enum Direction {
98    /// Left to right
99    L2R,
100    /// Right to left
101    R2L,
102}
103
104impl Direction {
105    /// Convert to PDF name
106    pub fn to_pdf_name(&self) -> &'static str {
107        match self {
108            Direction::L2R => "L2R",
109            Direction::R2L => "R2L",
110        }
111    }
112}
113
114/// Print scaling options
115#[derive(Debug, Clone, Copy, PartialEq)]
116pub enum PrintScaling {
117    /// No scaling
118    None,
119    /// Scale to fit page
120    AppDefault,
121}
122
123impl PrintScaling {
124    /// Convert to PDF name
125    pub fn to_pdf_name(&self) -> &'static str {
126        match self {
127            PrintScaling::None => "None",
128            PrintScaling::AppDefault => "AppDefault",
129        }
130    }
131}
132
133/// Duplex printing modes
134#[derive(Debug, Clone, Copy, PartialEq)]
135pub enum Duplex {
136    /// Single-sided printing
137    Simplex,
138    /// Double-sided printing, flip on short edge
139    DuplexFlipShortEdge,
140    /// Double-sided printing, flip on long edge
141    DuplexFlipLongEdge,
142}
143
144impl Duplex {
145    /// Convert to PDF name
146    pub fn to_pdf_name(&self) -> &'static str {
147        match self {
148            Duplex::Simplex => "Simplex",
149            Duplex::DuplexFlipShortEdge => "DuplexFlipShortEdge",
150            Duplex::DuplexFlipLongEdge => "DuplexFlipLongEdge",
151        }
152    }
153}
154
155/// Viewer preferences for controlling document display
156#[derive(Debug, Clone, Default)]
157pub struct ViewerPreferences {
158    /// Hide the application's tool bars
159    pub hide_toolbar: Option<bool>,
160    /// Hide the application's menu bar
161    pub hide_menubar: Option<bool>,
162    /// Hide user interface elements like scroll bars, navigation controls
163    pub hide_window_ui: Option<bool>,
164    /// Resize document window to fit the size of the first displayed page
165    pub fit_window: Option<bool>,
166    /// Center the document window on the screen
167    pub center_window: Option<bool>,
168    /// Display document title instead of filename in window title bar
169    pub display_doc_title: Option<bool>,
170    /// Page layout to use when document is opened
171    pub page_layout: Option<PageLayout>,
172    /// How to display document when opened
173    pub page_mode: Option<PageMode>,
174    /// Page mode after exiting full-screen mode
175    pub non_full_screen_page_mode: Option<NonFullScreenPageMode>,
176    /// Reading order direction
177    pub direction: Option<Direction>,
178    /// Area of default page to display when document is opened
179    pub view_area: Option<String>,
180    /// Area of page to use for clipping when displaying
181    pub view_clip: Option<String>,
182    /// Area to use for printing
183    pub print_area: Option<String>,
184    /// Area to use for clipping when printing
185    pub print_clip: Option<String>,
186    /// Print scaling mode
187    pub print_scaling: Option<PrintScaling>,
188    /// Duplex printing mode
189    pub duplex: Option<Duplex>,
190    /// Page ranges for printing
191    pub print_page_range: Option<Vec<(u32, u32)>>,
192    /// Number of copies
193    pub num_copies: Option<u32>,
194    /// Whether to pick tray by PDF size
195    pub pick_tray_by_pdf_size: Option<bool>,
196}
197
198impl ViewerPreferences {
199    /// Create new viewer preferences with default values
200    pub fn new() -> Self {
201        ViewerPreferences::default()
202    }
203
204    /// Hide toolbar
205    pub fn hide_toolbar(mut self, hide: bool) -> Self {
206        self.hide_toolbar = Some(hide);
207        self
208    }
209
210    /// Hide menubar
211    pub fn hide_menubar(mut self, hide: bool) -> Self {
212        self.hide_menubar = Some(hide);
213        self
214    }
215
216    /// Hide window UI elements
217    pub fn hide_window_ui(mut self, hide: bool) -> Self {
218        self.hide_window_ui = Some(hide);
219        self
220    }
221
222    /// Fit window to page size
223    pub fn fit_window(mut self, fit: bool) -> Self {
224        self.fit_window = Some(fit);
225        self
226    }
227
228    /// Center window on screen
229    pub fn center_window(mut self, center: bool) -> Self {
230        self.center_window = Some(center);
231        self
232    }
233
234    /// Display document title in window title bar
235    pub fn display_doc_title(mut self, display: bool) -> Self {
236        self.display_doc_title = Some(display);
237        self
238    }
239
240    /// Set page layout
241    pub fn page_layout(mut self, layout: PageLayout) -> Self {
242        self.page_layout = Some(layout);
243        self
244    }
245
246    /// Set page mode
247    pub fn page_mode(mut self, mode: PageMode) -> Self {
248        self.page_mode = Some(mode);
249        self
250    }
251
252    /// Set non-full-screen page mode
253    pub fn non_full_screen_page_mode(mut self, mode: NonFullScreenPageMode) -> Self {
254        self.non_full_screen_page_mode = Some(mode);
255        self
256    }
257
258    /// Set reading direction
259    pub fn direction(mut self, direction: Direction) -> Self {
260        self.direction = Some(direction);
261        self
262    }
263
264    /// Set print scaling
265    pub fn print_scaling(mut self, scaling: PrintScaling) -> Self {
266        self.print_scaling = Some(scaling);
267        self
268    }
269
270    /// Set duplex mode
271    pub fn duplex(mut self, duplex: Duplex) -> Self {
272        self.duplex = Some(duplex);
273        self
274    }
275
276    /// Set number of copies for printing
277    pub fn num_copies(mut self, copies: u32) -> Self {
278        self.num_copies = Some(copies.max(1));
279        self
280    }
281
282    /// Pick tray by PDF size
283    pub fn pick_tray_by_pdf_size(mut self, pick: bool) -> Self {
284        self.pick_tray_by_pdf_size = Some(pick);
285        self
286    }
287
288    /// Add page range for printing
289    pub fn add_print_page_range(mut self, start: u32, end: u32) -> Self {
290        if self.print_page_range.is_none() {
291            self.print_page_range = Some(Vec::new());
292        }
293        if let Some(ref mut ranges) = self.print_page_range {
294            ranges.push((start.min(end), start.max(end)));
295        }
296        self
297    }
298
299    /// Convert to PDF dictionary
300    pub fn to_dict(&self) -> Dictionary {
301        let mut dict = Dictionary::new();
302
303        if let Some(hide) = self.hide_toolbar {
304            dict.set("HideToolbar", Object::Boolean(hide));
305        }
306
307        if let Some(hide) = self.hide_menubar {
308            dict.set("HideMenubar", Object::Boolean(hide));
309        }
310
311        if let Some(hide) = self.hide_window_ui {
312            dict.set("HideWindowUI", Object::Boolean(hide));
313        }
314
315        if let Some(fit) = self.fit_window {
316            dict.set("FitWindow", Object::Boolean(fit));
317        }
318
319        if let Some(center) = self.center_window {
320            dict.set("CenterWindow", Object::Boolean(center));
321        }
322
323        if let Some(display) = self.display_doc_title {
324            dict.set("DisplayDocTitle", Object::Boolean(display));
325        }
326
327        if let Some(layout) = self.page_layout {
328            dict.set("PageLayout", Object::Name(layout.to_pdf_name().to_string()));
329        }
330
331        if let Some(mode) = self.page_mode {
332            dict.set("PageMode", Object::Name(mode.to_pdf_name().to_string()));
333        }
334
335        if let Some(mode) = self.non_full_screen_page_mode {
336            dict.set(
337                "NonFullScreenPageMode",
338                Object::Name(mode.to_pdf_name().to_string()),
339            );
340        }
341
342        if let Some(direction) = self.direction {
343            dict.set(
344                "Direction",
345                Object::Name(direction.to_pdf_name().to_string()),
346            );
347        }
348
349        if let Some(ref area) = self.view_area {
350            dict.set("ViewArea", Object::Name(area.clone()));
351        }
352
353        if let Some(ref clip) = self.view_clip {
354            dict.set("ViewClip", Object::Name(clip.clone()));
355        }
356
357        if let Some(ref area) = self.print_area {
358            dict.set("PrintArea", Object::Name(area.clone()));
359        }
360
361        if let Some(ref clip) = self.print_clip {
362            dict.set("PrintClip", Object::Name(clip.clone()));
363        }
364
365        if let Some(scaling) = self.print_scaling {
366            dict.set(
367                "PrintScaling",
368                Object::Name(scaling.to_pdf_name().to_string()),
369            );
370        }
371
372        if let Some(duplex) = self.duplex {
373            dict.set("Duplex", Object::Name(duplex.to_pdf_name().to_string()));
374        }
375
376        if let Some(ref ranges) = self.print_page_range {
377            let range_array: Vec<Object> = ranges
378                .iter()
379                .flat_map(|(start, end)| {
380                    vec![Object::Integer(*start as i64), Object::Integer(*end as i64)]
381                })
382                .collect();
383            dict.set("PrintPageRange", Object::Array(range_array));
384        }
385
386        if let Some(copies) = self.num_copies {
387            dict.set("NumCopies", Object::Integer(copies as i64));
388        }
389
390        if let Some(pick) = self.pick_tray_by_pdf_size {
391            dict.set("PickTrayByPDFSize", Object::Boolean(pick));
392        }
393
394        dict
395    }
396
397    // Convenience constructors
398
399    /// Create preferences for presentation mode
400    pub fn presentation() -> Self {
401        ViewerPreferences::new()
402            .page_mode(PageMode::FullScreen)
403            .hide_toolbar(true)
404            .hide_menubar(true)
405            .hide_window_ui(true)
406            .fit_window(true)
407            .center_window(true)
408    }
409
410    /// Create preferences for reading mode
411    pub fn reading() -> Self {
412        ViewerPreferences::new()
413            .page_layout(PageLayout::TwoColumnRight)
414            .page_mode(PageMode::UseOutlines)
415            .fit_window(true)
416            .display_doc_title(true)
417    }
418
419    /// Create preferences for printing
420    pub fn printing() -> Self {
421        ViewerPreferences::new()
422            .print_scaling(PrintScaling::None)
423            .duplex(Duplex::DuplexFlipLongEdge)
424            .pick_tray_by_pdf_size(true)
425    }
426
427    /// Create minimal UI preferences
428    pub fn minimal_ui() -> Self {
429        ViewerPreferences::new()
430            .hide_toolbar(true)
431            .hide_menubar(true)
432            .center_window(true)
433            .fit_window(true)
434    }
435}
436
437#[cfg(test)]
438mod tests {
439    use super::*;
440
441    #[test]
442    fn test_page_layout_names() {
443        assert_eq!(PageLayout::SinglePage.to_pdf_name(), "SinglePage");
444        assert_eq!(PageLayout::OneColumn.to_pdf_name(), "OneColumn");
445        assert_eq!(PageLayout::TwoColumnLeft.to_pdf_name(), "TwoColumnLeft");
446        assert_eq!(PageLayout::TwoColumnRight.to_pdf_name(), "TwoColumnRight");
447        assert_eq!(PageLayout::TwoPageLeft.to_pdf_name(), "TwoPageLeft");
448        assert_eq!(PageLayout::TwoPageRight.to_pdf_name(), "TwoPageRight");
449    }
450
451    #[test]
452    fn test_page_mode_names() {
453        assert_eq!(PageMode::UseNone.to_pdf_name(), "UseNone");
454        assert_eq!(PageMode::UseOutlines.to_pdf_name(), "UseOutlines");
455        assert_eq!(PageMode::UseThumbs.to_pdf_name(), "UseThumbs");
456        assert_eq!(PageMode::FullScreen.to_pdf_name(), "FullScreen");
457        assert_eq!(PageMode::UseOC.to_pdf_name(), "UseOC");
458        assert_eq!(PageMode::UseAttachments.to_pdf_name(), "UseAttachments");
459    }
460
461    #[test]
462    fn test_basic_preferences() {
463        let prefs = ViewerPreferences::new()
464            .hide_toolbar(true)
465            .hide_menubar(true)
466            .fit_window(true);
467
468        let dict = prefs.to_dict();
469        assert_eq!(dict.get("HideToolbar"), Some(&Object::Boolean(true)));
470        assert_eq!(dict.get("HideMenubar"), Some(&Object::Boolean(true)));
471        assert_eq!(dict.get("FitWindow"), Some(&Object::Boolean(true)));
472    }
473
474    #[test]
475    fn test_page_layout_preference() {
476        let prefs = ViewerPreferences::new().page_layout(PageLayout::TwoColumnLeft);
477
478        let dict = prefs.to_dict();
479        assert_eq!(
480            dict.get("PageLayout"),
481            Some(&Object::Name("TwoColumnLeft".to_string()))
482        );
483    }
484
485    #[test]
486    fn test_print_preferences() {
487        let prefs = ViewerPreferences::new()
488            .print_scaling(PrintScaling::None)
489            .duplex(Duplex::DuplexFlipLongEdge)
490            .num_copies(3);
491
492        let dict = prefs.to_dict();
493        assert_eq!(
494            dict.get("PrintScaling"),
495            Some(&Object::Name("None".to_string()))
496        );
497        assert_eq!(
498            dict.get("Duplex"),
499            Some(&Object::Name("DuplexFlipLongEdge".to_string()))
500        );
501        assert_eq!(dict.get("NumCopies"), Some(&Object::Integer(3)));
502    }
503
504    #[test]
505    fn test_print_page_ranges() {
506        let prefs = ViewerPreferences::new()
507            .add_print_page_range(1, 5)
508            .add_print_page_range(10, 15);
509
510        let dict = prefs.to_dict();
511        if let Some(Object::Array(ranges)) = dict.get("PrintPageRange") {
512            assert_eq!(ranges.len(), 4); // Two ranges, each with start and end
513            assert_eq!(ranges[0], Object::Integer(1));
514            assert_eq!(ranges[1], Object::Integer(5));
515            assert_eq!(ranges[2], Object::Integer(10));
516            assert_eq!(ranges[3], Object::Integer(15));
517        } else {
518            panic!("Expected PrintPageRange array");
519        }
520    }
521
522    #[test]
523    fn test_convenience_constructors() {
524        let presentation = ViewerPreferences::presentation();
525        assert_eq!(presentation.page_mode, Some(PageMode::FullScreen));
526        assert_eq!(presentation.hide_toolbar, Some(true));
527
528        let reading = ViewerPreferences::reading();
529        assert_eq!(reading.page_layout, Some(PageLayout::TwoColumnRight));
530        assert_eq!(reading.page_mode, Some(PageMode::UseOutlines));
531
532        let printing = ViewerPreferences::printing();
533        assert_eq!(printing.print_scaling, Some(PrintScaling::None));
534        assert_eq!(printing.duplex, Some(Duplex::DuplexFlipLongEdge));
535
536        let minimal = ViewerPreferences::minimal_ui();
537        assert_eq!(minimal.hide_toolbar, Some(true));
538        assert_eq!(minimal.hide_menubar, Some(true));
539    }
540
541    #[test]
542    fn test_num_copies_bounds() {
543        let prefs = ViewerPreferences::new().num_copies(0);
544        assert_eq!(prefs.num_copies, Some(1)); // Should be clamped to minimum 1
545    }
546
547    #[test]
548    fn test_direction() {
549        assert_eq!(Direction::L2R.to_pdf_name(), "L2R");
550        assert_eq!(Direction::R2L.to_pdf_name(), "R2L");
551    }
552
553    #[test]
554    fn test_print_scaling() {
555        assert_eq!(PrintScaling::None.to_pdf_name(), "None");
556        assert_eq!(PrintScaling::AppDefault.to_pdf_name(), "AppDefault");
557    }
558
559    #[test]
560    fn test_duplex_modes() {
561        assert_eq!(Duplex::Simplex.to_pdf_name(), "Simplex");
562        assert_eq!(
563            Duplex::DuplexFlipShortEdge.to_pdf_name(),
564            "DuplexFlipShortEdge"
565        );
566        assert_eq!(
567            Duplex::DuplexFlipLongEdge.to_pdf_name(),
568            "DuplexFlipLongEdge"
569        );
570    }
571
572    #[test]
573    fn test_empty_preferences() {
574        let prefs = ViewerPreferences::new();
575        let dict = prefs.to_dict();
576        assert!(dict.is_empty()); // No preferences set, should result in empty dictionary
577    }
578
579    #[test]
580    fn test_non_full_screen_page_mode_names() {
581        assert_eq!(NonFullScreenPageMode::UseNone.to_pdf_name(), "UseNone");
582        assert_eq!(
583            NonFullScreenPageMode::UseOutlines.to_pdf_name(),
584            "UseOutlines"
585        );
586        assert_eq!(NonFullScreenPageMode::UseThumbs.to_pdf_name(), "UseThumbs");
587        assert_eq!(NonFullScreenPageMode::UseOC.to_pdf_name(), "UseOC");
588    }
589
590    #[test]
591    fn test_non_full_screen_page_mode_to_dict() {
592        let prefs =
593            ViewerPreferences::new().non_full_screen_page_mode(NonFullScreenPageMode::UseOutlines);
594        let dict = prefs.to_dict();
595        assert_eq!(
596            dict.get("NonFullScreenPageMode"),
597            Some(&Object::Name("UseOutlines".to_string()))
598        );
599    }
600
601    #[test]
602    fn test_direction_to_dict() {
603        let prefs_l2r = ViewerPreferences::new().direction(Direction::L2R);
604        let dict_l2r = prefs_l2r.to_dict();
605        assert_eq!(
606            dict_l2r.get("Direction"),
607            Some(&Object::Name("L2R".to_string()))
608        );
609
610        let prefs_r2l = ViewerPreferences::new().direction(Direction::R2L);
611        let dict_r2l = prefs_r2l.to_dict();
612        assert_eq!(
613            dict_r2l.get("Direction"),
614            Some(&Object::Name("R2L".to_string()))
615        );
616    }
617
618    #[test]
619    fn test_view_area_and_clip() {
620        let mut prefs = ViewerPreferences::new();
621        prefs.view_area = Some("MediaBox".to_string());
622        prefs.view_clip = Some("CropBox".to_string());
623
624        let dict = prefs.to_dict();
625        assert_eq!(
626            dict.get("ViewArea"),
627            Some(&Object::Name("MediaBox".to_string()))
628        );
629        assert_eq!(
630            dict.get("ViewClip"),
631            Some(&Object::Name("CropBox".to_string()))
632        );
633    }
634
635    #[test]
636    fn test_print_area_and_clip() {
637        let mut prefs = ViewerPreferences::new();
638        prefs.print_area = Some("BleedBox".to_string());
639        prefs.print_clip = Some("TrimBox".to_string());
640
641        let dict = prefs.to_dict();
642        assert_eq!(
643            dict.get("PrintArea"),
644            Some(&Object::Name("BleedBox".to_string()))
645        );
646        assert_eq!(
647            dict.get("PrintClip"),
648            Some(&Object::Name("TrimBox".to_string()))
649        );
650    }
651
652    #[test]
653    fn test_pick_tray_by_pdf_size_to_dict() {
654        let prefs = ViewerPreferences::new().pick_tray_by_pdf_size(true);
655        let dict = prefs.to_dict();
656        assert_eq!(dict.get("PickTrayByPDFSize"), Some(&Object::Boolean(true)));
657
658        let prefs_false = ViewerPreferences::new().pick_tray_by_pdf_size(false);
659        let dict_false = prefs_false.to_dict();
660        assert_eq!(
661            dict_false.get("PickTrayByPDFSize"),
662            Some(&Object::Boolean(false))
663        );
664    }
665
666    #[test]
667    fn test_hide_window_ui_to_dict() {
668        let prefs = ViewerPreferences::new().hide_window_ui(true);
669        let dict = prefs.to_dict();
670        assert_eq!(dict.get("HideWindowUI"), Some(&Object::Boolean(true)));
671    }
672
673    #[test]
674    fn test_center_window_to_dict() {
675        let prefs = ViewerPreferences::new().center_window(true);
676        let dict = prefs.to_dict();
677        assert_eq!(dict.get("CenterWindow"), Some(&Object::Boolean(true)));
678    }
679
680    #[test]
681    fn test_display_doc_title_to_dict() {
682        let prefs = ViewerPreferences::new().display_doc_title(true);
683        let dict = prefs.to_dict();
684        assert_eq!(dict.get("DisplayDocTitle"), Some(&Object::Boolean(true)));
685    }
686
687    #[test]
688    fn test_page_mode_to_dict() {
689        let prefs = ViewerPreferences::new().page_mode(PageMode::UseAttachments);
690        let dict = prefs.to_dict();
691        assert_eq!(
692            dict.get("PageMode"),
693            Some(&Object::Name("UseAttachments".to_string()))
694        );
695    }
696
697    #[test]
698    fn test_print_page_range_reversed() {
699        // Test that reversed ranges are handled correctly (start > end)
700        let prefs = ViewerPreferences::new().add_print_page_range(10, 5);
701        let dict = prefs.to_dict();
702        if let Some(Object::Array(ranges)) = dict.get("PrintPageRange") {
703            assert_eq!(ranges[0], Object::Integer(5)); // min(10, 5) = 5
704            assert_eq!(ranges[1], Object::Integer(10)); // max(10, 5) = 10
705        } else {
706            panic!("Expected PrintPageRange array");
707        }
708    }
709
710    #[test]
711    fn test_all_page_layouts_to_dict() {
712        let layouts = [
713            (PageLayout::SinglePage, "SinglePage"),
714            (PageLayout::OneColumn, "OneColumn"),
715            (PageLayout::TwoColumnLeft, "TwoColumnLeft"),
716            (PageLayout::TwoColumnRight, "TwoColumnRight"),
717            (PageLayout::TwoPageLeft, "TwoPageLeft"),
718            (PageLayout::TwoPageRight, "TwoPageRight"),
719        ];
720
721        for (layout, expected_name) in layouts {
722            let prefs = ViewerPreferences::new().page_layout(layout);
723            let dict = prefs.to_dict();
724            assert_eq!(
725                dict.get("PageLayout"),
726                Some(&Object::Name(expected_name.to_string()))
727            );
728        }
729    }
730
731    #[test]
732    fn test_print_scaling_app_default_to_dict() {
733        let prefs = ViewerPreferences::new().print_scaling(PrintScaling::AppDefault);
734        let dict = prefs.to_dict();
735        assert_eq!(
736            dict.get("PrintScaling"),
737            Some(&Object::Name("AppDefault".to_string()))
738        );
739    }
740
741    #[test]
742    fn test_duplex_simplex_to_dict() {
743        let prefs = ViewerPreferences::new().duplex(Duplex::Simplex);
744        let dict = prefs.to_dict();
745        assert_eq!(
746            dict.get("Duplex"),
747            Some(&Object::Name("Simplex".to_string()))
748        );
749    }
750
751    #[test]
752    fn test_duplex_flip_short_edge_to_dict() {
753        let prefs = ViewerPreferences::new().duplex(Duplex::DuplexFlipShortEdge);
754        let dict = prefs.to_dict();
755        assert_eq!(
756            dict.get("Duplex"),
757            Some(&Object::Name("DuplexFlipShortEdge".to_string()))
758        );
759    }
760
761    #[test]
762    fn test_full_preferences_to_dict() {
763        let mut prefs = ViewerPreferences::new()
764            .hide_toolbar(true)
765            .hide_menubar(true)
766            .hide_window_ui(true)
767            .fit_window(true)
768            .center_window(true)
769            .display_doc_title(true)
770            .page_layout(PageLayout::TwoColumnRight)
771            .page_mode(PageMode::UseOutlines)
772            .non_full_screen_page_mode(NonFullScreenPageMode::UseThumbs)
773            .direction(Direction::R2L)
774            .print_scaling(PrintScaling::None)
775            .duplex(Duplex::DuplexFlipLongEdge)
776            .num_copies(2)
777            .pick_tray_by_pdf_size(true)
778            .add_print_page_range(1, 10);
779
780        prefs.view_area = Some("MediaBox".to_string());
781        prefs.view_clip = Some("CropBox".to_string());
782        prefs.print_area = Some("BleedBox".to_string());
783        prefs.print_clip = Some("TrimBox".to_string());
784
785        let dict = prefs.to_dict();
786
787        // Verify all fields are present
788        assert!(dict.contains_key("HideToolbar"));
789        assert!(dict.contains_key("HideMenubar"));
790        assert!(dict.contains_key("HideWindowUI"));
791        assert!(dict.contains_key("FitWindow"));
792        assert!(dict.contains_key("CenterWindow"));
793        assert!(dict.contains_key("DisplayDocTitle"));
794        assert!(dict.contains_key("PageLayout"));
795        assert!(dict.contains_key("PageMode"));
796        assert!(dict.contains_key("NonFullScreenPageMode"));
797        assert!(dict.contains_key("Direction"));
798        assert!(dict.contains_key("ViewArea"));
799        assert!(dict.contains_key("ViewClip"));
800        assert!(dict.contains_key("PrintArea"));
801        assert!(dict.contains_key("PrintClip"));
802        assert!(dict.contains_key("PrintScaling"));
803        assert!(dict.contains_key("Duplex"));
804        assert!(dict.contains_key("PrintPageRange"));
805        assert!(dict.contains_key("NumCopies"));
806        assert!(dict.contains_key("PickTrayByPDFSize"));
807    }
808}