1use crate::object::{Dict, Object};
6use crate::source::AsyncObjectSource;
7use crate::tree::resolved_dict;
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
233struct Entries<'a, S> {
236 src: &'a S,
237 dict: &'a Dict,
238}
239
240impl<S: AsyncObjectSource> Entries<'_, S> {
241 async fn value(&self, key: &str) -> Option<Object> {
242 self.src.resolve(self.dict.get(key)?).await.ok()
243 }
244
245 async fn flag(&self, key: &str) -> Option<bool> {
246 self.value(key).await?.as_bool()
247 }
248
249 async fn named<T>(&self, key: &str, from_name: fn(&str) -> Option<T>) -> Option<T> {
250 from_name(&self.value(key).await?.as_name()?.0)
251 }
252}
253
254fn page_ranges(value: Option<&Object>) -> Vec<(u32, u32)> {
258 let Some(items) = value.and_then(Object::as_array) else {
259 return Vec::new();
260 };
261 items
262 .as_chunks::<2>()
263 .0
264 .iter()
265 .filter_map(|[first, last]| {
266 let first = u32::try_from(first.as_int()?).ok()?;
267 let last = u32::try_from(last.as_int()?).ok()?;
268 (first > 0 && last > 0).then_some((first, last))
269 })
270 .collect()
271}
272
273#[cfg(test)]
274mod tests {
275 use super::{
276 Direction, Duplex, NonFullScreenPageMode, PageBoundary, PrintScaling, ViewerPreferences,
277 };
278 use crate::Document;
279 use pdfboss_testkit::PdfBuilder;
280
281 fn doc(catalog_extra: &str, objects: &[(u32, &str)]) -> Document {
282 let mut b = PdfBuilder::new();
283 b.object(
284 1,
285 &format!("<< /Type /Catalog /Pages 2 0 R {catalog_extra} >>"),
286 );
287 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
288 b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
289 for (num, body) in objects {
290 b.object(*num, body);
291 }
292 Document::load(b.build(1)).unwrap()
293 }
294
295 #[test]
299 fn every_viewer_preference_is_read() {
300 let found = doc(
301 "/ViewerPreferences 4 0 R",
302 &[
303 (
304 4,
305 "<< /HideToolbar true /HideMenubar true /HideWindowUI true /FitWindow true \
306 /CenterWindow true /DisplayDocTitle true /NonFullScreenPageMode /UseOutlines \
307 /Direction /R2L /ViewArea /MediaBox /ViewClip /BleedBox /PrintArea /TrimBox \
308 /PrintClip /ArtBox /PrintScaling /None /Duplex /DuplexFlipLongEdge \
309 /PickTrayByPDFSize 5 0 R /PrintPageRange [1 3 7 7] /NumCopies 6 0 R >>",
310 ),
311 (5, "false"),
312 (6, "2"),
313 ],
314 )
315 .viewer_preferences();
316 assert_eq!(
317 found,
318 Some(ViewerPreferences {
319 hide_toolbar: true,
320 hide_menubar: true,
321 hide_window_ui: true,
322 fit_window: true,
323 center_window: true,
324 display_doc_title: true,
325 non_full_screen_page_mode: NonFullScreenPageMode::UseOutlines,
326 direction: Direction::RightToLeft,
327 view_area: PageBoundary::MediaBox,
328 view_clip: PageBoundary::BleedBox,
329 print_area: PageBoundary::TrimBox,
330 print_clip: PageBoundary::ArtBox,
331 print_scaling: PrintScaling::None,
332 duplex: Some(Duplex::DuplexFlipLongEdge),
333 pick_tray_by_pdf_size: Some(false),
334 print_page_range: vec![(1, 3), (7, 7)],
335 num_copies: Some(2),
336 })
337 );
338 }
339
340 #[test]
346 fn missing_and_unknown_viewer_preferences_take_the_defaults() {
347 assert_eq!(
348 doc("/ViewerPreferences << >>", &[]).viewer_preferences(),
349 Some(ViewerPreferences::default())
350 );
351 assert_eq!(doc("", &[]).viewer_preferences(), None);
352 assert_eq!(doc("/ViewerPreferences 7", &[]).viewer_preferences(), None);
353 let odd = doc(
354 "/ViewerPreferences << /Direction /Down /PrintScaling /Bogus /Duplex /Both \
355 /ViewArea /Page /PrintPageRange [1 3 5] /NumCopies 0 /HideToolbar /yes >>",
356 &[],
357 )
358 .viewer_preferences()
359 .unwrap();
360 assert_eq!(
361 odd,
362 ViewerPreferences {
363 print_page_range: vec![(1, 3)],
364 ..ViewerPreferences::default()
365 }
366 );
367 }
368}