Skip to main content

pdfpecker/
options.rs

1//! Options for each call. Each is built with chained methods from its `Default`, so options can be added in later
2//! releases without breaking code that uses them:
3//!
4//! ```
5//! use pdfpecker::{Margin, PageSize, PdfOptions};
6//! let options = PdfOptions::new().page_size(PageSize::Letter).margin(Margin::all(54.0)).title("Invoice 42");
7//! ```
8
9use crate::error::{Error, Result};
10use crate::json::Object;
11use std::ops::RangeInclusive;
12use std::path::{Path, PathBuf};
13use std::time::{Duration, SystemTime, UNIX_EPOCH};
14
15/// The paper size of the pages. A CSS `@page { size: ... }` rule in the document wins over it.
16#[derive(Debug, Clone, Copy, PartialEq, Default)]
17#[non_exhaustive]
18pub enum PageSize {
19    /// 297 × 420 mm.
20    A3,
21    /// 210 × 297 mm, the default.
22    #[default]
23    A4,
24    /// 148 × 210 mm.
25    A5,
26    /// 8.5 × 11 inches.
27    Letter,
28    /// 8.5 × 14 inches.
29    Legal,
30    /// Any size, in millimetres.
31    Millimeters {
32        /// The page width in millimetres.
33        width: f32,
34        /// The page height in millimetres.
35        height: f32,
36    },
37}
38
39/// Page margins in points (72 per inch). A CSS `@page { margin: ... }` rule in the document wins over them.
40#[derive(Debug, Clone, Copy, PartialEq)]
41pub struct Margin {
42    /// The top margin in points.
43    pub top: f32,
44    /// The right margin in points.
45    pub right: f32,
46    /// The bottom margin in points.
47    pub bottom: f32,
48    /// The left margin in points.
49    pub left: f32,
50}
51
52impl Margin {
53    /// The same margin on all four sides, in points.
54    pub fn all(points: f32) -> Self {
55        Self {
56            top: points,
57            right: points,
58            bottom: points,
59            left: points,
60        }
61    }
62
63    /// Four margins in points, in CSS order: top, right, bottom, left.
64    pub fn new(top: f32, right: f32, bottom: f32, left: f32) -> Self {
65        Self {
66            top,
67            right,
68            bottom,
69            left,
70        }
71    }
72}
73
74impl Default for Margin {
75    /// Half an inch (36 points) on every side, PdfPecker's default.
76    fn default() -> Self {
77        Self::all(36.0)
78    }
79}
80
81/// A PDF/A level for archiving.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
83#[non_exhaustive]
84pub enum Conformance {
85    /// PDF/A-2b: the pages look the same in the future.
86    PdfA2b,
87    /// PDF/A-2u: PDF/A-2b, and all text can be extracted as Unicode.
88    PdfA2u,
89    /// PDF/A-3b: PDF/A-2b that may carry any attached file.
90    PdfA3b,
91    /// PDF/A-3u: PDF/A-3b, and all text can be extracted as Unicode.
92    PdfA3u,
93    /// PDF/A-2a: PDF/A-2u with a tagged structure tree and a declared language, whatever the accessibility option
94    /// says. Only HTML and Markdown conversion write it.
95    PdfA2a,
96    /// PDF/A-3a: PDF/A-2a that may carry any attached file. Only HTML and Markdown conversion write it.
97    PdfA3a,
98    /// PDF/A-4, on PDF 2.0. Only HTML and Markdown conversion write it.
99    PdfA4,
100    /// PDF/A-4f, which allows attachments. Only HTML and Markdown conversion write it.
101    PdfA4f,
102}
103
104impl Conformance {
105    fn name(self) -> &'static str {
106        match self {
107            Self::PdfA2b => "PDF/A-2b",
108            Self::PdfA2u => "PDF/A-2u",
109            Self::PdfA3b => "PDF/A-3b",
110            Self::PdfA3u => "PDF/A-3u",
111            Self::PdfA2a => "PDF/A-2a",
112            Self::PdfA3a => "PDF/A-3a",
113            Self::PdfA4 => "PDF/A-4",
114            Self::PdfA4f => "PDF/A-4f",
115        }
116    }
117}
118
119/// How much structure the PDF carries for screen readers and text extraction.
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
121#[non_exhaustive]
122pub enum Accessibility {
123    /// A tagged PDF: headings, paragraphs, lists, tables and figures in reading order.
124    Tagged,
125    /// PDF/UA-1, the accessibility standard, which also needs a title and a language.
126    PdfUa1,
127    /// PDF/UA-2, on PDF 2.0.
128    PdfUa2,
129}
130
131impl Accessibility {
132    fn name(self) -> &'static str {
133        match self {
134            Self::Tagged => "tagged",
135            Self::PdfUa1 => "PDF/UA-1",
136            Self::PdfUa2 => "PDF/UA-2",
137        }
138    }
139}
140
141/// The format of rendered pages.
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
143#[non_exhaustive]
144pub enum ImageFormat {
145    /// PNG, the default: lossless.
146    #[default]
147    Png,
148    /// JPEG: small, for photos; see [`ImageOptions::quality`].
149    Jpeg,
150    /// WebP; see [`ImageOptions::quality`].
151    WebP,
152}
153
154impl ImageFormat {
155    fn name(self) -> &'static str {
156        match self {
157            Self::Png => "png",
158            Self::Jpeg => "jpeg",
159            Self::WebP => "webp",
160        }
161    }
162}
163
164/// The order in which [`pdf_to_text`](crate::pdf_to_text) reads a page's text.
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
166#[non_exhaustive]
167pub enum TextOrder {
168    /// Reading order, the default: columns one after the other, and tables row by row.
169    #[default]
170    Reading,
171    /// Lines from the top of the page to the bottom, left to right, as the text stands on the page.
172    TopToBottom,
173}
174
175impl TextOrder {
176    fn name(self) -> &'static str {
177        match self {
178            Self::Reading => "reading",
179            Self::TopToBottom => "top_to_bottom",
180        }
181    }
182}
183
184/// A barcode symbology that [`read_barcodes`](crate::read_barcodes) can find.
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
186#[non_exhaustive]
187pub enum BarcodeFormat {
188    /// QR code.
189    QrCode,
190    /// Data Matrix.
191    DataMatrix,
192    /// Code 128, GS1-128 included.
193    Code128,
194    /// EAN-13.
195    Ean13,
196}
197
198impl BarcodeFormat {
199    pub(crate) fn name(self) -> &'static str {
200        match self {
201            Self::QrCode => "qr_code",
202            Self::DataMatrix => "data_matrix",
203            Self::Code128 => "code128",
204            Self::Ean13 => "ean13",
205        }
206    }
207
208    pub(crate) fn from_name(name: &str) -> Option<Self> {
209        match name {
210            "qr_code" => Some(Self::QrCode),
211            "data_matrix" => Some(Self::DataMatrix),
212            "code128" => Some(Self::Code128),
213            "ean13" => Some(Self::Ean13),
214            _ => None,
215        }
216    }
217}
218
219/// Which pages a call reads, numbered from 1.
220///
221/// Made from a page number (`3`), a list (`[1, 3]` or `vec![1, 3]`), a range (`2..=4`) or a string in PdfPecker's
222/// syntax (`"1-3,5"`, `"last"`, `"2-last"`). A page past the end is an [`ErrorKind::InvalidArgument`](crate::ErrorKind::InvalidArgument).
223#[derive(Debug, Clone, PartialEq, Eq, Hash)]
224pub struct Pages(String);
225
226impl From<u32> for Pages {
227    fn from(page: u32) -> Self {
228        Self(page.to_string())
229    }
230}
231
232impl From<&str> for Pages {
233    fn from(range: &str) -> Self {
234        Self(range.to_string())
235    }
236}
237
238impl From<String> for Pages {
239    fn from(range: String) -> Self {
240        Self(range)
241    }
242}
243
244impl From<&[u32]> for Pages {
245    fn from(pages: &[u32]) -> Self {
246        Self(
247            pages
248                .iter()
249                .map(u32::to_string)
250                .collect::<Vec<_>>()
251                .join(","),
252        )
253    }
254}
255
256impl<const N: usize> From<[u32; N]> for Pages {
257    fn from(pages: [u32; N]) -> Self {
258        Self::from(&pages[..])
259    }
260}
261
262impl From<Vec<u32>> for Pages {
263    fn from(pages: Vec<u32>) -> Self {
264        Self::from(&pages[..])
265    }
266}
267
268impl From<RangeInclusive<u32>> for Pages {
269    fn from(range: RangeInclusive<u32>) -> Self {
270        Self(format!("{}-{}", range.start(), range.end()))
271    }
272}
273
274fn absolute(path: &Path, what: &str) -> Result<String> {
275    let absolute = std::path::absolute(path)
276        .map_err(|e| Error::argument(format!("{what}: {}: {e}", path.display())))?;
277    absolute
278        .into_os_string()
279        .into_string()
280        .map_err(|p| Error::argument(format!("{what} is not valid Unicode: {p:?}")))
281}
282
283fn unix_seconds(time: SystemTime) -> i64 {
284    match time.duration_since(UNIX_EPOCH) {
285        Ok(d) => d.as_secs() as i64,
286        Err(e) => -(e.duration().as_secs_f64().ceil() as i64),
287    }
288}
289
290// The native timer takes at most 2^31 - 1 milliseconds (about 24.8 days); the 1.23.0 library reports a longer
291// timeout as an internal error, so it is checked here.
292fn timeout(o: &mut Object, timeout: Option<Duration>) -> Result<()> {
293    if let Some(t) = timeout {
294        if t.is_zero() || t.as_secs_f64() > 2_147_483.0 {
295            return Err(Error::argument(format!(
296                "timeout must be above 0 and at most 2,147,483 seconds, not {t:?}"
297            )));
298        }
299        o.num("timeout", t.as_secs_f64())?;
300    }
301    Ok(())
302}
303
304/// Options of HTML and Markdown conversion.
305#[derive(Debug, Clone, Default)]
306pub struct PdfOptions {
307    base_dir: Option<PathBuf>,
308    allow_file_access: Option<bool>,
309    page_size: Option<PageSize>,
310    landscape: Option<bool>,
311    margin: Option<Margin>,
312    scale: Option<f32>,
313    shrink_to_fit: Option<bool>,
314    title: Option<String>,
315    author: Option<String>,
316    subject: Option<String>,
317    keywords: Option<String>,
318    language: Option<String>,
319    header_text: Option<String>,
320    footer_text: Option<String>,
321    user_style_sheet: Option<String>,
322    fonts: Vec<PathBuf>,
323    use_system_fonts: Option<bool>,
324    compress: Option<bool>,
325    bookmarks: Option<bool>,
326    max_pages: Option<u32>,
327    creation_date: Option<SystemTime>,
328    conformance: Option<Conformance>,
329    accessibility: Option<Accessibility>,
330    timeout: Option<Duration>,
331}
332
333impl PdfOptions {
334    /// The defaults: A4, half-inch margins, compressed, bookmarks from headings, no local files.
335    pub fn new() -> Self {
336        Self::default()
337    }
338
339    /// The folder that relative image, stylesheet and font paths resolve against. Without it, a document from a
340    /// string loads no local files. Files outside it load only with [`allow_file_access`](Self::allow_file_access).
341    pub fn base_dir(mut self, dir: impl Into<PathBuf>) -> Self {
342        self.base_dir = Some(dir.into());
343        self
344    }
345
346    /// Lets the document load local files outside [`base_dir`](Self::base_dir), by absolute path or `../`. Only for
347    /// documents you trust.
348    pub fn allow_file_access(mut self, allow: bool) -> Self {
349        self.allow_file_access = Some(allow);
350        self
351    }
352
353    /// The paper size (default A4). A CSS `@page` size wins over it.
354    pub fn page_size(mut self, size: PageSize) -> Self {
355        self.page_size = Some(size);
356        self
357    }
358
359    /// Turns the page size sideways.
360    pub fn landscape(mut self, landscape: bool) -> Self {
361        self.landscape = Some(landscape);
362        self
363    }
364
365    /// The page margins in points (default 36, half an inch). A CSS `@page` margin wins over them.
366    pub fn margin(mut self, margin: Margin) -> Self {
367        self.margin = Some(margin);
368        self
369    }
370
371    /// Zooms the content, such as 0.8 for 80%.
372    pub fn scale(mut self, scale: f32) -> Self {
373        self.scale = Some(scale);
374        self
375    }
376
377    /// Shrinks content wider than the page to fit it (on by default).
378    pub fn shrink_to_fit(mut self, shrink: bool) -> Self {
379        self.shrink_to_fit = Some(shrink);
380        self
381    }
382
383    /// The document title, which PDF viewers show in their title bar.
384    pub fn title(mut self, title: impl Into<String>) -> Self {
385        self.title = Some(title.into());
386        self
387    }
388
389    /// The document author.
390    pub fn author(mut self, author: impl Into<String>) -> Self {
391        self.author = Some(author.into());
392        self
393    }
394
395    /// The document subject.
396    pub fn subject(mut self, subject: impl Into<String>) -> Self {
397        self.subject = Some(subject.into());
398        self
399    }
400
401    /// The document keywords.
402    pub fn keywords(mut self, keywords: impl Into<String>) -> Self {
403        self.keywords = Some(keywords.into());
404        self
405    }
406
407    /// The document language, such as `"en-GB"`; it also picks the hyphenation patterns.
408    pub fn language(mut self, language: impl Into<String>) -> Self {
409        self.language = Some(language.into());
410        self
411    }
412
413    /// A line of text at the top of every page; `{page}` and `{pages}` are replaced, as in `"Page {page} of {pages}"`.
414    pub fn header_text(mut self, text: impl Into<String>) -> Self {
415        self.header_text = Some(text.into());
416        self
417    }
418
419    /// A line of text at the bottom of every page; `{page}` and `{pages}` are replaced.
420    pub fn footer_text(mut self, text: impl Into<String>) -> Self {
421        self.footer_text = Some(text.into());
422        self
423    }
424
425    /// CSS applied after the document's own.
426    pub fn user_style_sheet(mut self, css: impl Into<String>) -> Self {
427        self.user_style_sheet = Some(css.into());
428        self
429    }
430
431    /// A TrueType or OpenType file, or a folder of them, that CSS `font-family` can name. Without fonts, text uses the
432    /// standard PDF fonts, which cover Western European characters only; add fonts for other scripts. Call it once
433    /// for each file or folder.
434    pub fn font(mut self, path: impl Into<PathBuf>) -> Self {
435        self.fonts.push(path.into());
436        self
437    }
438
439    /// Also looks up font families among the fonts installed on this machine.
440    pub fn use_system_fonts(mut self, use_them: bool) -> Self {
441        self.use_system_fonts = Some(use_them);
442        self
443    }
444
445    /// Compresses the PDF's streams (on by default).
446    pub fn compress(mut self, compress: bool) -> Self {
447        self.compress = Some(compress);
448        self
449    }
450
451    /// Makes bookmarks from the headings (on by default).
452    pub fn bookmarks(mut self, bookmarks: bool) -> Self {
453        self.bookmarks = Some(bookmarks);
454        self
455    }
456
457    /// Stops with [`ErrorKind::Limit`](crate::ErrorKind::Limit) beyond this many pages (default 100,000).
458    pub fn max_pages(mut self, pages: u32) -> Self {
459        self.max_pages = Some(pages);
460        self
461    }
462
463    /// A fixed creation date, which makes the output byte-identical from run to run.
464    pub fn creation_date(mut self, date: SystemTime) -> Self {
465        self.creation_date = Some(date);
466        self
467    }
468
469    /// Writes a PDF/A file for archiving.
470    pub fn conformance(mut self, conformance: Conformance) -> Self {
471        self.conformance = Some(conformance);
472        self
473    }
474
475    /// Writes a tagged or PDF/UA file for screen readers.
476    pub fn accessibility(mut self, accessibility: Accessibility) -> Self {
477        self.accessibility = Some(accessibility);
478        self
479    }
480
481    /// Stops with [`ErrorKind::Timeout`](crate::ErrorKind::Timeout) after this long.
482    pub fn timeout(mut self, timeout: Duration) -> Self {
483        self.timeout = Some(timeout);
484        self
485    }
486
487    pub(crate) fn has_base_dir(&self) -> bool {
488        self.base_dir.is_some()
489    }
490
491    pub(crate) fn to_json(&self) -> Result<String> {
492        let mut o = Object::default();
493        if let Some(dir) = &self.base_dir {
494            o.str("base_dir", &absolute(dir, "base_dir")?);
495        }
496        if let Some(v) = self.allow_file_access {
497            o.bool("allow_file_access", v);
498        }
499        match self.page_size {
500            Some(PageSize::Millimeters { width, height }) => {
501                o.nums("page_size", &[width as f64, height as f64])?
502            }
503            Some(size) => o.str(
504                "page_size",
505                match size {
506                    PageSize::A3 => "A3",
507                    PageSize::A5 => "A5",
508                    PageSize::Letter => "Letter",
509                    PageSize::Legal => "Legal",
510                    _ => "A4",
511                },
512            ),
513            None => {}
514        }
515        if let Some(v) = self.landscape {
516            o.bool("landscape", v);
517        }
518        if let Some(m) = self.margin {
519            o.nums(
520                "margin",
521                &[m.top as f64, m.right as f64, m.bottom as f64, m.left as f64],
522            )?;
523        }
524        if let Some(v) = self.scale {
525            o.num("scale", v as f64)?;
526        }
527        if let Some(v) = self.shrink_to_fit {
528            o.bool("shrink_to_fit", v);
529        }
530        for (name, value) in [
531            ("title", &self.title),
532            ("author", &self.author),
533            ("subject", &self.subject),
534            ("keywords", &self.keywords),
535            ("language", &self.language),
536            ("header_text", &self.header_text),
537            ("footer_text", &self.footer_text),
538            ("user_style_sheet", &self.user_style_sheet),
539        ] {
540            if let Some(v) = value {
541                o.str(name, v);
542            }
543        }
544        if !self.fonts.is_empty() {
545            let fonts = self
546                .fonts
547                .iter()
548                .map(|f| absolute(f, "font"))
549                .collect::<Result<Vec<_>>>()?;
550            o.strs("fonts", &fonts);
551        }
552        if let Some(v) = self.use_system_fonts {
553            o.bool("use_system_fonts", v);
554        }
555        if let Some(v) = self.compress {
556            o.bool("compress", v);
557        }
558        if let Some(v) = self.bookmarks {
559            o.bool("bookmarks", v);
560        }
561        if let Some(v) = self.max_pages {
562            o.int("max_pages", v.min(i32::MAX as u32) as i64);
563        }
564        if let Some(v) = self.creation_date {
565            o.int("creation_date", unix_seconds(v));
566        }
567        if let Some(v) = self.conformance {
568            o.str("conformance", v.name());
569        }
570        if let Some(v) = self.accessibility {
571            o.str("accessibility", v.name());
572        }
573        timeout(&mut o, self.timeout)?;
574        Ok(o.finish())
575    }
576}
577
578/// Options of [`pdf_to_images`](crate::pdf_to_images).
579#[derive(Debug, Clone, Default)]
580pub struct ImageOptions {
581    dpi: Option<f32>,
582    format: Option<ImageFormat>,
583    pages: Option<Pages>,
584    width: Option<u32>,
585    height: Option<u32>,
586    transparent: Option<bool>,
587    quality: Option<u8>,
588    password: Option<String>,
589    timeout: Option<Duration>,
590}
591
592impl ImageOptions {
593    /// The defaults: every page as a PNG at 150 dpi.
594    pub fn new() -> Self {
595        Self::default()
596    }
597
598    /// The resolution in dots per inch (default 150).
599    pub fn dpi(mut self, dpi: f32) -> Self {
600        self.dpi = Some(dpi);
601        self
602    }
603
604    /// PNG (default), JPEG or WebP.
605    pub fn format(mut self, format: ImageFormat) -> Self {
606        self.format = Some(format);
607        self
608    }
609
610    /// Which pages to render (default all).
611    pub fn pages(mut self, pages: impl Into<Pages>) -> Self {
612        self.pages = Some(pages.into());
613        self
614    }
615
616    /// A fixed width in pixels instead of a resolution, keeping the aspect ratio.
617    pub fn width(mut self, pixels: u32) -> Self {
618        self.width = Some(pixels);
619        self
620    }
621
622    /// A fixed height in pixels instead of a resolution, keeping the aspect ratio.
623    pub fn height(mut self, pixels: u32) -> Self {
624        self.height = Some(pixels);
625        self
626    }
627
628    /// Leaves the page background transparent instead of white (PNG and WebP).
629    pub fn transparent(mut self, transparent: bool) -> Self {
630        self.transparent = Some(transparent);
631        self
632    }
633
634    /// The quality of JPEG and WebP images, 1 to 100.
635    pub fn quality(mut self, quality: u8) -> Self {
636        self.quality = Some(quality);
637        self
638    }
639
640    /// The password of an encrypted PDF.
641    pub fn password(mut self, password: impl Into<String>) -> Self {
642        self.password = Some(password.into());
643        self
644    }
645
646    /// Stops with [`ErrorKind::Timeout`](crate::ErrorKind::Timeout) after this long.
647    pub fn timeout(mut self, timeout: Duration) -> Self {
648        self.timeout = Some(timeout);
649        self
650    }
651
652    pub(crate) fn to_json(&self) -> Result<String> {
653        let mut o = Object::default();
654        if let Some(v) = self.dpi {
655            o.num("dpi", v as f64)?;
656        }
657        if let Some(v) = self.format {
658            o.str("format", v.name());
659        }
660        if let Some(v) = &self.pages {
661            o.str("pages", &v.0);
662        }
663        if let Some(v) = self.width {
664            o.int("width", v.min(i32::MAX as u32) as i64);
665        }
666        if let Some(v) = self.height {
667            o.int("height", v.min(i32::MAX as u32) as i64);
668        }
669        if let Some(v) = self.transparent {
670            o.bool("transparent", v);
671        }
672        if let Some(v) = self.quality {
673            o.int("quality", v as i64);
674        }
675        if let Some(v) = &self.password {
676            o.str("password", v);
677        }
678        timeout(&mut o, self.timeout)?;
679        Ok(o.finish())
680    }
681}
682
683/// Options of [`pdf_to_text`](crate::pdf_to_text) and [`pdf_to_text_pages`](crate::pdf_to_text_pages).
684#[derive(Debug, Clone, Default)]
685pub struct TextOptions {
686    pages: Option<Pages>,
687    password: Option<String>,
688    order: Option<TextOrder>,
689    timeout: Option<Duration>,
690}
691
692impl TextOptions {
693    /// The defaults: every page.
694    pub fn new() -> Self {
695        Self::default()
696    }
697
698    /// Which pages to read (default all).
699    pub fn pages(mut self, pages: impl Into<Pages>) -> Self {
700        self.pages = Some(pages.into());
701        self
702    }
703
704    /// The password of an encrypted PDF.
705    pub fn password(mut self, password: impl Into<String>) -> Self {
706        self.password = Some(password.into());
707        self
708    }
709
710    /// The order of the text on each page: [`TextOrder::Reading`] (default) or [`TextOrder::TopToBottom`].
711    pub fn order(mut self, order: TextOrder) -> Self {
712        self.order = Some(order);
713        self
714    }
715
716    /// Stops with [`ErrorKind::Timeout`](crate::ErrorKind::Timeout) after this long.
717    pub fn timeout(mut self, timeout: Duration) -> Self {
718        self.timeout = Some(timeout);
719        self
720    }
721
722    pub(crate) fn to_json(&self) -> Result<String> {
723        let mut o = Object::default();
724        if let Some(v) = &self.pages {
725            o.str("pages", &v.0);
726        }
727        if let Some(v) = &self.password {
728            o.str("password", v);
729        }
730        if let Some(v) = self.order {
731            o.str("order", v.name());
732        }
733        timeout(&mut o, self.timeout)?;
734        Ok(o.finish())
735    }
736}
737
738/// Options of [`pdf_to_markdown`](crate::pdf_to_markdown).
739#[derive(Debug, Clone, Default)]
740pub struct MarkdownOptions {
741    pages: Option<Pages>,
742    include_links: Option<bool>,
743    include_annotations: Option<bool>,
744    include_images: Option<bool>,
745    page_comments: Option<bool>,
746    max_pages: Option<u32>,
747    password: Option<String>,
748    timeout: Option<Duration>,
749}
750
751impl MarkdownOptions {
752    /// The defaults: every page, with links and annotation text, without images.
753    pub fn new() -> Self {
754        Self::default()
755    }
756
757    /// Which pages to read (default all).
758    pub fn pages(mut self, pages: impl Into<Pages>) -> Self {
759        self.pages = Some(pages.into());
760        self
761    }
762
763    /// Text that links to a web or email address becomes a Markdown link (on by default).
764    pub fn include_links(mut self, include: bool) -> Self {
765        self.include_links = Some(include);
766        self
767    }
768
769    /// Includes the text of annotations, such as form field values (on by default).
770    pub fn include_annotations(mut self, include: bool) -> Self {
771        self.include_annotations = Some(include);
772        self
773    }
774
775    /// Includes pictures as `data:` URI images where they sit in the text (off by default: they are long).
776    pub fn include_images(mut self, include: bool) -> Self {
777        self.include_images = Some(include);
778        self
779    }
780
781    /// Starts each page with an HTML comment such as `<!-- page 3 -->`, so a passage can be traced to its page.
782    pub fn page_comments(mut self, comments: bool) -> Self {
783        self.page_comments = Some(comments);
784        self
785    }
786
787    /// Stops with [`ErrorKind::Limit`](crate::ErrorKind::Limit) beyond this many pages (default 10,000).
788    pub fn max_pages(mut self, pages: u32) -> Self {
789        self.max_pages = Some(pages);
790        self
791    }
792
793    /// The password of an encrypted PDF.
794    pub fn password(mut self, password: impl Into<String>) -> Self {
795        self.password = Some(password.into());
796        self
797    }
798
799    /// Stops with [`ErrorKind::Timeout`](crate::ErrorKind::Timeout) after this long.
800    pub fn timeout(mut self, timeout: Duration) -> Self {
801        self.timeout = Some(timeout);
802        self
803    }
804
805    pub(crate) fn to_json(&self) -> Result<String> {
806        let mut o = Object::default();
807        if let Some(v) = &self.pages {
808            o.str("pages", &v.0);
809        }
810        for (name, value) in [
811            ("include_links", self.include_links),
812            ("include_annotations", self.include_annotations),
813            ("include_images", self.include_images),
814            ("page_comments", self.page_comments),
815        ] {
816            if let Some(v) = value {
817                o.bool(name, v);
818            }
819        }
820        if let Some(v) = self.max_pages {
821            o.int("max_pages", v.min(i32::MAX as u32) as i64);
822        }
823        if let Some(v) = &self.password {
824            o.str("password", v);
825        }
826        timeout(&mut o, self.timeout)?;
827        Ok(o.finish())
828    }
829}
830
831/// Options of [`pdf_page_count`](crate::pdf_page_count).
832#[derive(Debug, Clone, Default)]
833pub struct PageCountOptions {
834    password: Option<String>,
835}
836
837impl PageCountOptions {
838    /// The defaults: no password.
839    pub fn new() -> Self {
840        Self::default()
841    }
842
843    /// The password of an encrypted PDF.
844    pub fn password(mut self, password: impl Into<String>) -> Self {
845        self.password = Some(password.into());
846        self
847    }
848
849    pub(crate) fn to_json(&self) -> Result<String> {
850        let mut o = Object::default();
851        if let Some(v) = &self.password {
852            o.str("password", v);
853        }
854        Ok(o.finish())
855    }
856}
857
858/// Options of [`pdf_to_pdfa`](crate::pdf_to_pdfa).
859#[derive(Debug, Clone, Default)]
860pub struct PdfAOptions {
861    conformance: Option<Conformance>,
862    timeout: Option<Duration>,
863}
864
865impl PdfAOptions {
866    /// The defaults: PDF/A-2b.
867    pub fn new() -> Self {
868        Self::default()
869    }
870
871    /// PDF/A-2b (default), PDF/A-2u, PDF/A-3b or PDF/A-3u. PDF/A-2 keeps only attachments that are PDF/A files
872    /// themselves; PDF/A-3 keeps every attachment.
873    pub fn conformance(mut self, conformance: Conformance) -> Self {
874        self.conformance = Some(conformance);
875        self
876    }
877
878    /// Stops with [`ErrorKind::Timeout`](crate::ErrorKind::Timeout) after this long.
879    pub fn timeout(mut self, timeout: Duration) -> Self {
880        self.timeout = Some(timeout);
881        self
882    }
883
884    pub(crate) fn to_json(&self) -> Result<String> {
885        let mut o = Object::default();
886        if let Some(v) = self.conformance {
887            o.str("conformance", v.name());
888        }
889        timeout(&mut o, self.timeout)?;
890        Ok(o.finish())
891    }
892}
893
894/// Options of [`merge_pdfs`](crate::merge_pdfs).
895#[derive(Debug, Clone, Default)]
896pub struct MergeOptions {
897    title: Option<String>,
898    author: Option<String>,
899    bookmarks: Option<bool>,
900    creation_date: Option<SystemTime>,
901    timeout: Option<Duration>,
902}
903
904impl MergeOptions {
905    /// The defaults: keeps each file's bookmarks, one after the other.
906    pub fn new() -> Self {
907        Self::default()
908    }
909
910    /// The merged document's title.
911    pub fn title(mut self, title: impl Into<String>) -> Self {
912        self.title = Some(title.into());
913        self
914    }
915
916    /// The merged document's author.
917    pub fn author(mut self, author: impl Into<String>) -> Self {
918        self.author = Some(author.into());
919        self
920    }
921
922    /// Keeps each file's bookmarks, one after the other (on by default).
923    pub fn bookmarks(mut self, bookmarks: bool) -> Self {
924        self.bookmarks = Some(bookmarks);
925        self
926    }
927
928    /// A fixed creation date, which makes the output byte-identical from run to run.
929    pub fn creation_date(mut self, date: SystemTime) -> Self {
930        self.creation_date = Some(date);
931        self
932    }
933
934    /// Stops with [`ErrorKind::Timeout`](crate::ErrorKind::Timeout) after this long.
935    pub fn timeout(mut self, timeout: Duration) -> Self {
936        self.timeout = Some(timeout);
937        self
938    }
939
940    pub(crate) fn to_json(&self) -> Result<String> {
941        let mut o = Object::default();
942        if let Some(v) = &self.title {
943            o.str("title", v);
944        }
945        if let Some(v) = &self.author {
946            o.str("author", v);
947        }
948        if let Some(v) = self.bookmarks {
949            o.bool("bookmarks", v);
950        }
951        if let Some(v) = self.creation_date {
952            o.int("creation_date", unix_seconds(v));
953        }
954        timeout(&mut o, self.timeout)?;
955        Ok(o.finish())
956    }
957}
958
959/// What [`redact_pdf`](crate::redact_pdf) removes, and how the removed places look.
960///
961/// Text, a pattern and areas can be combined; each adds to what is removed.
962#[derive(Debug, Clone, Default)]
963pub struct RedactOptions {
964    text: Option<String>,
965    pattern: Option<String>,
966    areas: Vec<[f64; 5]>,
967    ignore_case: Option<bool>,
968    fill_color: Option<String>,
969    timeout: Option<Duration>,
970}
971
972impl RedactOptions {
973    /// Nothing to remove yet, and black boxes over what is removed.
974    pub fn new() -> Self {
975        Self::default()
976    }
977
978    /// Removes every word that holds this text; a phrase matches across the words of a line.
979    pub fn text(mut self, text: impl Into<String>) -> Self {
980        self.text = Some(text.into());
981        self
982    }
983
984    /// Removes every word that this regular expression matches, in .NET syntax, which agrees with Rust's `regex`
985    /// crate for common patterns.
986    pub fn pattern(mut self, pattern: impl Into<String>) -> Self {
987        self.pattern = Some(pattern.into());
988        self
989    }
990
991    /// Removes everything in a rectangle on a page (numbered from 1), in points from the page's top-left corner.
992    pub fn area(mut self, page: u32, x: f64, y: f64, width: f64, height: f64) -> Self {
993        self.areas.push([f64::from(page), x, y, width, height]);
994        self
995    }
996
997    /// Compares [`text`](Self::text) and [`pattern`](Self::pattern) without regard to case (off by default).
998    pub fn ignore_case(mut self, ignore_case: bool) -> Self {
999        self.ignore_case = Some(ignore_case);
1000        self
1001    }
1002
1003    /// The colour of the boxes drawn over what was removed, as a CSS colour (default black); `"transparent"` draws
1004    /// no box.
1005    pub fn fill_color(mut self, color: impl Into<String>) -> Self {
1006        self.fill_color = Some(color.into());
1007        self
1008    }
1009
1010    /// Stops with [`ErrorKind::Timeout`](crate::ErrorKind::Timeout) after this long.
1011    pub fn timeout(mut self, timeout: Duration) -> Self {
1012        self.timeout = Some(timeout);
1013        self
1014    }
1015
1016    pub(crate) fn to_json(&self) -> Result<String> {
1017        let mut o = Object::default();
1018        if let Some(v) = &self.text {
1019            o.str("text", v);
1020        }
1021        if let Some(v) = &self.pattern {
1022            o.str("pattern", v);
1023        }
1024        if !self.areas.is_empty() {
1025            o.num_lists("areas", &self.areas)?;
1026        }
1027        if let Some(v) = self.ignore_case {
1028            o.bool("ignore_case", v);
1029        }
1030        if let Some(v) = &self.fill_color {
1031            o.str("fill_color", v);
1032        }
1033        timeout(&mut o, self.timeout)?;
1034        Ok(o.finish())
1035    }
1036}
1037
1038/// Options of [`read_barcodes`](crate::read_barcodes).
1039#[derive(Debug, Clone, Default)]
1040pub struct BarcodeOptions {
1041    formats: Vec<BarcodeFormat>,
1042    try_inverted: Option<bool>,
1043    pages: Option<Pages>,
1044    password: Option<String>,
1045    dpi: Option<f64>,
1046    timeout: Option<Duration>,
1047}
1048
1049impl BarcodeOptions {
1050    /// The defaults: every format, light-on-dark codes too, and for a PDF every page at 300 dpi.
1051    pub fn new() -> Self {
1052        Self::default()
1053    }
1054
1055    /// Looks only for these formats (default all).
1056    pub fn formats(mut self, formats: impl IntoIterator<Item = BarcodeFormat>) -> Self {
1057        self.formats = formats.into_iter().collect();
1058        self
1059    }
1060
1061    /// Also looks for light codes on a dark background (on by default).
1062    pub fn try_inverted(mut self, try_inverted: bool) -> Self {
1063        self.try_inverted = Some(try_inverted);
1064        self
1065    }
1066
1067    /// For a PDF, which pages to search (default all).
1068    pub fn pages(mut self, pages: impl Into<Pages>) -> Self {
1069        self.pages = Some(pages.into());
1070        self
1071    }
1072
1073    /// For a PDF, its password.
1074    pub fn password(mut self, password: impl Into<String>) -> Self {
1075        self.password = Some(password.into());
1076        self
1077    }
1078
1079    /// For a PDF, the resolution pages are rendered at (default 300).
1080    pub fn dpi(mut self, dpi: f64) -> Self {
1081        self.dpi = Some(dpi);
1082        self
1083    }
1084
1085    /// Stops with [`ErrorKind::Timeout`](crate::ErrorKind::Timeout) after this long.
1086    pub fn timeout(mut self, timeout: Duration) -> Self {
1087        self.timeout = Some(timeout);
1088        self
1089    }
1090
1091    pub(crate) fn to_json(&self) -> Result<String> {
1092        let mut o = Object::default();
1093        if !self.formats.is_empty() {
1094            let names: Vec<String> = self.formats.iter().map(|f| f.name().to_string()).collect();
1095            o.strs("formats", &names);
1096        }
1097        if let Some(v) = self.try_inverted {
1098            o.bool("try_inverted", v);
1099        }
1100        if let Some(v) = &self.pages {
1101            o.str("pages", &v.0);
1102        }
1103        if let Some(v) = &self.password {
1104            o.str("password", v);
1105        }
1106        if let Some(v) = self.dpi {
1107            o.num("dpi", v)?;
1108        }
1109        timeout(&mut o, self.timeout)?;
1110        Ok(o.finish())
1111    }
1112}
1113
1114#[cfg(test)]
1115mod tests {
1116    use super::*;
1117
1118    #[test]
1119    fn option_names_are_the_native_ones() {
1120        let json = PdfOptions::new()
1121            .page_size(PageSize::Millimeters {
1122                width: 100.0,
1123                height: 50.0,
1124            })
1125            .margin(Margin::new(1.0, 2.0, 3.0, 4.0))
1126            .footer_text("Page {page}")
1127            .creation_date(UNIX_EPOCH + Duration::from_secs(5))
1128            .conformance(Conformance::PdfA3u)
1129            .timeout(Duration::from_millis(1500))
1130            .to_json()
1131            .unwrap();
1132        assert_eq!(
1133            json,
1134            r#"{"page_size":[100,50],"margin":[1,2,3,4],"footer_text":"Page {page}","creation_date":5,"conformance":"PDF/A-3u","timeout":1.5}"#
1135        );
1136        assert_eq!(PdfOptions::new().to_json().unwrap(), "{}");
1137        assert_eq!(
1138            ImageOptions::new().pages([1, 3]).to_json().unwrap(),
1139            r#"{"pages":"1,3"}"#
1140        );
1141        assert_eq!(
1142            TextOptions::new().pages(2..=4).to_json().unwrap(),
1143            r#"{"pages":"2-4"}"#
1144        );
1145        assert_eq!(
1146            TextOptions::new()
1147                .order(TextOrder::TopToBottom)
1148                .to_json()
1149                .unwrap(),
1150            r#"{"order":"top_to_bottom"}"#
1151        );
1152        assert_eq!(
1153            RedactOptions::new()
1154                .text("secret")
1155                .area(2, 10.0, 20.5, 30.0, 40.0)
1156                .fill_color("transparent")
1157                .to_json()
1158                .unwrap(),
1159            r#"{"text":"secret","areas":[[2,10,20.5,30,40]],"fill_color":"transparent"}"#
1160        );
1161        assert_eq!(
1162            BarcodeOptions::new()
1163                .formats([BarcodeFormat::QrCode, BarcodeFormat::Ean13])
1164                .try_inverted(false)
1165                .to_json()
1166                .unwrap(),
1167            r#"{"formats":["qr_code","ean13"],"try_inverted":false}"#
1168        );
1169        assert_eq!(unix_seconds(UNIX_EPOCH - Duration::from_millis(1500)), -2);
1170    }
1171}