use std::fmt::Write as _;
use std::sync::Arc;
use kurbo::Size;
use pdfrum_parser::{Document, LoadOptions, load};
use crate::Error;
pub fn blank_document(pages: &[Size]) -> Result<Document, Error> {
if let Some(bad) = pages.iter().find(|size| {
!(size.width.is_finite()
&& size.height.is_finite()
&& size.width > 0.0
&& size.height > 0.0)
}) {
return Err(Error::BadPageSize(*bad));
}
load(Arc::from(file(pages).into_bytes()), &LoadOptions::default())
.map_err(|error| Error::BlankDocument(error.to_string()))
}
fn file(pages: &[Size]) -> String {
let kids: Vec<String> = (0..pages.len()).map(|i| format!("{} 0 R", i + 3)).collect();
let mut objects = vec![
"<< /Type /Catalog /Pages 2 0 R >>".to_owned(),
format!(
"<< /Type /Pages /Count {} /Kids [{}] >>",
pages.len(),
kids.join(" ")
),
];
objects.extend(pages.iter().map(|size| {
format!(
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {} {}] >>",
pdfrum_object::fmt_number(narrow(size.width)),
pdfrum_object::fmt_number(narrow(size.height))
)
}));
let mut out = String::from("%PDF-1.7\n%\u{e2}\u{e3}\u{cf}\u{d3}\n");
let mut offsets = Vec::with_capacity(objects.len());
for (i, body) in objects.iter().enumerate() {
offsets.push(out.len());
let _ = writeln!(out, "{} 0 obj\n{body}\nendobj", i + 1);
}
let xref = out.len();
let _ = writeln!(out, "xref\n0 {}\n0000000000 65535 f ", objects.len() + 1);
for offset in offsets {
let _ = writeln!(out, "{offset:010} 00000 n ");
}
let _ = writeln!(
out,
"trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF",
objects.len() + 1
);
out
}
#[expect(
clippy::cast_possible_truncation,
reason = "PDF numbers are f32; the geometry vocabulary is f64"
)]
fn narrow(value: f64) -> f32 {
value as f32
}
#[cfg(test)]
mod tests {
use super::blank_document;
use crate::Error;
use kurbo::Size;
#[test]
fn every_size_becomes_a_page_in_order() {
let doc = blank_document(&[Size::new(100.0, 200.0), Size::new(300.5, 50.0)])
.expect("a blank document parses");
assert_eq!(doc.page_count(), 2);
}
#[test]
fn a_page_without_area_is_refused() {
for size in [
Size::new(0.0, 10.0),
Size::new(10.0, -1.0),
Size::new(f64::NAN, 10.0),
] {
assert!(
matches!(blank_document(&[size]), Err(Error::BadPageSize(_))),
"{size:?}"
);
}
}
#[test]
fn no_pages_is_an_empty_tree() {
let doc = blank_document(&[]).expect("an empty tree parses");
assert_eq!(doc.page_count(), 0);
}
}