use pdfrum::{Document, FlattenMode, Flattened, RenderOptions, SaveOptions, VelloCpuBackend};
use std::sync::Arc;
fn open(name: &str) -> pdfrum::Result<Document> {
Document::open(format!("tests/fixtures/{name}.pdf"))
}
fn saved(edit: &pdfrum::DocEdit<'_>) -> pdfrum::Result<(Vec<u8>, Document)> {
let mut out = Vec::new();
edit.write_to(&mut out, &SaveOptions::default())?;
let doc = Document::from_bytes(Arc::from(out.clone()))?;
Ok((out, doc))
}
fn with_annotations() -> RenderOptions {
let mut options = RenderOptions::default();
options.annotations = true;
options
}
fn flattened_matches(name: &str, expected: &str) -> Result<(), Box<dyn std::error::Error>> {
let doc = open(name)?;
let mut edit = doc.edit();
assert_eq!(edit.flatten(0, FlattenMode::Print)?, Flattened::Done);
let (_, flat) = saved(&edit)?;
let plain = flat
.page(0)?
.render(&VelloCpuBackend::new(), &RenderOptions::default())?;
let (width, height, rgba) = expected_rgba(expected)?;
assert_eq!(
(plain.width(), plain.height()),
(width, height),
"{name} size"
);
let differing = plain
.data()
.iter()
.zip(&rgba)
.filter(|(a, b)| a != b)
.count();
assert!(
differing * 200 <= rgba.len(),
"{name}: {differing} of {} bytes differ from {expected}",
rgba.len()
);
Ok(())
}
#[test]
fn a_page_without_annotations_has_nothing_to_do() {
let doc = open("hello_world").unwrap();
assert_eq!(
doc.edit().flatten(0, FlattenMode::Display).unwrap(),
Flattened::NothingToDo
);
}
#[test]
fn a_page_with_annotations_flattens_for_display_and_for_print() {
let doc = open("annotiter").unwrap();
assert_eq!(
doc.edit().flatten(0, FlattenMode::Display).unwrap(),
Flattened::Done
);
assert_eq!(
doc.edit().flatten(0, FlattenMode::Print).unwrap(),
Flattened::Done
);
}
#[test]
fn a_font_with_a_bad_base_encoding_loses_its_encoding() {
let doc = open("344775293").unwrap();
let mut edit = doc.edit();
assert_eq!(
edit.flatten(0, FlattenMode::Print).unwrap(),
Flattened::Done
);
let (bytes, _) = saved(&edit).unwrap();
let text = String::from_utf8_lossy(&bytes);
assert!(!text.contains("/PDFDocEncoding"));
}
#[test]
fn a_font_with_differences_and_no_base_encoding_keeps_them() {
let doc = open("363015187").unwrap();
let mut edit = doc.edit();
assert_eq!(
edit.flatten(0, FlattenMode::Print).unwrap(),
Flattened::Done
);
let (bytes, _) = saved(&edit).unwrap();
assert!(String::from_utf8_lossy(&bytes).contains("/Differences"));
}
fn expected_rgba(name: &str) -> Result<(u32, u32, Vec<u8>), Box<dyn std::error::Error>> {
let file = std::fs::File::open(format!("tests/fixtures/{name}.png"))?;
let mut decoder = png::Decoder::new(std::io::BufReader::new(file));
decoder.set_transformations(png::Transformations::EXPAND);
let mut reader = decoder.read_info()?;
let mut buffer = vec![0; reader.output_buffer_size().ok_or("no buffer size")?];
let info = reader.next_frame(&mut buffer)?;
buffer.truncate(info.buffer_size());
let rgba = match info.color_type {
png::ColorType::Rgb => buffer
.as_chunks::<3>()
.0
.iter()
.flat_map(|&[r, g, b]| [r, g, b, 255])
.collect(),
png::ColorType::Rgba => buffer,
png::ColorType::Grayscale => buffer.iter().flat_map(|&g| [g, g, g, 255]).collect(),
png::ColorType::GrayscaleAlpha => buffer
.as_chunks::<2>()
.0
.iter()
.flat_map(|&[v, a]| [v, v, v, a])
.collect(),
png::ColorType::Indexed => return Err(format!("{name}: an indexed PNG").into()),
};
Ok((info.width, info.height, rgba))
}
fn dark_pixels(pixmap: &pdfrum::Pixmap) -> Vec<usize> {
pixmap
.data()
.as_chunks::<4>()
.0
.iter()
.enumerate()
.filter(|(_, p)| u32::from(p[0]) + u32::from(p[1]) + u32::from(p[2]) < 384)
.map(|(i, _)| i)
.collect()
}
#[test]
fn the_flattened_page_draws_what_the_annotated_page_drew() {
for name in ["bug_890322", "bug_896366"] {
flattened_matches(name, &format!("{name}_agg")).unwrap();
}
}
#[test]
fn a_malformed_media_box_flattens_to_the_oracles_picture() {
flattened_matches("bug_889099", "bug_889099_flattened_agg").unwrap_or_else(|e| panic!("{e}"));
}
#[test]
fn a_flattened_page_is_not_blank() {
let doc = open("bug_861842").unwrap();
let annotated = doc
.page(0)
.unwrap()
.render(&VelloCpuBackend::new(), &with_annotations())
.unwrap();
let mut edit = doc.edit();
assert_eq!(
edit.flatten(0, FlattenMode::Print).unwrap(),
Flattened::Done
);
let (_, flat) = saved(&edit).unwrap();
let plain = flat
.page(0)
.unwrap()
.render(&VelloCpuBackend::new(), &RenderOptions::default())
.unwrap();
let before = dark_pixels(&annotated);
let after = dark_pixels(&plain);
assert!(!after.is_empty(), "the flattened page is blank");
let shared = after.iter().filter(|p| before.contains(p)).count();
assert!(
shared * 10 >= before.len() * 9,
"{shared} of {} ink pixels kept",
before.len()
);
assert!(
after.len() * 10 <= before.len() * 15,
"{} ink pixels from {}",
after.len(),
before.len()
);
}
#[test]
fn flattening_a_forms_only_page_removes_the_form() {
let doc = open("text_form").unwrap();
let mut edit = doc.edit();
assert_eq!(
edit.flatten(0, FlattenMode::Display).unwrap(),
Flattened::Done
);
let (_, flat) = saved(&edit).unwrap();
assert!(flat.form().is_none());
}
#[test]
fn a_widget_another_page_shares_keeps_the_form_until_that_page_goes_too() {
for name in ["bug_498010830_shared_annots", "bug_498010830_shared_widget"] {
let doc = open(name).unwrap();
let mut edit = doc.edit();
assert_eq!(
edit.flatten(0, FlattenMode::Display).unwrap(),
Flattened::Done
);
let (_, flat) = saved(&edit).unwrap();
assert!(flat.form().is_some(), "{name}: the form survives page 0");
assert_eq!(
edit.flatten(1, FlattenMode::Display).unwrap(),
Flattened::Done
);
let (_, flat) = saved(&edit).unwrap();
assert!(flat.form().is_none(), "{name}: the form goes with page 1");
}
}