1use crate::object::{Dict, Object};
6use crate::source::AsyncObjectSource;
7use crate::tree::{resolved_dict, Entries};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum NonFullScreenPageMode {
14 #[default]
16 UseNone,
17 UseOutlines,
19 UseThumbs,
21 UseOC,
23}
24
25impl NonFullScreenPageMode {
26 fn from_name(name: &str) -> Option<Self> {
27 match name {
28 "UseNone" => Some(Self::UseNone),
29 "UseOutlines" => Some(Self::UseOutlines),
30 "UseThumbs" => Some(Self::UseThumbs),
31 "UseOC" => Some(Self::UseOC),
32 _ => None,
33 }
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
40pub enum Direction {
41 #[default]
43 LeftToRight,
44 RightToLeft,
46}
47
48impl Direction {
49 fn from_name(name: &str) -> Option<Self> {
50 match name {
51 "L2R" => Some(Self::LeftToRight),
52 "R2L" => Some(Self::RightToLeft),
53 _ => None,
54 }
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
61pub enum PageBoundary {
62 MediaBox,
63 #[default]
64 CropBox,
65 BleedBox,
66 TrimBox,
67 ArtBox,
68}
69
70impl PageBoundary {
71 fn from_name(name: &str) -> Option<Self> {
72 match name {
73 "MediaBox" => Some(Self::MediaBox),
74 "CropBox" => Some(Self::CropBox),
75 "BleedBox" => Some(Self::BleedBox),
76 "TrimBox" => Some(Self::TrimBox),
77 "ArtBox" => Some(Self::ArtBox),
78 _ => None,
79 }
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
86pub enum PrintScaling {
87 None,
89 #[default]
91 AppDefault,
92}
93
94impl PrintScaling {
95 fn from_name(name: &str) -> Option<Self> {
96 match name {
97 "None" => Some(Self::None),
98 "AppDefault" => Some(Self::AppDefault),
99 _ => None,
100 }
101 }
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub enum Duplex {
107 Simplex,
109 DuplexFlipShortEdge,
111 DuplexFlipLongEdge,
113}
114
115impl Duplex {
116 fn from_name(name: &str) -> Option<Self> {
117 match name {
118 "Simplex" => Some(Self::Simplex),
119 "DuplexFlipShortEdge" => Some(Self::DuplexFlipShortEdge),
120 "DuplexFlipLongEdge" => Some(Self::DuplexFlipLongEdge),
121 _ => None,
122 }
123 }
124}
125
126#[derive(Debug, Clone, PartialEq, Default)]
130pub struct ViewerPreferences {
131 pub hide_toolbar: bool,
133 pub hide_menubar: bool,
135 pub hide_window_ui: bool,
137 pub fit_window: bool,
139 pub center_window: bool,
141 pub display_doc_title: bool,
144 pub non_full_screen_page_mode: NonFullScreenPageMode,
146 pub direction: Direction,
148 pub view_area: PageBoundary,
150 pub view_clip: PageBoundary,
152 pub print_area: PageBoundary,
154 pub print_clip: PageBoundary,
156 pub print_scaling: PrintScaling,
159 pub duplex: Option<Duplex>,
162 pub pick_tray_by_pdf_size: Option<bool>,
164 pub print_page_range: Vec<(u32, u32)>,
168 pub num_copies: Option<u32>,
171}
172
173pub async fn viewer_preferences_with<S: AsyncObjectSource>(
180 src: &S,
181 trailer: &Dict,
182) -> Option<ViewerPreferences> {
183 let catalog = resolved_dict(src, trailer.get("Root")?).await?;
184 let dict = resolved_dict(src, catalog.get("ViewerPreferences")?).await?;
185 let entries = Entries { src, dict: &dict };
186 Some(ViewerPreferences {
187 hide_toolbar: entries.flag("HideToolbar").await.unwrap_or(false),
188 hide_menubar: entries.flag("HideMenubar").await.unwrap_or(false),
189 hide_window_ui: entries.flag("HideWindowUI").await.unwrap_or(false),
190 fit_window: entries.flag("FitWindow").await.unwrap_or(false),
191 center_window: entries.flag("CenterWindow").await.unwrap_or(false),
192 display_doc_title: entries.flag("DisplayDocTitle").await.unwrap_or(false),
193 non_full_screen_page_mode: entries
194 .named("NonFullScreenPageMode", NonFullScreenPageMode::from_name)
195 .await
196 .unwrap_or_default(),
197 direction: entries
198 .named("Direction", Direction::from_name)
199 .await
200 .unwrap_or_default(),
201 view_area: entries
202 .named("ViewArea", PageBoundary::from_name)
203 .await
204 .unwrap_or_default(),
205 view_clip: entries
206 .named("ViewClip", PageBoundary::from_name)
207 .await
208 .unwrap_or_default(),
209 print_area: entries
210 .named("PrintArea", PageBoundary::from_name)
211 .await
212 .unwrap_or_default(),
213 print_clip: entries
214 .named("PrintClip", PageBoundary::from_name)
215 .await
216 .unwrap_or_default(),
217 print_scaling: entries
218 .named("PrintScaling", PrintScaling::from_name)
219 .await
220 .unwrap_or_default(),
221 duplex: entries.named("Duplex", Duplex::from_name).await,
222 pick_tray_by_pdf_size: entries.flag("PickTrayByPDFSize").await,
223 print_page_range: page_ranges(entries.value("PrintPageRange").await.as_ref()),
224 num_copies: entries
225 .value("NumCopies")
226 .await
227 .and_then(|value| value.as_int())
228 .and_then(|copies| u32::try_from(copies).ok())
229 .filter(|copies| *copies > 0),
230 })
231}
232
233fn page_ranges(value: Option<&Object>) -> Vec<(u32, u32)> {
237 let Some(items) = value.and_then(Object::as_array) else {
238 return Vec::new();
239 };
240 items
241 .as_chunks::<2>()
242 .0
243 .iter()
244 .filter_map(|[first, last]| {
245 let first = u32::try_from(first.as_int()?).ok()?;
246 let last = u32::try_from(last.as_int()?).ok()?;
247 (first > 0 && last > 0).then_some((first, last))
248 })
249 .collect()
250}
251
252#[cfg(test)]
253mod tests {
254 use super::{
255 Direction, Duplex, NonFullScreenPageMode, PageBoundary, PrintScaling, ViewerPreferences,
256 };
257 use crate::Document;
258 use pdfboss_testkit::PdfBuilder;
259
260 fn doc(catalog_extra: &str, objects: &[(u32, &str)]) -> Document {
261 let mut b = PdfBuilder::new();
262 b.object(
263 1,
264 &format!("<< /Type /Catalog /Pages 2 0 R {catalog_extra} >>"),
265 );
266 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
267 b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
268 for (num, body) in objects {
269 b.object(*num, body);
270 }
271 Document::load(b.build(1)).unwrap()
272 }
273
274 #[test]
278 fn every_viewer_preference_is_read() {
279 let found = doc(
280 "/ViewerPreferences 4 0 R",
281 &[
282 (
283 4,
284 "<< /HideToolbar true /HideMenubar true /HideWindowUI true /FitWindow true \
285 /CenterWindow true /DisplayDocTitle true /NonFullScreenPageMode /UseOutlines \
286 /Direction /R2L /ViewArea /MediaBox /ViewClip /BleedBox /PrintArea /TrimBox \
287 /PrintClip /ArtBox /PrintScaling /None /Duplex /DuplexFlipLongEdge \
288 /PickTrayByPDFSize 5 0 R /PrintPageRange [1 3 7 7] /NumCopies 6 0 R >>",
289 ),
290 (5, "false"),
291 (6, "2"),
292 ],
293 )
294 .viewer_preferences();
295 assert_eq!(
296 found,
297 Some(ViewerPreferences {
298 hide_toolbar: true,
299 hide_menubar: true,
300 hide_window_ui: true,
301 fit_window: true,
302 center_window: true,
303 display_doc_title: true,
304 non_full_screen_page_mode: NonFullScreenPageMode::UseOutlines,
305 direction: Direction::RightToLeft,
306 view_area: PageBoundary::MediaBox,
307 view_clip: PageBoundary::BleedBox,
308 print_area: PageBoundary::TrimBox,
309 print_clip: PageBoundary::ArtBox,
310 print_scaling: PrintScaling::None,
311 duplex: Some(Duplex::DuplexFlipLongEdge),
312 pick_tray_by_pdf_size: Some(false),
313 print_page_range: vec![(1, 3), (7, 7)],
314 num_copies: Some(2),
315 })
316 );
317 }
318
319 #[test]
325 fn missing_and_unknown_viewer_preferences_take_the_defaults() {
326 assert_eq!(
327 doc("/ViewerPreferences << >>", &[]).viewer_preferences(),
328 Some(ViewerPreferences::default())
329 );
330 assert_eq!(doc("", &[]).viewer_preferences(), None);
331 assert_eq!(doc("/ViewerPreferences 7", &[]).viewer_preferences(), None);
332 let odd = doc(
333 "/ViewerPreferences << /Direction /Down /PrintScaling /Bogus /Duplex /Both \
334 /ViewArea /Page /PrintPageRange [1 3 5] /NumCopies 0 /HideToolbar /yes >>",
335 &[],
336 )
337 .viewer_preferences()
338 .unwrap();
339 assert_eq!(
340 odd,
341 ViewerPreferences {
342 print_page_range: vec![(1, 3)],
343 ..ViewerPreferences::default()
344 }
345 );
346 }
347}