use std::collections::HashMap;
use rpdfium_core::{Name, PdfSource};
use rpdfium_parser::object::Object;
use rpdfium_parser::store::ObjectStore;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReadingDirection {
L2R,
R2L,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DuplexMode {
#[default]
Simplex,
DuplexFlipShortEdge,
DuplexFlipLongEdge,
}
#[derive(Debug, Clone)]
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 direction: ReadingDirection,
pub print_scaling: Option<String>,
pub num_copies: Option<u32>,
pub duplex: Option<String>,
pub print_page_range: Option<Vec<i64>>,
}
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,
direction: ReadingDirection::L2R,
print_scaling: None,
num_copies: None,
duplex: None,
print_page_range: None,
}
}
}
impl ViewerPreferences {
pub fn from_dict<S: PdfSource>(dict: &HashMap<Name, Object>, store: &ObjectStore<S>) -> Self {
let get_bool = |name: &Name| -> bool {
dict.get(name)
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_bool())
.unwrap_or(false)
};
let get_name_string = |name: &Name| -> Option<String> {
dict.get(name)
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_name().map(|n| n.as_str().into_owned()))
};
let get_string = |name: &Name| -> Option<String> {
dict.get(name)
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| {
if let Some(s) = o.as_string() {
Some(s.to_string_lossy())
} else {
o.as_name().map(|n| n.as_str().into_owned())
}
})
};
let direction = match get_name_string(&Name::direction()).as_deref() {
Some("R2L") => ReadingDirection::R2L,
_ => ReadingDirection::L2R,
};
let num_copies = dict
.get(&Name::num_copies())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_i64())
.map(|n| n.max(1) as u32);
let print_page_range = dict
.get(&Name::print_page_range())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| {
o.as_array().map(|arr| {
arr.iter()
.filter_map(|item| item.as_i64())
.collect::<Vec<i64>>()
})
})
.filter(|v| !v.is_empty());
Self {
hide_toolbar: get_bool(&Name::hide_toolbar()),
hide_menubar: get_bool(&Name::hide_menubar()),
hide_window_ui: get_bool(&Name::hide_window_ui()),
fit_window: get_bool(&Name::fit_window()),
center_window: get_bool(&Name::center_window()),
display_doc_title: get_bool(&Name::display_doc_title()),
direction,
print_scaling: get_string(&Name::print_scaling()),
num_copies,
duplex: get_string(&Name::duplex()),
print_page_range,
}
}
pub fn is_direction_r2l(&self) -> bool {
self.direction == ReadingDirection::R2L
}
pub fn print_scaling(&self) -> bool {
self.print_scaling.as_deref() != Some("None")
}
pub fn is_print_scaling_suppressed(&self) -> bool {
!self.print_scaling()
}
pub fn num_copies(&self) -> Option<u32> {
self.num_copies
}
#[deprecated(since = "0.1.0", note = "use num_copies() instead")]
#[inline]
pub fn get_num_copies(&self) -> Option<u32> {
self.num_copies()
}
pub fn print_page_range(&self) -> Option<&[i64]> {
self.print_page_range.as_deref()
}
pub fn duplex_mode(&self) -> DuplexMode {
match self.duplex.as_deref() {
Some("DuplexFlipShortEdge") => DuplexMode::DuplexFlipShortEdge,
Some("DuplexFlipLongEdge") => DuplexMode::DuplexFlipLongEdge,
_ => DuplexMode::Simplex,
}
}
#[inline]
pub fn duplex(&self) -> DuplexMode {
self.duplex_mode()
}
pub fn generic_name(&self, key: &str) -> Option<&str> {
match key {
"HideToolbar" => self.hide_toolbar.then_some("true"),
"HideMenubar" => self.hide_menubar.then_some("true"),
"HideWindowUI" => self.hide_window_ui.then_some("true"),
"FitWindow" => self.fit_window.then_some("true"),
"CenterWindow" => self.center_window.then_some("true"),
"DisplayDocTitle" => self.display_doc_title.then_some("true"),
"Direction" => match self.direction {
ReadingDirection::R2L => Some("R2L"),
ReadingDirection::L2R => Some("L2R"),
},
"PrintScaling" => self.print_scaling.as_deref(),
"Duplex" => self.duplex.as_deref(),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn build_store() -> ObjectStore<Vec<u8>> {
let pdf = build_minimal_pdf();
ObjectStore::open(pdf, rpdfium_core::ParsingMode::Lenient).unwrap()
}
fn build_minimal_pdf() -> Vec<u8> {
let mut pdf = Vec::new();
pdf.extend_from_slice(b"%PDF-1.4\n");
let obj1_offset = pdf.len();
pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
let obj2_offset = pdf.len();
pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
let xref_offset = pdf.len();
pdf.extend_from_slice(b"xref\n0 3\n");
pdf.extend_from_slice(b"0000000000 65535 f \r\n");
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj1_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj2_offset).as_bytes());
pdf.extend_from_slice(b"trailer\n<< /Size 3 /Root 1 0 R >>\n");
pdf.extend_from_slice(format!("startxref\n{}\n%%EOF", xref_offset).as_bytes());
pdf
}
#[test]
fn test_default_preferences() {
let prefs = ViewerPreferences::default();
assert!(!prefs.hide_toolbar);
assert!(!prefs.hide_menubar);
assert!(!prefs.hide_window_ui);
assert!(!prefs.fit_window);
assert!(!prefs.center_window);
assert!(!prefs.display_doc_title);
assert_eq!(prefs.direction, ReadingDirection::L2R);
assert!(prefs.print_scaling.is_none());
assert!(prefs.num_copies.is_none());
assert!(prefs.duplex.is_none());
assert!(prefs.print_page_range.is_none());
}
#[test]
fn test_parse_all_fields() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::hide_toolbar(), Object::Boolean(true));
dict.insert(Name::hide_menubar(), Object::Boolean(true));
dict.insert(Name::hide_window_ui(), Object::Boolean(true));
dict.insert(Name::fit_window(), Object::Boolean(true));
dict.insert(Name::center_window(), Object::Boolean(true));
dict.insert(Name::display_doc_title(), Object::Boolean(true));
dict.insert(Name::direction(), Object::Name(Name::from("R2L")));
dict.insert(Name::print_scaling(), Object::Name(Name::from("None")));
dict.insert(Name::num_copies(), Object::Integer(3));
dict.insert(
Name::duplex(),
Object::Name(Name::from("DuplexFlipLongEdge")),
);
let prefs = ViewerPreferences::from_dict(&dict, &store);
assert!(prefs.hide_toolbar);
assert!(prefs.hide_menubar);
assert!(prefs.hide_window_ui);
assert!(prefs.fit_window);
assert!(prefs.center_window);
assert!(prefs.display_doc_title);
assert_eq!(prefs.direction, ReadingDirection::R2L);
assert_eq!(prefs.print_scaling.as_deref(), Some("None"));
assert_eq!(prefs.num_copies, Some(3));
assert_eq!(prefs.duplex.as_deref(), Some("DuplexFlipLongEdge"));
}
#[test]
fn test_parse_empty_dict() {
let store = build_store();
let dict = HashMap::new();
let prefs = ViewerPreferences::from_dict(&dict, &store);
assert!(!prefs.hide_toolbar);
assert_eq!(prefs.direction, ReadingDirection::L2R);
assert!(prefs.print_scaling.is_none());
}
#[test]
fn test_print_page_range_parsed() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(
Name::print_page_range(),
Object::Array(vec![
Object::Integer(0),
Object::Integer(3),
Object::Integer(5),
Object::Integer(7),
]),
);
let prefs = ViewerPreferences::from_dict(&dict, &store);
let range = prefs.print_page_range.unwrap();
assert_eq!(range, vec![0, 3, 5, 7]);
}
#[test]
fn test_print_page_range_empty_is_none() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::print_page_range(), Object::Array(vec![]));
let prefs = ViewerPreferences::from_dict(&dict, &store);
assert!(prefs.print_page_range.is_none());
}
#[test]
fn test_direction_default_l2r() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::direction(), Object::Name(Name::from("L2R")));
let prefs = ViewerPreferences::from_dict(&dict, &store);
assert_eq!(prefs.direction, ReadingDirection::L2R);
}
#[test]
fn test_direction_r2l() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::direction(), Object::Name(Name::from("R2L")));
let prefs = ViewerPreferences::from_dict(&dict, &store);
assert_eq!(prefs.direction, ReadingDirection::R2L);
}
#[test]
fn test_is_print_scaling_suppressed_none_value() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::print_scaling(), Object::Name(Name::from("None")));
let prefs = ViewerPreferences::from_dict(&dict, &store);
assert!(prefs.is_print_scaling_suppressed());
}
#[test]
fn test_is_print_scaling_suppressed_app_default() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(
Name::print_scaling(),
Object::Name(Name::from("AppDefault")),
);
let prefs = ViewerPreferences::from_dict(&dict, &store);
assert!(!prefs.is_print_scaling_suppressed());
}
#[test]
fn test_is_print_scaling_suppressed_absent() {
let store = build_store();
let prefs = ViewerPreferences::from_dict(&HashMap::new(), &store);
assert!(!prefs.is_print_scaling_suppressed());
}
#[test]
fn test_generic_name_direction() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::direction(), Object::Name(Name::from("R2L")));
let prefs = ViewerPreferences::from_dict(&dict, &store);
assert_eq!(prefs.generic_name("Direction"), Some("R2L"));
}
#[test]
fn test_generic_name_print_scaling() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::print_scaling(), Object::Name(Name::from("None")));
let prefs = ViewerPreferences::from_dict(&dict, &store);
assert_eq!(prefs.generic_name("PrintScaling"), Some("None"));
}
#[test]
fn test_generic_name_unknown_key_is_none() {
let prefs = ViewerPreferences::default();
assert_eq!(prefs.generic_name("SomeUnknownKey"), None);
}
#[test]
fn test_generic_name_duplex() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(
Name::duplex(),
Object::Name(Name::from("DuplexFlipShortEdge")),
);
let prefs = ViewerPreferences::from_dict(&dict, &store);
assert_eq!(prefs.generic_name("Duplex"), Some("DuplexFlipShortEdge"));
}
#[test]
fn test_duplex_mode_simplex_by_default() {
let prefs = ViewerPreferences::default();
assert_eq!(prefs.duplex_mode(), DuplexMode::Simplex);
}
#[test]
fn test_duplex_mode_flip_short_edge() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(
Name::duplex(),
Object::Name(Name::from("DuplexFlipShortEdge")),
);
let prefs = ViewerPreferences::from_dict(&dict, &store);
assert_eq!(prefs.duplex_mode(), DuplexMode::DuplexFlipShortEdge);
}
#[test]
fn test_duplex_mode_flip_long_edge() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(
Name::duplex(),
Object::Name(Name::from("DuplexFlipLongEdge")),
);
let prefs = ViewerPreferences::from_dict(&dict, &store);
assert_eq!(prefs.duplex_mode(), DuplexMode::DuplexFlipLongEdge);
}
#[test]
fn test_duplex_mode_unknown_is_simplex() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::duplex(), Object::Name(Name::from("Unknown")));
let prefs = ViewerPreferences::from_dict(&dict, &store);
assert_eq!(prefs.duplex_mode(), DuplexMode::Simplex);
}
}