1use std::fmt::Write as _;
11use std::sync::Arc;
12
13use kurbo::Size;
14use pdfrum_parser::{Document, LoadOptions, load};
15
16use crate::Error;
17
18pub 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
50fn 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}