Phase 3 (Organize + robust save) + Phase 4 (Edit content); update docs
Engine: - organize.rs: build_document(plan) core — insert/delete/rotate/crop/extract/merge/split - watermark.rs: watermark + header/footer via PdfPages::watermark, CID Unicode fonts - qpdf.rs: QPDF CLI wrapper — repair corrupt files, re-encrypt after edits, ensure_openable - edit.rs: MOAT — list_objects + apply_edits (SetText VN, Transform, Delete, Add/ReplaceImage) - Tests: organize/watermark/qpdf_safety/edit roundtrip — 43/43 engine tests green UI (Tauri): "Tổ chức trang" mode (grid + drag-reorder + multi-select), "Sửa nội dung" mode (WYSIWYG object overlay, edit-in-place), global undo/redo, robust file open. Corpus: add encrypted.pdf + corrupt-truncated.pdf fixtures. Docs: mark Phase 1-4 done in roadmap; document QPDF setup + engine module layout in 05-dev-setup; add 10-13 phase summaries & user-test checklists; README status.
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
//! Test lớp an toàn QPDF (Phase 3): repair file hỏng trước khi PDFium mở,
|
||||
//! và mã hoá lại file sau khi PDFium/lopdf chỉnh sửa — đường GHI/MỞ file rủi
|
||||
//! ro nhất nên test bằng fixture THẬT (corrupt-truncated.pdf bị PDFium từ
|
||||
//! chối thẳng, encrypted.pdf cần đúng password).
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn workspace_root() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
.join("..")
|
||||
.canonicalize()
|
||||
.expect("workspace root")
|
||||
}
|
||||
|
||||
fn pdfium() -> pdfium_render::prelude::Pdfium {
|
||||
if std::env::var("FOFREEXIT_PDFIUM_PATH").is_err() {
|
||||
std::env::set_var("FOFREEXIT_PDFIUM_PATH", workspace_root());
|
||||
}
|
||||
if std::env::var("FOFREEXIT_QPDF_PATH").is_err() {
|
||||
std::env::set_var("FOFREEXIT_QPDF_PATH", workspace_root().join("qpdf").join("bin"));
|
||||
}
|
||||
ff_engine::bind_pdfium().expect("nạp PDFium")
|
||||
}
|
||||
|
||||
fn tmp(name: &str) -> PathBuf {
|
||||
let p = std::env::temp_dir().join(name);
|
||||
let _ = std::fs::remove_file(&p);
|
||||
p
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_qpdf_locates_binary() {
|
||||
let _ = pdfium(); // đảm bảo FOFREEXIT_QPDF_PATH đã set
|
||||
let path = ff_engine::find_qpdf().expect("find_qpdf");
|
||||
assert!(path.is_file(), "qpdf.exe phải tồn tại tại {}", path.display());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdfium_rejects_truncated_file_directly() {
|
||||
let pdf = pdfium();
|
||||
let input = workspace_root().join("corpus").join("corrupt-truncated.pdf");
|
||||
let err = pdf.load_pdf_from_file(&input, None);
|
||||
assert!(err.is_err(), "PDFium phải từ chối file bị cắt cụt khi chưa repair");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repair_makes_truncated_file_openable() {
|
||||
let pdf = pdfium();
|
||||
let input = workspace_root().join("corpus").join("corrupt-truncated.pdf");
|
||||
let output = tmp("ff_qpdf_repaired.pdf");
|
||||
|
||||
ff_engine::repair(&input, &output).expect("repair");
|
||||
let doc = pdf.load_pdf_from_file(&output, None).expect("mở file đã repair");
|
||||
assert_eq!(doc.pages().len(), 3, "phải khôi phục đủ 3 trang gốc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_openable_repairs_automatically() {
|
||||
let pdf = pdfium();
|
||||
let input = workspace_root().join("corpus").join("corrupt-truncated.pdf");
|
||||
|
||||
let usable = ff_engine::ensure_openable(&pdf, &input, None).expect("ensure_openable");
|
||||
assert_ne!(usable, input, "phải trả về 1 file tạm đã repair, không phải input gốc");
|
||||
assert_eq!(
|
||||
ff_engine::page_count(&pdf, &usable, None).expect("page_count"),
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_openable_passes_through_file_that_already_opens() {
|
||||
let pdf = pdfium();
|
||||
let input = workspace_root().join("corpus").join("sample-multipage.pdf");
|
||||
let usable = ff_engine::ensure_openable(&pdf, &input, None).expect("ensure_openable");
|
||||
assert_eq!(usable, input, "file mở thẳng được thì không cần repair");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypted_fixture_requires_correct_password() {
|
||||
let pdf = pdfium();
|
||||
let input = workspace_root().join("corpus").join("encrypted.pdf");
|
||||
|
||||
assert!(pdf.load_pdf_from_file(&input, None).is_err(), "không password phải lỗi");
|
||||
assert!(
|
||||
pdf.load_pdf_from_file(&input, Some("sai-password")).is_err(),
|
||||
"sai password phải lỗi"
|
||||
);
|
||||
let doc = pdf
|
||||
.load_pdf_from_file(&input, Some("fofreexit"))
|
||||
.expect("đúng password phải mở được");
|
||||
assert_eq!(doc.pages().len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypt_with_password_round_trips() {
|
||||
let pdf = pdfium();
|
||||
let input = workspace_root().join("corpus").join("sample-multipage.pdf"); // chưa mã hoá
|
||||
let output = tmp("ff_qpdf_encrypted.pdf");
|
||||
|
||||
ff_engine::encrypt_with_password(&input, &output, "userpw", "ownerpw")
|
||||
.expect("encrypt_with_password");
|
||||
|
||||
assert!(pdf.load_pdf_from_file(&output, None).is_err(), "phải cần password sau khi mã hoá");
|
||||
let doc = pdf
|
||||
.load_pdf_from_file(&output, Some("userpw"))
|
||||
.expect("đúng password phải mở được");
|
||||
assert_eq!(doc.pages().len(), 3, "nội dung phải giữ nguyên sau khi mã hoá");
|
||||
}
|
||||
Reference in New Issue
Block a user