Skip to main content

pdfrum_edit/
blank.rs

1//! A new document with nothing on it, for a caller that writes a PDF from
2//! scratch rather than editing one.
3//!
4//! [`EditDoc`](crate::EditDoc) edits a parsed base, so a fresh file starts as
5//! a base too: a catalog, a page tree and one empty page per size asked for,
6//! written here as bytes and read back by the parser. Drawing then goes
7//! through [`EditDoc::draw_pages`](crate::EditDoc::draw_pages) like any other
8//! page, and a full save writes the result.
9
10use std::fmt::Write as _;
11use std::sync::Arc;
12
13use kurbo::Size;
14use pdfrum_parser::{Document, LoadOptions, load};
15
16use crate::Error;
17
18/// A document of blank pages, one per entry of `pages`, each `width` by
19/// `height` points and in that order.
20///
21/// Every page has only `/Type`, `/Parent` and `/MediaBox`, so a canvas drawn
22/// on it sees exactly that box as its space. An empty `pages` is a document
23/// with an empty page tree.
24///
25/// # Errors
26///
27/// [`Error::BadPageSize`] when a size is not finite and positive: a page with
28/// no area has no space to draw in.
29///
30/// ```
31/// use pdfrum_edit::{Size, blank_document};
32///
33/// let doc = blank_document(&[Size::new(595.0, 842.0), Size::new(612.0, 792.0)])?;
34/// assert_eq!(doc.page_count(), 2);
35/// # Ok::<(), pdfrum_edit::Error>(())
36/// ```
37pub fn blank_document(pages: &[Size]) -> Result<Document, Error> {
38    if let Some(bad) = pages.iter().find(|size| {
39        !(size.width.is_finite()
40            && size.height.is_finite()
41            && size.width > 0.0
42            && size.height > 0.0)
43    }) {
44        return Err(Error::BadPageSize(*bad));
45    }
46    load(Arc::from(file(pages).into_bytes()), &LoadOptions::default())
47        .map_err(|error| Error::BlankDocument(error.to_string()))
48}
49
50/// The file: catalog (1), page tree (2), then the pages from 3.
51fn file(pages: &[Size]) -> String {
52    let kids: Vec<String> = (0..pages.len()).map(|i| format!("{} 0 R", i + 3)).collect();
53    let mut objects = vec![
54        "<< /Type /Catalog /Pages 2 0 R >>".to_owned(),
55        format!(
56            "<< /Type /Pages /Count {} /Kids [{}] >>",
57            pages.len(),
58            kids.join(" ")
59        ),
60    ];
61    objects.extend(pages.iter().map(|size| {
62        format!(
63            "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {} {}] >>",
64            pdfrum_object::fmt_number(narrow(size.width)),
65            pdfrum_object::fmt_number(narrow(size.height))
66        )
67    }));
68    let mut out = String::from("%PDF-1.7\n%\u{e2}\u{e3}\u{cf}\u{d3}\n");
69    let mut offsets = Vec::with_capacity(objects.len());
70    for (i, body) in objects.iter().enumerate() {
71        offsets.push(out.len());
72        let _ = writeln!(out, "{} 0 obj\n{body}\nendobj", i + 1);
73    }
74    let xref = out.len();
75    let _ = writeln!(out, "xref\n0 {}\n0000000000 65535 f ", objects.len() + 1);
76    for offset in offsets {
77        let _ = writeln!(out, "{offset:010} 00000 n ");
78    }
79    let _ = writeln!(
80        out,
81        "trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF",
82        objects.len() + 1
83    );
84    out
85}
86
87#[expect(
88    clippy::cast_possible_truncation,
89    reason = "PDF numbers are f32; the geometry vocabulary is f64"
90)]
91fn narrow(value: f64) -> f32 {
92    value as f32
93}
94
95#[cfg(test)]
96mod tests {
97    use super::blank_document;
98    use crate::Error;
99    use kurbo::Size;
100
101    #[test]
102    fn every_size_becomes_a_page_in_order() {
103        let doc = blank_document(&[Size::new(100.0, 200.0), Size::new(300.5, 50.0)])
104            .expect("a blank document parses");
105        assert_eq!(doc.page_count(), 2);
106    }
107
108    #[test]
109    fn a_page_without_area_is_refused() {
110        for size in [
111            Size::new(0.0, 10.0),
112            Size::new(10.0, -1.0),
113            Size::new(f64::NAN, 10.0),
114        ] {
115            assert!(
116                matches!(blank_document(&[size]), Err(Error::BadPageSize(_))),
117                "{size:?}"
118            );
119        }
120    }
121
122    #[test]
123    fn no_pages_is_an_empty_tree() {
124        let doc = blank_document(&[]).expect("an empty tree parses");
125        assert_eq!(doc.page_count(), 0);
126    }
127}