mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
* Build each crate's integration tests as one binary Every tests/*.rs file is its own executable that links the whole crate graph. Move each crate's test files into tests/it/ as modules of one tests/it/main.rs, for apps/pdfcraft, apps/pdfcraft-cli, automation, cos, crypt, engine, render, sign and ui-egui. ui-egui's ui_group binary and the support modules move into tests/it/ too. Crates with a single test file keep it. The test executables go from 150 to 52, and every crate runs the same number of tests as before. ui-egui perf keeps its own binary (own process): its frame-time budgets would charge each measurement for the other tests running beside it. What the move needs: - include_str!/include_bytes! and #[path] paths gain one "../". - ui-egui: one GPU lock in tests/it/main.rs replaces the five per-file ones (content, document_links, editing, fill_sign, view), so one process creates one wgpu device at a time. The drop-line test in ui.rs and the screenshot blocks behind PDFCRAFT_*_SHOT(S) and CRAFT_UI_DOCKING_DIR, which render without holding it, take it too. - pdfcraft winit_wayland_scroll: the vendored winit source it compiles names crate::event, so that shim lives in main.rs. - parity evidence names the new paths. * Name the one-binary test paths in docs and comments - READMEs, docs, ATTRIBUTION and source comments name tests/it/<file>.rs. - The oracle README's example filters with --test it encryption::qpdf_agrees. - pdfcraft's GPU lock comment points at the UI tests' shared lock in crates/ui-egui/tests/it/main.rs.
137 lines
6.7 KiB
Rust
137 lines
6.7 KiB
Rust
//! CLI page commands must use the same dynamic XFA layout as the app and tools (#1645).
|
|
|
|
use std::path::PathBuf;
|
|
use std::process::{Command, Output};
|
|
use std::sync::Arc;
|
|
|
|
const BIN: &str = env!("CARGO_BIN_EXE_pdfcraft-cli");
|
|
|
|
fn fixture(test: &str) -> PathBuf {
|
|
let xdp = r#"<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
|
|
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
|
|
<subform name="form" layout="tb">
|
|
<pageSet><pageArea name="page"><occur max="-1"/>
|
|
<contentArea x="20pt" y="20pt" w="160pt" h="260pt"/>
|
|
<medium short="200pt" long="300pt"/>
|
|
</pageArea></pageSet>
|
|
<draw w="160pt" h="20pt"><value><text>First XFA page</text></value></draw>
|
|
<field name="name" w="160pt" h="20pt"><ui><textEdit/></ui><value><text>Ada</text></value></field>
|
|
<subform name="last" layout="tb"><breakBefore targetType="pageArea"/>
|
|
<draw w="160pt" h="20pt"><value><text>Second XFA page</text></value></draw>
|
|
</subform>
|
|
</subform></template></xdp:xdp>"#;
|
|
let objects = [
|
|
"<< /Type /Catalog /Pages 2 0 R /NeedsRendering true /AcroForm << /XFA 5 0 R /Fields [] >> >>".to_string(),
|
|
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(),
|
|
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >>".into(),
|
|
"<< /Length 0 >>\nstream\n\nendstream".into(),
|
|
format!("<< /Length {} >>\nstream\n{xdp}\nendstream", xdp.len()),
|
|
];
|
|
let mut bytes = b"%PDF-1.7\n".to_vec();
|
|
let mut offsets = Vec::new();
|
|
for (i, object) in objects.iter().enumerate() {
|
|
offsets.push(bytes.len());
|
|
bytes.extend_from_slice(format!("{} 0 obj\n{object}\nendobj\n", i + 1).as_bytes());
|
|
}
|
|
let xref = bytes.len();
|
|
bytes.extend_from_slice(b"xref\n0 6\n0000000000 65535 f \n");
|
|
for offset in offsets {
|
|
bytes.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
|
|
}
|
|
bytes.extend_from_slice(format!("trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n").as_bytes());
|
|
let dir = std::env::temp_dir().join(format!("pdfcraft-cli-xfa-{test}-{}", std::process::id()));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let path = dir.join("form.pdf");
|
|
std::fs::write(&path, bytes).unwrap();
|
|
path
|
|
}
|
|
|
|
fn success(output: &Output) {
|
|
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
|
|
}
|
|
|
|
#[test]
|
|
fn info_reports_the_laid_out_pages_and_fields() {
|
|
let path = fixture("info");
|
|
let original = std::fs::read(&path).unwrap();
|
|
let output = Command::new(BIN).arg("info").arg(&path).output().unwrap();
|
|
success(&output);
|
|
let info: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
assert_eq!(info["xfa_layout"]["pages"], 2, "the synthetic form lays out into two pages");
|
|
assert_eq!(info["pages"], 2, "top-level page count must agree with the app");
|
|
assert_eq!(info["fields"], 1);
|
|
assert_eq!(info["pdf_pages"], 1);
|
|
assert_eq!(info["acroform_fields"], 0);
|
|
assert_eq!(info["first_page_pt"], serde_json::json!([200.0, 300.0]));
|
|
assert_eq!(info["file_size"], original.len());
|
|
assert_eq!(std::fs::read(path).unwrap(), original, "inspection must not write the layout to the input");
|
|
}
|
|
|
|
#[test]
|
|
fn text_reads_the_layout_including_a_page_absent_from_the_placeholder() {
|
|
let path = fixture("text");
|
|
let output = Command::new(BIN).arg("text").arg(&path).output().unwrap();
|
|
success(&output);
|
|
let text = String::from_utf8(output.stdout).unwrap();
|
|
assert!(text.contains("First XFA page") && text.contains("Second XFA page"), "{text:?}");
|
|
let output = Command::new(BIN).arg("text").arg(&path).args(["--page", "2"]).output().unwrap();
|
|
success(&output);
|
|
let text = String::from_utf8(output.stdout).unwrap();
|
|
assert!(text.contains("Second XFA page") && !text.contains("First XFA page"), "{text:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn render_matches_the_engine_layout_on_the_second_page() {
|
|
use pdfcraft_render::{PageRenderer, RenderConfig, RenderRequest, RequestKind};
|
|
let path = fixture("render");
|
|
let original = std::fs::read(&path).unwrap();
|
|
let mut session = pdfcraft_engine::Session::new();
|
|
let id = session.open("form.pdf", None, Arc::new(original.clone()), None).unwrap();
|
|
let doc = session.get(id).unwrap();
|
|
assert_eq!(doc.info.pages.len(), 2);
|
|
assert!(doc.text_lines(1).iter().any(|line| line.text.contains("Second XFA page")));
|
|
let mut renderer = PageRenderer::new(doc.display.clone(), RenderConfig::default());
|
|
let expected = renderer.render(RenderRequest { page: 1, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, preview: false });
|
|
assert!(expected.error.is_none(), "{:?}", expected.error);
|
|
assert!(expected.rgba.as_chunks::<4>().0.iter().filter(|[r, _, _, a]| *r < 128 && *a > 0).count() > 10, "the layout has visible ink");
|
|
let out = path.with_extension("pam");
|
|
let output = Command::new(BIN).arg("render").arg(&path).args(["--page", "2", "--dpi", "72", "--out"]).arg(&out).output().unwrap();
|
|
success(&output);
|
|
let actual = std::fs::read(out).unwrap();
|
|
let header = format!("P7\nWIDTH {}\nHEIGHT {}\nDEPTH 4\nMAXVAL 255\nTUPLTYPE RGB_ALPHA\nENDHDR\n", expected.width, expected.height);
|
|
assert!(actual.starts_with(header.as_bytes()));
|
|
assert_eq!(&actual[header.len()..], &expected.rgba[..]);
|
|
assert_eq!(std::fs::read(path).unwrap(), original);
|
|
}
|
|
|
|
#[test]
|
|
fn a_laid_out_form_keeps_the_original_copy_permissions() {
|
|
let path = fixture("permissions");
|
|
let mut doc = pdfcraft_cos::Document::open(Arc::new(std::fs::read(&path).unwrap())).unwrap();
|
|
doc.set_encryption(&pdfcraft_cos::NewEncryption {
|
|
algorithm: pdfcraft_cos::Algorithm::Aes256,
|
|
user_password: "openme",
|
|
owner_password: "boss",
|
|
permissions: -17,
|
|
encrypt_metadata: true,
|
|
seed: [17; 32],
|
|
})
|
|
.unwrap();
|
|
let original = pdfcraft_cos::write_full(&doc, &Default::default()).unwrap();
|
|
std::fs::write(&path, &original).unwrap();
|
|
let output = Command::new(BIN).arg("text").arg(&path).args(["--password", "openme"]).output().unwrap();
|
|
assert!(!output.status.success());
|
|
assert!(output.stdout.is_empty());
|
|
assert!(String::from_utf8_lossy(&output.stderr).contains("don't allow copying"));
|
|
let output = Command::new(BIN).arg("text").arg(&path).args(["--password", "boss", "--page", "2"]).output().unwrap();
|
|
success(&output);
|
|
assert!(String::from_utf8_lossy(&output.stdout).contains("Second XFA page"));
|
|
let output = Command::new(BIN).arg("info").arg(&path).args(["--password", "openme"]).output().unwrap();
|
|
success(&output);
|
|
let info: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
|
|
assert_eq!(info["pages"], 2);
|
|
assert_eq!(info["encrypted"], true);
|
|
assert_eq!(info["file_size"], original.len());
|
|
assert_eq!(std::fs::read(path).unwrap(), original);
|
|
}
|