use crate::object::{Dict, Object};
use crate::source::AsyncObjectSource;
use crate::tree::{resolved_dict, Entries};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum NonFullScreenPageMode {
#[default]
UseNone,
UseOutlines,
UseThumbs,
UseOC,
}
impl NonFullScreenPageMode {
fn from_name(name: &str) -> Option<Self> {
match name {
"UseNone" => Some(Self::UseNone),
"UseOutlines" => Some(Self::UseOutlines),
"UseThumbs" => Some(Self::UseThumbs),
"UseOC" => Some(Self::UseOC),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Direction {
#[default]
LeftToRight,
RightToLeft,
}
impl Direction {
fn from_name(name: &str) -> Option<Self> {
match name {
"L2R" => Some(Self::LeftToRight),
"R2L" => Some(Self::RightToLeft),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PageBoundary {
MediaBox,
#[default]
CropBox,
BleedBox,
TrimBox,
ArtBox,
}
impl PageBoundary {
fn from_name(name: &str) -> Option<Self> {
match name {
"MediaBox" => Some(Self::MediaBox),
"CropBox" => Some(Self::CropBox),
"BleedBox" => Some(Self::BleedBox),
"TrimBox" => Some(Self::TrimBox),
"ArtBox" => Some(Self::ArtBox),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PrintScaling {
None,
#[default]
AppDefault,
}
impl PrintScaling {
fn from_name(name: &str) -> Option<Self> {
match name {
"None" => Some(Self::None),
"AppDefault" => Some(Self::AppDefault),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Duplex {
Simplex,
DuplexFlipShortEdge,
DuplexFlipLongEdge,
}
impl Duplex {
fn from_name(name: &str) -> Option<Self> {
match name {
"Simplex" => Some(Self::Simplex),
"DuplexFlipShortEdge" => Some(Self::DuplexFlipShortEdge),
"DuplexFlipLongEdge" => Some(Self::DuplexFlipLongEdge),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct ViewerPreferences {
pub hide_toolbar: bool,
pub hide_menubar: bool,
pub hide_window_ui: bool,
pub fit_window: bool,
pub center_window: bool,
pub display_doc_title: bool,
pub non_full_screen_page_mode: NonFullScreenPageMode,
pub direction: Direction,
pub view_area: PageBoundary,
pub view_clip: PageBoundary,
pub print_area: PageBoundary,
pub print_clip: PageBoundary,
pub print_scaling: PrintScaling,
pub duplex: Option<Duplex>,
pub pick_tray_by_pdf_size: Option<bool>,
pub print_page_range: Vec<(u32, u32)>,
pub num_copies: Option<u32>,
}
pub async fn viewer_preferences_with<S: AsyncObjectSource>(
src: &S,
trailer: &Dict,
) -> Option<ViewerPreferences> {
let catalog = resolved_dict(src, trailer.get("Root")?).await?;
let dict = resolved_dict(src, catalog.get("ViewerPreferences")?).await?;
let entries = Entries { src, dict: &dict };
Some(ViewerPreferences {
hide_toolbar: entries.flag("HideToolbar").await.unwrap_or(false),
hide_menubar: entries.flag("HideMenubar").await.unwrap_or(false),
hide_window_ui: entries.flag("HideWindowUI").await.unwrap_or(false),
fit_window: entries.flag("FitWindow").await.unwrap_or(false),
center_window: entries.flag("CenterWindow").await.unwrap_or(false),
display_doc_title: entries.flag("DisplayDocTitle").await.unwrap_or(false),
non_full_screen_page_mode: entries
.named("NonFullScreenPageMode", NonFullScreenPageMode::from_name)
.await
.unwrap_or_default(),
direction: entries
.named("Direction", Direction::from_name)
.await
.unwrap_or_default(),
view_area: entries
.named("ViewArea", PageBoundary::from_name)
.await
.unwrap_or_default(),
view_clip: entries
.named("ViewClip", PageBoundary::from_name)
.await
.unwrap_or_default(),
print_area: entries
.named("PrintArea", PageBoundary::from_name)
.await
.unwrap_or_default(),
print_clip: entries
.named("PrintClip", PageBoundary::from_name)
.await
.unwrap_or_default(),
print_scaling: entries
.named("PrintScaling", PrintScaling::from_name)
.await
.unwrap_or_default(),
duplex: entries.named("Duplex", Duplex::from_name).await,
pick_tray_by_pdf_size: entries.flag("PickTrayByPDFSize").await,
print_page_range: page_ranges(entries.value("PrintPageRange").await.as_ref()),
num_copies: entries
.value("NumCopies")
.await
.and_then(|value| value.as_int())
.and_then(|copies| u32::try_from(copies).ok())
.filter(|copies| *copies > 0),
})
}
fn page_ranges(value: Option<&Object>) -> Vec<(u32, u32)> {
let Some(items) = value.and_then(Object::as_array) else {
return Vec::new();
};
items
.as_chunks::<2>()
.0
.iter()
.filter_map(|[first, last]| {
let first = u32::try_from(first.as_int()?).ok()?;
let last = u32::try_from(last.as_int()?).ok()?;
(first > 0 && last > 0).then_some((first, last))
})
.collect()
}
#[cfg(test)]
mod tests {
use super::{
Direction, Duplex, NonFullScreenPageMode, PageBoundary, PrintScaling, ViewerPreferences,
};
use crate::Document;
use pdfboss_testkit::PdfBuilder;
fn doc(catalog_extra: &str, objects: &[(u32, &str)]) -> Document {
let mut b = PdfBuilder::new();
b.object(
1,
&format!("<< /Type /Catalog /Pages 2 0 R {catalog_extra} >>"),
);
b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
for (num, body) in objects {
b.object(*num, body);
}
Document::load(b.build(1)).unwrap()
}
#[test]
fn every_viewer_preference_is_read() {
let found = doc(
"/ViewerPreferences 4 0 R",
&[
(
4,
"<< /HideToolbar true /HideMenubar true /HideWindowUI true /FitWindow true \
/CenterWindow true /DisplayDocTitle true /NonFullScreenPageMode /UseOutlines \
/Direction /R2L /ViewArea /MediaBox /ViewClip /BleedBox /PrintArea /TrimBox \
/PrintClip /ArtBox /PrintScaling /None /Duplex /DuplexFlipLongEdge \
/PickTrayByPDFSize 5 0 R /PrintPageRange [1 3 7 7] /NumCopies 6 0 R >>",
),
(5, "false"),
(6, "2"),
],
)
.viewer_preferences();
assert_eq!(
found,
Some(ViewerPreferences {
hide_toolbar: true,
hide_menubar: true,
hide_window_ui: true,
fit_window: true,
center_window: true,
display_doc_title: true,
non_full_screen_page_mode: NonFullScreenPageMode::UseOutlines,
direction: Direction::RightToLeft,
view_area: PageBoundary::MediaBox,
view_clip: PageBoundary::BleedBox,
print_area: PageBoundary::TrimBox,
print_clip: PageBoundary::ArtBox,
print_scaling: PrintScaling::None,
duplex: Some(Duplex::DuplexFlipLongEdge),
pick_tray_by_pdf_size: Some(false),
print_page_range: vec![(1, 3), (7, 7)],
num_copies: Some(2),
})
);
}
#[test]
fn missing_and_unknown_viewer_preferences_take_the_defaults() {
assert_eq!(
doc("/ViewerPreferences << >>", &[]).viewer_preferences(),
Some(ViewerPreferences::default())
);
assert_eq!(doc("", &[]).viewer_preferences(), None);
assert_eq!(doc("/ViewerPreferences 7", &[]).viewer_preferences(), None);
let odd = doc(
"/ViewerPreferences << /Direction /Down /PrintScaling /Bogus /Duplex /Both \
/ViewArea /Page /PrintPageRange [1 3 5] /NumCopies 0 /HideToolbar /yes >>",
&[],
)
.viewer_preferences()
.unwrap();
assert_eq!(
odd,
ViewerPreferences {
print_page_range: vec![(1, 3)],
..ViewerPreferences::default()
}
);
}
}