use crate::error::{Error, Result};
use crate::json::Object;
use std::ops::RangeInclusive;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[non_exhaustive]
pub enum PageSize {
A3,
#[default]
A4,
A5,
Letter,
Legal,
Millimeters {
width: f32,
height: f32,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Margin {
pub top: f32,
pub right: f32,
pub bottom: f32,
pub left: f32,
}
impl Margin {
pub fn all(points: f32) -> Self {
Self {
top: points,
right: points,
bottom: points,
left: points,
}
}
pub fn new(top: f32, right: f32, bottom: f32, left: f32) -> Self {
Self {
top,
right,
bottom,
left,
}
}
}
impl Default for Margin {
fn default() -> Self {
Self::all(36.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Conformance {
PdfA2b,
PdfA2u,
PdfA3b,
PdfA3u,
PdfA2a,
PdfA3a,
PdfA4,
PdfA4f,
}
impl Conformance {
fn name(self) -> &'static str {
match self {
Self::PdfA2b => "PDF/A-2b",
Self::PdfA2u => "PDF/A-2u",
Self::PdfA3b => "PDF/A-3b",
Self::PdfA3u => "PDF/A-3u",
Self::PdfA2a => "PDF/A-2a",
Self::PdfA3a => "PDF/A-3a",
Self::PdfA4 => "PDF/A-4",
Self::PdfA4f => "PDF/A-4f",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Accessibility {
Tagged,
PdfUa1,
PdfUa2,
}
impl Accessibility {
fn name(self) -> &'static str {
match self {
Self::Tagged => "tagged",
Self::PdfUa1 => "PDF/UA-1",
Self::PdfUa2 => "PDF/UA-2",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum ImageFormat {
#[default]
Png,
Jpeg,
WebP,
}
impl ImageFormat {
fn name(self) -> &'static str {
match self {
Self::Png => "png",
Self::Jpeg => "jpeg",
Self::WebP => "webp",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum TextOrder {
#[default]
Reading,
TopToBottom,
}
impl TextOrder {
fn name(self) -> &'static str {
match self {
Self::Reading => "reading",
Self::TopToBottom => "top_to_bottom",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum BarcodeFormat {
QrCode,
DataMatrix,
Code128,
Ean13,
}
impl BarcodeFormat {
pub(crate) fn name(self) -> &'static str {
match self {
Self::QrCode => "qr_code",
Self::DataMatrix => "data_matrix",
Self::Code128 => "code128",
Self::Ean13 => "ean13",
}
}
pub(crate) fn from_name(name: &str) -> Option<Self> {
match name {
"qr_code" => Some(Self::QrCode),
"data_matrix" => Some(Self::DataMatrix),
"code128" => Some(Self::Code128),
"ean13" => Some(Self::Ean13),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Pages(String);
impl From<u32> for Pages {
fn from(page: u32) -> Self {
Self(page.to_string())
}
}
impl From<&str> for Pages {
fn from(range: &str) -> Self {
Self(range.to_string())
}
}
impl From<String> for Pages {
fn from(range: String) -> Self {
Self(range)
}
}
impl From<&[u32]> for Pages {
fn from(pages: &[u32]) -> Self {
Self(
pages
.iter()
.map(u32::to_string)
.collect::<Vec<_>>()
.join(","),
)
}
}
impl<const N: usize> From<[u32; N]> for Pages {
fn from(pages: [u32; N]) -> Self {
Self::from(&pages[..])
}
}
impl From<Vec<u32>> for Pages {
fn from(pages: Vec<u32>) -> Self {
Self::from(&pages[..])
}
}
impl From<RangeInclusive<u32>> for Pages {
fn from(range: RangeInclusive<u32>) -> Self {
Self(format!("{}-{}", range.start(), range.end()))
}
}
fn absolute(path: &Path, what: &str) -> Result<String> {
let absolute = std::path::absolute(path)
.map_err(|e| Error::argument(format!("{what}: {}: {e}", path.display())))?;
absolute
.into_os_string()
.into_string()
.map_err(|p| Error::argument(format!("{what} is not valid Unicode: {p:?}")))
}
fn unix_seconds(time: SystemTime) -> i64 {
match time.duration_since(UNIX_EPOCH) {
Ok(d) => d.as_secs() as i64,
Err(e) => -(e.duration().as_secs_f64().ceil() as i64),
}
}
fn timeout(o: &mut Object, timeout: Option<Duration>) -> Result<()> {
if let Some(t) = timeout {
if t.is_zero() || t.as_secs_f64() > 2_147_483.0 {
return Err(Error::argument(format!(
"timeout must be above 0 and at most 2,147,483 seconds, not {t:?}"
)));
}
o.num("timeout", t.as_secs_f64())?;
}
Ok(())
}
#[derive(Debug, Clone, Default)]
pub struct PdfOptions {
base_dir: Option<PathBuf>,
allow_file_access: Option<bool>,
page_size: Option<PageSize>,
landscape: Option<bool>,
margin: Option<Margin>,
scale: Option<f32>,
shrink_to_fit: Option<bool>,
title: Option<String>,
author: Option<String>,
subject: Option<String>,
keywords: Option<String>,
language: Option<String>,
header_text: Option<String>,
footer_text: Option<String>,
user_style_sheet: Option<String>,
fonts: Vec<PathBuf>,
use_system_fonts: Option<bool>,
compress: Option<bool>,
bookmarks: Option<bool>,
max_pages: Option<u32>,
creation_date: Option<SystemTime>,
conformance: Option<Conformance>,
accessibility: Option<Accessibility>,
timeout: Option<Duration>,
}
impl PdfOptions {
pub fn new() -> Self {
Self::default()
}
pub fn base_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.base_dir = Some(dir.into());
self
}
pub fn allow_file_access(mut self, allow: bool) -> Self {
self.allow_file_access = Some(allow);
self
}
pub fn page_size(mut self, size: PageSize) -> Self {
self.page_size = Some(size);
self
}
pub fn landscape(mut self, landscape: bool) -> Self {
self.landscape = Some(landscape);
self
}
pub fn margin(mut self, margin: Margin) -> Self {
self.margin = Some(margin);
self
}
pub fn scale(mut self, scale: f32) -> Self {
self.scale = Some(scale);
self
}
pub fn shrink_to_fit(mut self, shrink: bool) -> Self {
self.shrink_to_fit = Some(shrink);
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn author(mut self, author: impl Into<String>) -> Self {
self.author = Some(author.into());
self
}
pub fn subject(mut self, subject: impl Into<String>) -> Self {
self.subject = Some(subject.into());
self
}
pub fn keywords(mut self, keywords: impl Into<String>) -> Self {
self.keywords = Some(keywords.into());
self
}
pub fn language(mut self, language: impl Into<String>) -> Self {
self.language = Some(language.into());
self
}
pub fn header_text(mut self, text: impl Into<String>) -> Self {
self.header_text = Some(text.into());
self
}
pub fn footer_text(mut self, text: impl Into<String>) -> Self {
self.footer_text = Some(text.into());
self
}
pub fn user_style_sheet(mut self, css: impl Into<String>) -> Self {
self.user_style_sheet = Some(css.into());
self
}
pub fn font(mut self, path: impl Into<PathBuf>) -> Self {
self.fonts.push(path.into());
self
}
pub fn use_system_fonts(mut self, use_them: bool) -> Self {
self.use_system_fonts = Some(use_them);
self
}
pub fn compress(mut self, compress: bool) -> Self {
self.compress = Some(compress);
self
}
pub fn bookmarks(mut self, bookmarks: bool) -> Self {
self.bookmarks = Some(bookmarks);
self
}
pub fn max_pages(mut self, pages: u32) -> Self {
self.max_pages = Some(pages);
self
}
pub fn creation_date(mut self, date: SystemTime) -> Self {
self.creation_date = Some(date);
self
}
pub fn conformance(mut self, conformance: Conformance) -> Self {
self.conformance = Some(conformance);
self
}
pub fn accessibility(mut self, accessibility: Accessibility) -> Self {
self.accessibility = Some(accessibility);
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub(crate) fn has_base_dir(&self) -> bool {
self.base_dir.is_some()
}
pub(crate) fn to_json(&self) -> Result<String> {
let mut o = Object::default();
if let Some(dir) = &self.base_dir {
o.str("base_dir", &absolute(dir, "base_dir")?);
}
if let Some(v) = self.allow_file_access {
o.bool("allow_file_access", v);
}
match self.page_size {
Some(PageSize::Millimeters { width, height }) => {
o.nums("page_size", &[width as f64, height as f64])?
}
Some(size) => o.str(
"page_size",
match size {
PageSize::A3 => "A3",
PageSize::A5 => "A5",
PageSize::Letter => "Letter",
PageSize::Legal => "Legal",
_ => "A4",
},
),
None => {}
}
if let Some(v) = self.landscape {
o.bool("landscape", v);
}
if let Some(m) = self.margin {
o.nums(
"margin",
&[m.top as f64, m.right as f64, m.bottom as f64, m.left as f64],
)?;
}
if let Some(v) = self.scale {
o.num("scale", v as f64)?;
}
if let Some(v) = self.shrink_to_fit {
o.bool("shrink_to_fit", v);
}
for (name, value) in [
("title", &self.title),
("author", &self.author),
("subject", &self.subject),
("keywords", &self.keywords),
("language", &self.language),
("header_text", &self.header_text),
("footer_text", &self.footer_text),
("user_style_sheet", &self.user_style_sheet),
] {
if let Some(v) = value {
o.str(name, v);
}
}
if !self.fonts.is_empty() {
let fonts = self
.fonts
.iter()
.map(|f| absolute(f, "font"))
.collect::<Result<Vec<_>>>()?;
o.strs("fonts", &fonts);
}
if let Some(v) = self.use_system_fonts {
o.bool("use_system_fonts", v);
}
if let Some(v) = self.compress {
o.bool("compress", v);
}
if let Some(v) = self.bookmarks {
o.bool("bookmarks", v);
}
if let Some(v) = self.max_pages {
o.int("max_pages", v.min(i32::MAX as u32) as i64);
}
if let Some(v) = self.creation_date {
o.int("creation_date", unix_seconds(v));
}
if let Some(v) = self.conformance {
o.str("conformance", v.name());
}
if let Some(v) = self.accessibility {
o.str("accessibility", v.name());
}
timeout(&mut o, self.timeout)?;
Ok(o.finish())
}
}
#[derive(Debug, Clone, Default)]
pub struct ImageOptions {
dpi: Option<f32>,
format: Option<ImageFormat>,
pages: Option<Pages>,
width: Option<u32>,
height: Option<u32>,
transparent: Option<bool>,
quality: Option<u8>,
password: Option<String>,
timeout: Option<Duration>,
}
impl ImageOptions {
pub fn new() -> Self {
Self::default()
}
pub fn dpi(mut self, dpi: f32) -> Self {
self.dpi = Some(dpi);
self
}
pub fn format(mut self, format: ImageFormat) -> Self {
self.format = Some(format);
self
}
pub fn pages(mut self, pages: impl Into<Pages>) -> Self {
self.pages = Some(pages.into());
self
}
pub fn width(mut self, pixels: u32) -> Self {
self.width = Some(pixels);
self
}
pub fn height(mut self, pixels: u32) -> Self {
self.height = Some(pixels);
self
}
pub fn transparent(mut self, transparent: bool) -> Self {
self.transparent = Some(transparent);
self
}
pub fn quality(mut self, quality: u8) -> Self {
self.quality = Some(quality);
self
}
pub fn password(mut self, password: impl Into<String>) -> Self {
self.password = Some(password.into());
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub(crate) fn to_json(&self) -> Result<String> {
let mut o = Object::default();
if let Some(v) = self.dpi {
o.num("dpi", v as f64)?;
}
if let Some(v) = self.format {
o.str("format", v.name());
}
if let Some(v) = &self.pages {
o.str("pages", &v.0);
}
if let Some(v) = self.width {
o.int("width", v.min(i32::MAX as u32) as i64);
}
if let Some(v) = self.height {
o.int("height", v.min(i32::MAX as u32) as i64);
}
if let Some(v) = self.transparent {
o.bool("transparent", v);
}
if let Some(v) = self.quality {
o.int("quality", v as i64);
}
if let Some(v) = &self.password {
o.str("password", v);
}
timeout(&mut o, self.timeout)?;
Ok(o.finish())
}
}
#[derive(Debug, Clone, Default)]
pub struct TextOptions {
pages: Option<Pages>,
password: Option<String>,
order: Option<TextOrder>,
timeout: Option<Duration>,
}
impl TextOptions {
pub fn new() -> Self {
Self::default()
}
pub fn pages(mut self, pages: impl Into<Pages>) -> Self {
self.pages = Some(pages.into());
self
}
pub fn password(mut self, password: impl Into<String>) -> Self {
self.password = Some(password.into());
self
}
pub fn order(mut self, order: TextOrder) -> Self {
self.order = Some(order);
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub(crate) fn to_json(&self) -> Result<String> {
let mut o = Object::default();
if let Some(v) = &self.pages {
o.str("pages", &v.0);
}
if let Some(v) = &self.password {
o.str("password", v);
}
if let Some(v) = self.order {
o.str("order", v.name());
}
timeout(&mut o, self.timeout)?;
Ok(o.finish())
}
}
#[derive(Debug, Clone, Default)]
pub struct MarkdownOptions {
pages: Option<Pages>,
include_links: Option<bool>,
include_annotations: Option<bool>,
include_images: Option<bool>,
page_comments: Option<bool>,
max_pages: Option<u32>,
password: Option<String>,
timeout: Option<Duration>,
}
impl MarkdownOptions {
pub fn new() -> Self {
Self::default()
}
pub fn pages(mut self, pages: impl Into<Pages>) -> Self {
self.pages = Some(pages.into());
self
}
pub fn include_links(mut self, include: bool) -> Self {
self.include_links = Some(include);
self
}
pub fn include_annotations(mut self, include: bool) -> Self {
self.include_annotations = Some(include);
self
}
pub fn include_images(mut self, include: bool) -> Self {
self.include_images = Some(include);
self
}
pub fn page_comments(mut self, comments: bool) -> Self {
self.page_comments = Some(comments);
self
}
pub fn max_pages(mut self, pages: u32) -> Self {
self.max_pages = Some(pages);
self
}
pub fn password(mut self, password: impl Into<String>) -> Self {
self.password = Some(password.into());
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub(crate) fn to_json(&self) -> Result<String> {
let mut o = Object::default();
if let Some(v) = &self.pages {
o.str("pages", &v.0);
}
for (name, value) in [
("include_links", self.include_links),
("include_annotations", self.include_annotations),
("include_images", self.include_images),
("page_comments", self.page_comments),
] {
if let Some(v) = value {
o.bool(name, v);
}
}
if let Some(v) = self.max_pages {
o.int("max_pages", v.min(i32::MAX as u32) as i64);
}
if let Some(v) = &self.password {
o.str("password", v);
}
timeout(&mut o, self.timeout)?;
Ok(o.finish())
}
}
#[derive(Debug, Clone, Default)]
pub struct PageCountOptions {
password: Option<String>,
}
impl PageCountOptions {
pub fn new() -> Self {
Self::default()
}
pub fn password(mut self, password: impl Into<String>) -> Self {
self.password = Some(password.into());
self
}
pub(crate) fn to_json(&self) -> Result<String> {
let mut o = Object::default();
if let Some(v) = &self.password {
o.str("password", v);
}
Ok(o.finish())
}
}
#[derive(Debug, Clone, Default)]
pub struct PdfAOptions {
conformance: Option<Conformance>,
timeout: Option<Duration>,
}
impl PdfAOptions {
pub fn new() -> Self {
Self::default()
}
pub fn conformance(mut self, conformance: Conformance) -> Self {
self.conformance = Some(conformance);
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub(crate) fn to_json(&self) -> Result<String> {
let mut o = Object::default();
if let Some(v) = self.conformance {
o.str("conformance", v.name());
}
timeout(&mut o, self.timeout)?;
Ok(o.finish())
}
}
#[derive(Debug, Clone, Default)]
pub struct MergeOptions {
title: Option<String>,
author: Option<String>,
bookmarks: Option<bool>,
creation_date: Option<SystemTime>,
timeout: Option<Duration>,
}
impl MergeOptions {
pub fn new() -> Self {
Self::default()
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn author(mut self, author: impl Into<String>) -> Self {
self.author = Some(author.into());
self
}
pub fn bookmarks(mut self, bookmarks: bool) -> Self {
self.bookmarks = Some(bookmarks);
self
}
pub fn creation_date(mut self, date: SystemTime) -> Self {
self.creation_date = Some(date);
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub(crate) fn to_json(&self) -> Result<String> {
let mut o = Object::default();
if let Some(v) = &self.title {
o.str("title", v);
}
if let Some(v) = &self.author {
o.str("author", v);
}
if let Some(v) = self.bookmarks {
o.bool("bookmarks", v);
}
if let Some(v) = self.creation_date {
o.int("creation_date", unix_seconds(v));
}
timeout(&mut o, self.timeout)?;
Ok(o.finish())
}
}
#[derive(Debug, Clone, Default)]
pub struct RedactOptions {
text: Option<String>,
pattern: Option<String>,
areas: Vec<[f64; 5]>,
ignore_case: Option<bool>,
fill_color: Option<String>,
timeout: Option<Duration>,
}
impl RedactOptions {
pub fn new() -> Self {
Self::default()
}
pub fn text(mut self, text: impl Into<String>) -> Self {
self.text = Some(text.into());
self
}
pub fn pattern(mut self, pattern: impl Into<String>) -> Self {
self.pattern = Some(pattern.into());
self
}
pub fn area(mut self, page: u32, x: f64, y: f64, width: f64, height: f64) -> Self {
self.areas.push([f64::from(page), x, y, width, height]);
self
}
pub fn ignore_case(mut self, ignore_case: bool) -> Self {
self.ignore_case = Some(ignore_case);
self
}
pub fn fill_color(mut self, color: impl Into<String>) -> Self {
self.fill_color = Some(color.into());
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub(crate) fn to_json(&self) -> Result<String> {
let mut o = Object::default();
if let Some(v) = &self.text {
o.str("text", v);
}
if let Some(v) = &self.pattern {
o.str("pattern", v);
}
if !self.areas.is_empty() {
o.num_lists("areas", &self.areas)?;
}
if let Some(v) = self.ignore_case {
o.bool("ignore_case", v);
}
if let Some(v) = &self.fill_color {
o.str("fill_color", v);
}
timeout(&mut o, self.timeout)?;
Ok(o.finish())
}
}
#[derive(Debug, Clone, Default)]
pub struct BarcodeOptions {
formats: Vec<BarcodeFormat>,
try_inverted: Option<bool>,
pages: Option<Pages>,
password: Option<String>,
dpi: Option<f64>,
timeout: Option<Duration>,
}
impl BarcodeOptions {
pub fn new() -> Self {
Self::default()
}
pub fn formats(mut self, formats: impl IntoIterator<Item = BarcodeFormat>) -> Self {
self.formats = formats.into_iter().collect();
self
}
pub fn try_inverted(mut self, try_inverted: bool) -> Self {
self.try_inverted = Some(try_inverted);
self
}
pub fn pages(mut self, pages: impl Into<Pages>) -> Self {
self.pages = Some(pages.into());
self
}
pub fn password(mut self, password: impl Into<String>) -> Self {
self.password = Some(password.into());
self
}
pub fn dpi(mut self, dpi: f64) -> Self {
self.dpi = Some(dpi);
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub(crate) fn to_json(&self) -> Result<String> {
let mut o = Object::default();
if !self.formats.is_empty() {
let names: Vec<String> = self.formats.iter().map(|f| f.name().to_string()).collect();
o.strs("formats", &names);
}
if let Some(v) = self.try_inverted {
o.bool("try_inverted", v);
}
if let Some(v) = &self.pages {
o.str("pages", &v.0);
}
if let Some(v) = &self.password {
o.str("password", v);
}
if let Some(v) = self.dpi {
o.num("dpi", v)?;
}
timeout(&mut o, self.timeout)?;
Ok(o.finish())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn option_names_are_the_native_ones() {
let json = PdfOptions::new()
.page_size(PageSize::Millimeters {
width: 100.0,
height: 50.0,
})
.margin(Margin::new(1.0, 2.0, 3.0, 4.0))
.footer_text("Page {page}")
.creation_date(UNIX_EPOCH + Duration::from_secs(5))
.conformance(Conformance::PdfA3u)
.timeout(Duration::from_millis(1500))
.to_json()
.unwrap();
assert_eq!(
json,
r#"{"page_size":[100,50],"margin":[1,2,3,4],"footer_text":"Page {page}","creation_date":5,"conformance":"PDF/A-3u","timeout":1.5}"#
);
assert_eq!(PdfOptions::new().to_json().unwrap(), "{}");
assert_eq!(
ImageOptions::new().pages([1, 3]).to_json().unwrap(),
r#"{"pages":"1,3"}"#
);
assert_eq!(
TextOptions::new().pages(2..=4).to_json().unwrap(),
r#"{"pages":"2-4"}"#
);
assert_eq!(
TextOptions::new()
.order(TextOrder::TopToBottom)
.to_json()
.unwrap(),
r#"{"order":"top_to_bottom"}"#
);
assert_eq!(
RedactOptions::new()
.text("secret")
.area(2, 10.0, 20.5, 30.0, 40.0)
.fill_color("transparent")
.to_json()
.unwrap(),
r#"{"text":"secret","areas":[[2,10,20.5,30,40]],"fill_color":"transparent"}"#
);
assert_eq!(
BarcodeOptions::new()
.formats([BarcodeFormat::QrCode, BarcodeFormat::Ean13])
.try_inverted(false)
.to_json()
.unwrap(),
r#"{"formats":["qr_code","ean13"],"try_inverted":false}"#
);
assert_eq!(unix_seconds(UNIX_EPOCH - Duration::from_millis(1500)), -2);
}
}