Files
ElyOffice/crates/ely-pdf-engine/tests/output.rs
T
ZacharyZhang-NYandClaude 6f1eebb4b5 R8.1: document content keeps the file's colours in a dark app
Pages, cells, slides and PDF pages paint as the file says in any
appearance; only the chrome around them follows dark mode. The
kernel keeps Ely's one light scheme, sent once a view, and MuPDF
no longer inverts. Reviews now run gpt-6.1-sol at xhigh.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-18 19:42:13 -04:00

357 lines
18 KiB
Rust

//! Images, text, optimized copies, printing leave, repaired files (C11.8).
use std::path::Path;
use ely_pdf_engine::{
Doc, Lock, Optimizing, Worker,
proto::may,
sample::{Extras, one_page, pdf_with},
};
fn worker() -> Worker {
Worker::spawn_from(Path::new(env!("CARGO_BIN_EXE_ely-pdf-worker"))).expect("the worker")
}
fn opened(path: &Path) -> (Worker, Doc) {
let mut worker = worker();
let doc = worker.open(path).expect("opens").doc;
(worker, doc)
}
fn two_pages(dir: &Path) -> std::path::PathBuf {
let path = dir.join("doc.pdf");
std::fs::write(&path, pdf_with(&[(612.0, 792.0, &["First page"]), (612.0, 792.0, &["Second page"])], &[], &Extras::default())).expect("writes");
path
}
#[test]
fn a_page_exports_as_a_png_of_its_size() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = opened(&two_pages(dir.path()));
let png = dir.path().join("page.png");
worker.export_image(doc, 1, 144.0, &png).expect("exports");
let bytes = std::fs::read(&png).expect("reads");
assert_eq!(&bytes[1..4], b"PNG");
let size = |at: usize| u32::from_be_bytes(bytes[at..at + 4].try_into().expect("four bytes"));
assert_eq!((size(16), size(20)), (1224, 1584), "twice the points");
assert!(worker.export_image(doc, 0, 5000.0, &png).is_err(), "an absurd resolution");
}
#[test]
fn text_exports_page_by_page() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = opened(&two_pages(dir.path()));
let txt = dir.path().join("doc.txt");
worker.export_text(doc, &txt).expect("exports");
let text = std::fs::read_to_string(&txt).expect("UTF-8");
let pages: Vec<&str> = text.split('\u{c}').collect();
assert_eq!(pages.len(), 2);
assert!(pages[0].contains("First page") && pages[1].contains("Second page"), "{text:?}");
}
/// A 600-pixel picture drawn 100 points wide: 432 dpi.
fn heavy(dir: &Path) -> std::path::PathBuf {
let pixels = vec![120u8; 600 * 600 * 3];
let mut picture = format!("<< /Type /XObject /Subtype /Image /Width 600 /Height 600 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Length {} >>\nstream\n", pixels.len()).into_bytes();
picture.extend_from_slice(&pixels);
picture.extend_from_slice(b"\nendstream");
let path = dir.join("heavy.pdf");
let mut bytes = one_page("", " /Resources << /XObject << /Im 5 0 R >> >>", "q 100 0 0 100 72 600 cm /Im Do Q", &["PICTURE".to_string()]);
let at = bytes.windows(7).position(|w| w == b"PICTURE").expect("the slot");
bytes.splice(at..at + 7, picture);
std::fs::write(&path, &bytes).expect("writes");
path
}
#[test]
fn an_optimized_copy_is_smaller_and_its_picture_downsampled() {
let dir = tempfile::tempdir().expect("a temp dir");
let source = heavy(dir.path());
let (mut worker, doc) = opened(&source);
let out = dir.path().join("small.pdf");
worker.optimize(doc, &out, Optimizing { compress: true, from: 300, to: 150 }).expect("optimizes");
let (before, after) = (std::fs::metadata(&source).expect("size").len(), std::fs::metadata(&out).expect("size").len());
assert!(after * 10 < before, "{before} -> {after}");
let mut other = self::worker();
let read = other.open(&out).expect("opens").doc;
assert_eq!(other.pages(read).expect("pages"), 1);
let text = String::from_utf8_lossy(&std::fs::read(&out).expect("reads")).into_owned();
let width: u32 = text.split("/Width ").nth(1).and_then(|rest| rest.split(|c: char| !c.is_ascii_digit()).next()).and_then(|n| n.parse().ok()).expect("a width");
assert!((205..=212).contains(&width), "150 dpi over 100 points, not {width}");
assert!(worker.optimize(doc, &out, Optimizing { compress: true, from: 150, to: 300 }).is_err(), "up is not down");
}
#[test]
fn printing_and_copying_need_leave() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = opened(&two_pages(dir.path()));
let limited = dir.path().join("limited.pdf");
worker.protect(doc, &limited, Some(&Lock { user: String::new(), owner: "owner".into(), allow: may::EDIT })).expect("protects");
let (mut other, read) = opened(&limited);
for refused in [other.print_copy(read, &dir.path().join("p.pdf")), other.export_text(read, &dir.path().join("t.txt"))] {
let refused = refused.expect_err("refused");
assert!(ely_pdf_engine::refused(&refused).is_some(), "refused: {refused}");
}
worker.print_copy(doc, &dir.path().join("print.pdf")).expect("an unprotected file prints");
}
#[test]
fn a_damaged_file_opens_as_a_repaired_copy_and_stays_as_it_was() {
let dir = tempfile::tempdir().expect("a temp dir");
let path = two_pages(dir.path());
let mut bytes = std::fs::read(&path).expect("reads");
// Breaks every cross-reference offset.
let at = bytes.windows(9).rposition(|w| w == b"startxref").expect("the trailer");
bytes.truncate(at);
bytes.extend_from_slice(b"startxref\n9\n%%EOF\n");
std::fs::write(&path, &bytes).expect("writes");
let (mut worker, mut doc) = opened(&path);
let report = worker.repair_report(doc).expect("asks").expect("repaired");
assert!(!report.trim().is_empty(), "MuPDF says what it found");
assert_eq!(worker.pages(doc).expect("pages"), 2);
worker.rotate_pages(doc, &[0], 90).expect("edits the copy");
doc = worker.now(doc).expect("the state");
worker.save(doc, &dir.path().join("fixed.pdf")).expect("saves the copy");
assert_eq!(std::fs::read(&path).expect("reads"), bytes, "the damaged original stays");
let elsewhere = tempfile::tempdir().expect("a temp dir");
let (mut fresh, clean) = opened(&two_pages(elsewhere.path()));
assert_eq!(fresh.repair_report(clean).expect("asks"), None);
}
#[test]
fn a_broken_object_with_a_sound_trailer_is_repaired_on_open() {
let dir = tempfile::tempdir().expect("a temp dir");
let path = two_pages(dir.path());
let mut bytes = std::fs::read(&path).expect("reads");
// The page tree's offset points nowhere; startxref is sound.
let table = bytes.windows(6).position(|w| w == b"\nxref\n").expect("the table") + 6;
let entries = table + bytes[table..].iter().position(|b| *b == b'\n').expect("a header") + 1;
let at = entries + 2 * 20;
bytes[at..at + 10].copy_from_slice(b"0000000009");
std::fs::write(&path, &bytes).expect("writes");
let (mut worker, mut doc) = opened(&path);
assert!(worker.repair_report(doc).expect("asks").is_some(), "found at open, not mid-edit");
worker.rotate_pages(doc, &[0], 90).expect("edits");
doc = worker.now(doc).expect("the state");
worker.save(doc, &dir.path().join("fixed.pdf")).expect("saves");
assert_eq!(std::fs::read(&path).expect("reads"), bytes);
}
#[test]
fn a_broken_stream_under_a_sound_table_is_damage_and_fails_exports() {
let dir = tempfile::tempdir().expect("a temp dir");
let broken = "<< /Length 12 /Filter /FlateDecode >>\nstream\nnot a zlib!!\nendstream".to_string();
let path = dir.path().join("doc.pdf");
let bytes = one_page("", " /Contents 5 0 R", "", &[broken]);
std::fs::write(&path, &bytes).expect("writes");
let (mut worker, mut doc) = opened(&path);
let report = worker.repair_report(doc).expect("asks").expect("damage is reported");
assert!(!report.trim().is_empty());
assert!(worker.export_text(doc, &dir.path().join("doc.txt")).is_err(), "partial text is no export");
assert!(worker.export_image(doc, 0, 72.0, &dir.path().join("page.png")).is_err(), "nor a partial page");
worker.rotate_pages(doc, &[0], 90).expect("edits the copy");
doc = worker.now(doc).expect("the state");
worker.save(doc, &dir.path().join("fixed.pdf")).expect("saves the copy");
assert_eq!(std::fs::read(&path).expect("reads"), bytes, "the original stays");
}
#[test]
fn a_print_copy_carries_no_lock() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = opened(&two_pages(dir.path()));
let locked = dir.path().join("locked.pdf");
worker.protect(doc, &locked, Some(&Lock { user: "user".into(), owner: "owner".into(), allow: may::PRINT })).expect("protects");
let (mut other, read) = opened(&locked);
assert!(other.unlock(read, "user").expect("tries"));
let print = dir.path().join("print.pdf");
other.print_copy(read, &print).expect("writes");
let mut third = self::worker();
let opened = third.open(&print).expect("opens");
assert!(!opened.locked, "the printer needs no password");
assert_eq!(third.pages(opened.doc).expect("pages"), 2);
}
#[test]
fn a_print_copy_shows_what_prints_to_any_screen_renderer() {
let dir = tempfile::tempdir().expect("a temp dir");
// No looks of their own: MuPDF makes them.
let square = |flags: u8, rect: &str, color: &str| format!("<< /Type /Annot /Subtype /Square /F {flags} /Rect [{rect}] /C [{color}] /IC [{color}] >>");
let annots = [
square(4, "50 100 150 180", "1 0 0"),
square(0, "200 100 300 180", "0 1 0"),
// Print and NoView: printed, never shown.
square(36, "350 100 450 180", "0 0 1"),
// Print and Hidden: neither.
square(6, "50 300 150 380", "0 1 0"),
"<< /Type /Annot /Subtype /Link /F 4 /Rect [200 300 300 380] /A << /S /URI /URI (https://example.com) >> >>".to_string(),
];
let path = dir.path().join("doc.pdf");
std::fs::write(&path, one_page("", " /Annots [5 0 R 6 0 R 7 0 R 8 0 R 9 0 R]", "", &annots)).expect("writes");
let (mut worker, doc) = opened(&path);
let print = dir.path().join("print.pdf");
worker.print_copy(doc, &print).expect("writes");
// Only the link stays an annotation.
let copy = lopdf::Document::load(&print).expect("a PDF");
let left = copy.get_dictionary(copy.get_pages()[&1]).and_then(|page| page.get(b"Annots")).and_then(lopdf::Object::as_array).expect("annotations");
let kinds: Vec<&[u8]> = left.iter().map(|annot| copy.dereference(annot).and_then(|(_, a)| a.as_dict()?.get(b"Subtype")?.as_name()).expect("a kind")).collect();
assert_eq!(kinds, [b"Link".as_slice()]);
assert!(copy.catalog().expect("a catalog").get(b"AcroForm").is_err(), "no form left");
// MuPDF draws by screen rules, as Windows' renderer does.
let mut third = self::worker();
let printed = third.open(&print).expect("opens").doc;
let bgra = third.render(printed, 0, (1.0, 0), [0, 0, 612, 792]).expect("draws").bgra;
let at = |x: usize, y: usize| -> [u8; 3] {
let p = &bgra[((792 - y) * 612 + x) * 4..][..3];
[p[2], p[1], p[0]]
};
assert_eq!(at(100, 140), [255, 0, 0], "printable");
assert_eq!(at(250, 140), [255, 255, 255], "not for print");
assert_eq!(at(400, 140), [0, 0, 255], "print, not view");
assert_eq!(at(100, 340), [255, 255, 255], "hidden");
}
#[test]
fn a_broken_picture_shows_when_drawn_or_checked() {
let dir = tempfile::tempdir().expect("a temp dir");
let picture = "<< /Type /XObject /Subtype /Image /Width 4 /Height 4 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /FlateDecode /Length 12 >>\nstream\nnot a zlib!!\nendstream".to_string();
let path = dir.path().join("doc.pdf");
let bytes = one_page("", " /Resources << /XObject << /Im 5 0 R >> >>", "q 100 0 0 100 72 600 cm /Im Do Q", &[picture]);
std::fs::write(&path, &bytes).expect("writes");
let (mut worker, doc) = opened(&path);
assert_eq!(worker.repair_report(doc).expect("asks"), None, "pictures wait to be drawn");
let tile = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792]).expect("draws");
assert!(tile.damaged, "drawing finds it");
assert!(worker.repair_report(doc).expect("asks").is_some());
let (mut other, again) = opened(&path);
assert!(other.check_pictures(again).expect("checks"), "the check finds it undrawn");
assert_eq!(std::fs::read(&path).expect("reads"), bytes);
}
/// As many Chinese files name their fonts.
const SIMSUN: &str = "<< /Type /Font /Subtype /Type0 /BaseFont /SimSun /Encoding /Identity-H /DescendantFonts [<< /Type /Font /Subtype /CIDFontType2 /BaseFont /SimSun /CIDSystemInfo << /Registry (Adobe) /Ordering (GB1) /Supplement 2 >> /FontDescriptor << /Type /FontDescriptor /FontName /SimSun /Flags 6 /FontBBox [0 -141 996 855] /ItalicAngle 0 /Ascent 859 /Descent -141 /CapHeight 859 /StemV 91 >> >>] >>";
/// Two runs, each in its own unembedded font.
const TWO_RUNS: &str = "BT /F1 24 Tf 72 700 Td <00410042> Tj /F2 24 Tf <0043> Tj ET";
#[test]
fn an_unembedded_font_is_no_damage() {
let dir = tempfile::tempdir().expect("a temp dir");
let path = dir.path().join("doc.pdf");
// Two fonts, one notice: a repeat is no damage either.
let fonts = " /Resources << /Font << /F1 5 0 R /F2 6 0 R >> >>";
let bytes = one_page("", fonts, TWO_RUNS, &[SIMSUN.to_string(), SIMSUN.to_string()]);
std::fs::write(&path, &bytes).expect("writes");
// Each on a fresh worker: no font loaded before.
let (mut worker, doc) = opened(&path);
worker.export_text(doc, &dir.path().join("doc.txt")).expect("the text exports");
let (mut worker, doc) = opened(&path);
worker.export_image(doc, 0, 72.0, &dir.path().join("page.png")).expect("the page exports");
let (mut worker, doc) = opened(&path);
// Read unwatched first: its repeat is pending.
worker.text(doc, 0).expect("reads");
let tile = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792]).expect("draws");
assert!(!tile.damaged, "a substituted font draws whole");
assert_eq!(worker.repair_report(doc).expect("asks"), None);
}
#[test]
fn unreadable_content_still_draws_as_damage() {
let dir = tempfile::tempdir().expect("a temp dir");
// Text with no font; a stray bracket.
for content in ["BT 72 700 Td (Missing text) Tj ET", "BT ] ET"] {
let path = dir.path().join("doc.pdf");
std::fs::write(&path, one_page("", "", content, &[])).expect("writes");
let (mut worker, doc) = opened(&path);
let tile = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792]).expect("draws");
assert!(tile.damaged, "{content} draws as damage");
}
}
/// Two pictures whose zlib checksums are wrong.
fn broken_pictures(dir: &Path) -> std::path::PathBuf {
let hex = format!("{}>", "ff0000".repeat(16));
// One stored deflate block, its checksum wrong.
let mut zlib = vec![0x78, 0x01, 0x01];
zlib.extend_from_slice(&(hex.len() as u16).to_le_bytes());
zlib.extend_from_slice(&(!(hex.len() as u16)).to_le_bytes());
zlib.extend_from_slice(hex.as_bytes());
zlib.extend_from_slice(&[0, 0, 0, 0]);
let slot = "#".repeat(zlib.len());
let image = format!("<< /Type /XObject /Subtype /Image /Width 4 /Height 4 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter [/FlateDecode /ASCIIHexDecode] /Length {} >>
stream
{slot}
endstream", zlib.len());
let names = " /Resources << /XObject << /Im1 5 0 R /Im2 6 0 R >> >>";
let mut bytes = one_page("", names, "q 100 0 0 100 72 600 cm /Im1 Do Q q 100 0 0 100 72 400 cm /Im2 Do Q", &[image.clone(), image]);
for _ in 0..2 {
let at = bytes.windows(slot.len()).position(|w| w == slot.as_bytes()).expect("the slot");
bytes[at..at + zlib.len()].copy_from_slice(&zlib);
}
let path = dir.join("doc.pdf");
std::fs::write(&path, &bytes).expect("writes");
path
}
/// A picture whose colour key mask is invalid.
fn masked() -> String {
let hex = format!("{}>", "ff0000".repeat(16));
format!("<< /Type /XObject /Subtype /Image /Width 4 /Height 4 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Mask [null null null null null null] /Filter /ASCIIHexDecode /Length {} >>
stream
{hex}
endstream", hex.len())
}
/// The bad mask, alone or drawn beside two font notices.
fn bad_mask(dir: &Path, fonts: Option<bool>) -> std::path::PathBuf {
let picture = "q 100 0 0 100 72 600 cm /Im1 Do Q";
let (resources, content, objects) = match fonts {
None => (String::new(), picture.to_string(), vec![masked()]),
Some(first) => (
" /Font << /F1 6 0 R /F2 7 0 R >>".to_string(),
if first { format!("{picture} {TWO_RUNS}") } else { format!("{TWO_RUNS} {picture}") },
vec![masked(), SIMSUN.to_string(), SIMSUN.to_string()],
),
};
let names = format!(" /Resources << /XObject << /Im1 5 0 R >>{resources} >>");
let path = dir.join(format!("mask-{fonts:?}.pdf"));
std::fs::write(&path, one_page("", &names, &content, &objects)).expect("writes");
path
}
#[test]
fn a_broken_picture_under_two_filters_fails_exports() {
let dir = tempfile::tempdir().expect("a temp dir");
let path = broken_pictures(dir.path());
let (mut worker, doc) = opened(&path);
assert!(worker.export_text(doc, &dir.path().join("doc.txt")).is_err(), "its text read whole");
let (mut worker, doc) = opened(&path);
assert!(worker.export_image(doc, 0, 72.0, &dir.path().join("page.png")).is_err(), "its page read whole");
let (mut worker, doc) = opened(&path);
let tile = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792]).expect("draws");
assert!(tile.damaged, "the picture draws as damage");
}
#[test]
fn a_broken_picture_read_before_is_still_damage() {
let dir = tempfile::tempdir().expect("a temp dir");
let files = [broken_pictures(dir.path()), bad_mask(dir.path(), None), bad_mask(dir.path(), Some(true)), bad_mask(dir.path(), Some(false))];
// Reading caches the pictures; their repeat waits.
let read: [fn(&mut Worker, Doc); 2] = [|w, d| drop(w.text(d, 0).expect("reads")), |w, d| drop(w.search(d, 0, "x").expect("searches"))];
for (path, first) in files.iter().flat_map(|p| read.map(|r| (p, r))) {
let (mut worker, doc) = opened(path);
first(&mut worker, doc);
let tile = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792]).expect("draws");
assert!(tile.damaged, "the picture draws as damage");
assert!(worker.repair_report(doc).expect("asks").is_some());
let (mut worker, doc) = opened(path);
first(&mut worker, doc);
assert!(worker.export_image(doc, 0, 72.0, &dir.path().join("page.png")).is_err(), "its page read whole");
let (mut worker, doc) = opened(path);
first(&mut worker, doc);
assert!(worker.export_text(doc, &dir.path().join("doc.txt")).is_err(), "its text read whole");
let (mut worker, doc) = opened(path);
first(&mut worker, doc);
assert!(worker.check_pictures(doc).expect("checks"), "the check finds it");
}
}