use std::collections::HashSet;
use zpdf_writer::linearize_pdf;
fn multipage_source() -> Vec<u8> {
let mut b = zpdf_writer::builder::DocumentBuilder::new();
for i in 0..4 {
let p = b.add_page(612.0, 792.0);
b.add_text(
p,
&format!("page {}", i + 1),
50.0,
700.0,
"Helvetica",
12.0,
(0.0, 0.0, 0.0),
)
.unwrap();
}
b.build().unwrap()
}
fn body_objects(bytes: &[u8]) -> HashSet<u32> {
let text = String::from_utf8_lossy(bytes);
let mut out = HashSet::new();
for line in text.lines() {
let mut it = line.split_whitespace();
if let (Some(n), Some("0"), Some("obj")) = (it.next(), it.next(), it.next()) {
if let Ok(num) = n.parse::<u32>() {
out.insert(num);
}
}
}
out
}
fn xref_covered(bytes: &[u8]) -> HashSet<u32> {
let text = String::from_utf8_lossy(bytes);
let mut out = HashSet::new();
let mut lines = text.lines().peekable();
while let Some(line) = lines.next() {
if line.trim() != "xref" {
continue;
}
while let Some(peek) = lines.peek() {
let parts: Vec<&str> = peek.split_whitespace().collect();
if parts.len() == 2 {
if let (Ok(start), Ok(count)) = (parts[0].parse::<u32>(), parts[1].parse::<u32>()) {
lines.next();
for k in 0..count {
out.insert(start + k);
lines.next(); }
continue;
}
}
break;
}
}
out
}
fn trailer_size(bytes: &[u8]) -> Option<u32> {
let text = String::from_utf8_lossy(bytes);
let i = text.rfind("/Size")?;
text[i + 5..]
.split_whitespace()
.next()?
.trim_end_matches(|c: char| !c.is_ascii_digit())
.parse()
.ok()
}
#[test]
fn every_body_object_is_covered_by_a_cross_reference_table() {
let out = linearize_pdf(&zpdf_parser::PdfFile::parse(multipage_source()).unwrap()).unwrap();
let body = body_objects(&out);
let covered = xref_covered(&out);
assert!(!body.is_empty(), "no objects were written at all");
let missing: Vec<u32> = {
let mut v: Vec<u32> = body.difference(&covered).copied().collect();
v.sort_unstable();
v
};
assert!(
missing.is_empty(),
"objects exist in the body but in no xref table: {missing:?} \
(body={} objects, covered={} entries)",
body.len(),
covered.len(),
);
}
#[test]
fn trailer_size_matches_the_highest_object_number() {
let out = linearize_pdf(&zpdf_parser::PdfFile::parse(multipage_source()).unwrap()).unwrap();
let highest = *body_objects(&out).iter().max().unwrap();
let size = trailer_size(&out).expect("no /Size in any trailer");
assert_eq!(
size,
highest + 1,
"/Size must be one more than the highest object number \
(qpdf: \"reported number of objects is not one plus the highest object number\")"
);
}
#[test]
fn the_main_trailer_names_the_catalog() {
let out = linearize_pdf(&zpdf_parser::PdfFile::parse(multipage_source()).unwrap()).unwrap();
let text = String::from_utf8_lossy(&out);
let main_trailer = text.rfind("trailer").expect("no trailer");
assert!(
text[main_trailer..].contains("/Root"),
"the last trailer must carry /Root: {}",
&text[main_trailer..(main_trailer + 80).min(text.len())]
);
}