use crate::objects::{PdfDict, PdfObj};
use crate::resolver::Resolver;
#[derive(Debug, Clone, PartialEq)]
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: PageMode,
pub direction: ReadingDirection,
pub page_layout: PageLayout,
pub page_mode: PageMode,
pub print_scaling: PrintScaling,
pub duplex: Option<Duplex>,
pub pick_tray_by_pdf_size: Option<bool>,
pub num_copies: Option<u32>,
}
impl Default for ViewerPreferences {
fn default() -> Self {
Self {
hide_toolbar: false,
hide_menubar: false,
hide_window_ui: false,
fit_window: false,
center_window: false,
display_doc_title: false,
non_full_screen_page_mode: PageMode::UseNone,
direction: ReadingDirection::L2R,
page_layout: PageLayout::SinglePage,
page_mode: PageMode::UseNone,
print_scaling: PrintScaling::AppDefault,
duplex: None,
pick_tray_by_pdf_size: None,
num_copies: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PageLayout {
SinglePage,
OneColumn,
TwoColumnLeft,
TwoColumnRight,
TwoPageLeft,
TwoPageRight,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PageMode {
UseNone,
UseOutlines,
UseThumbs,
FullScreen,
UseOC,
UseAttachments,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ReadingDirection {
L2R,
R2L,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PrintScaling {
None,
AppDefault,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Duplex {
Simplex,
DuplexFlipShortEdge,
DuplexFlipLongEdge,
}
pub fn parse_viewer_preferences(resolver: &Resolver) -> ViewerPreferences {
let mut prefs = ViewerPreferences::default();
let Some(catalog) = catalog_dict(resolver) else {
return prefs;
};
if let Some(name) = catalog.get_name(b"PageLayout")
&& let Some(layout) = parse_page_layout(name)
{
prefs.page_layout = layout;
}
if let Some(name) = catalog.get_name(b"PageMode")
&& let Some(mode) = parse_page_mode(name)
{
prefs.page_mode = mode;
}
if let Some(vp_obj) = catalog.get(b"ViewerPreferences")
&& let Ok(vp) = resolver.deref(vp_obj)
&& let Some(vp_dict) = vp.as_dict()
{
fill_viewer_prefs(&mut prefs, vp_dict);
}
prefs
}
fn fill_viewer_prefs(prefs: &mut ViewerPreferences, dict: &PdfDict) {
if let Some(b) = dict.get(b"HideToolbar").and_then(as_bool) {
prefs.hide_toolbar = b;
}
if let Some(b) = dict.get(b"HideMenubar").and_then(as_bool) {
prefs.hide_menubar = b;
}
if let Some(b) = dict.get(b"HideWindowUI").and_then(as_bool) {
prefs.hide_window_ui = b;
}
if let Some(b) = dict.get(b"FitWindow").and_then(as_bool) {
prefs.fit_window = b;
}
if let Some(b) = dict.get(b"CenterWindow").and_then(as_bool) {
prefs.center_window = b;
}
if let Some(b) = dict.get(b"DisplayDocTitle").and_then(as_bool) {
prefs.display_doc_title = b;
}
if let Some(name) = dict.get_name(b"NonFullScreenPageMode")
&& let Some(m) = parse_page_mode(name)
{
prefs.non_full_screen_page_mode = m;
}
if let Some(name) = dict.get_name(b"Direction")
&& let Some(d) = parse_direction(name)
{
prefs.direction = d;
}
if let Some(name) = dict.get_name(b"PrintScaling")
&& let Some(p) = parse_print_scaling(name)
{
prefs.print_scaling = p;
}
if let Some(name) = dict.get_name(b"Duplex") {
prefs.duplex = parse_duplex(name);
}
if let Some(b) = dict.get(b"PickTrayByPDFSize").and_then(as_bool) {
prefs.pick_tray_by_pdf_size = Some(b);
}
if let Some(n) = dict.get_int(b"NumCopies")
&& (1..=10_000).contains(&n)
{
prefs.num_copies = Some(n as u32);
}
}
fn as_bool(obj: &PdfObj) -> Option<bool> {
match obj {
PdfObj::Bool(b) => Some(*b),
_ => None,
}
}
fn parse_page_layout(name: &[u8]) -> Option<PageLayout> {
match name {
b"SinglePage" => Some(PageLayout::SinglePage),
b"OneColumn" => Some(PageLayout::OneColumn),
b"TwoColumnLeft" => Some(PageLayout::TwoColumnLeft),
b"TwoColumnRight" => Some(PageLayout::TwoColumnRight),
b"TwoPageLeft" => Some(PageLayout::TwoPageLeft),
b"TwoPageRight" => Some(PageLayout::TwoPageRight),
_ => None,
}
}
fn parse_page_mode(name: &[u8]) -> Option<PageMode> {
match name {
b"UseNone" => Some(PageMode::UseNone),
b"UseOutlines" => Some(PageMode::UseOutlines),
b"UseThumbs" => Some(PageMode::UseThumbs),
b"FullScreen" => Some(PageMode::FullScreen),
b"UseOC" => Some(PageMode::UseOC),
b"UseAttachments" => Some(PageMode::UseAttachments),
_ => None,
}
}
fn parse_direction(name: &[u8]) -> Option<ReadingDirection> {
match name {
b"L2R" => Some(ReadingDirection::L2R),
b"R2L" => Some(ReadingDirection::R2L),
_ => None,
}
}
fn parse_print_scaling(name: &[u8]) -> Option<PrintScaling> {
match name {
b"None" => Some(PrintScaling::None),
b"AppDefault" => Some(PrintScaling::AppDefault),
_ => None,
}
}
fn parse_duplex(name: &[u8]) -> Option<Duplex> {
match name {
b"Simplex" => Some(Duplex::Simplex),
b"DuplexFlipShortEdge" => Some(Duplex::DuplexFlipShortEdge),
b"DuplexFlipLongEdge" => Some(Duplex::DuplexFlipLongEdge),
_ => None,
}
}
fn catalog_dict(resolver: &Resolver) -> Option<PdfDict> {
if let Some((num, gen_num)) = resolver.trailer().get_ref(b"Root")
&& let Ok(obj) = resolver.resolve(num, gen_num)
&& let Some(dict) = obj.as_dict()
&& (dict.get_name(b"Type") == Some(b"Catalog")
|| dict.get(b"Pages").is_some()
|| dict.get(b"PageLayout").is_some()
|| dict.get(b"PageMode").is_some()
|| dict.get(b"ViewerPreferences").is_some())
{
return Some(dict.clone());
}
crate::find_catalog(resolver).and_then(|obj| obj.as_dict().cloned())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_are_sane() {
let p = ViewerPreferences::default();
assert!(!p.hide_toolbar);
assert_eq!(p.page_layout, PageLayout::SinglePage);
assert_eq!(p.page_mode, PageMode::UseNone);
assert_eq!(p.print_scaling, PrintScaling::AppDefault);
assert!(p.duplex.is_none());
}
#[test]
fn fill_from_dict_sets_overrides() {
let mut dict = PdfDict::new();
dict.insert(b"HideToolbar".to_vec(), PdfObj::Bool(true));
dict.insert(b"FitWindow".to_vec(), PdfObj::Bool(true));
dict.insert(b"PrintScaling".to_vec(), PdfObj::Name(b"None".to_vec()));
dict.insert(
b"Duplex".to_vec(),
PdfObj::Name(b"DuplexFlipLongEdge".to_vec()),
);
dict.insert(b"NumCopies".to_vec(), PdfObj::Int(3));
dict.insert(
b"NonFullScreenPageMode".to_vec(),
PdfObj::Name(b"UseOutlines".to_vec()),
);
let mut prefs = ViewerPreferences::default();
fill_viewer_prefs(&mut prefs, &dict);
assert!(prefs.hide_toolbar);
assert!(prefs.fit_window);
assert_eq!(prefs.print_scaling, PrintScaling::None);
assert_eq!(prefs.duplex, Some(Duplex::DuplexFlipLongEdge));
assert_eq!(prefs.num_copies, Some(3));
assert_eq!(prefs.non_full_screen_page_mode, PageMode::UseOutlines);
}
#[test]
fn unknown_name_leaves_default() {
let mut dict = PdfDict::new();
dict.insert(b"PrintScaling".to_vec(), PdfObj::Name(b"Bogus".to_vec()));
let mut prefs = ViewerPreferences::default();
fill_viewer_prefs(&mut prefs, &dict);
assert_eq!(prefs.print_scaling, PrintScaling::AppDefault);
}
#[test]
fn num_copies_out_of_range_ignored() {
let mut dict = PdfDict::new();
dict.insert(b"NumCopies".to_vec(), PdfObj::Int(0));
let mut prefs = ViewerPreferences::default();
fill_viewer_prefs(&mut prefs, &dict);
assert!(prefs.num_copies.is_none());
let mut dict = PdfDict::new();
dict.insert(b"NumCopies".to_vec(), PdfObj::Int(99_999));
let mut prefs = ViewerPreferences::default();
fill_viewer_prefs(&mut prefs, &dict);
assert!(prefs.num_copies.is_none());
}
#[test]
fn page_layout_round_trip() {
for (name, expect) in [
(&b"SinglePage"[..], PageLayout::SinglePage),
(&b"OneColumn"[..], PageLayout::OneColumn),
(&b"TwoColumnLeft"[..], PageLayout::TwoColumnLeft),
(&b"TwoPageRight"[..], PageLayout::TwoPageRight),
] {
assert_eq!(parse_page_layout(name), Some(expect));
}
assert!(parse_page_layout(b"unknown").is_none());
}
}