Engine (form.rs MỚI): - list_form_fields: duyệt cây /AcroForm/Fields (đệ quy /Kids), tên đầy đủ, phân loại theo /FT + cờ /Ff (text/checkbox/radio/combo/list/button/sig), đọc /V + options + on-state + trang + rect; kế thừa /FT từ cha - fill_form_fields: text/combo đặt /V (UTF-16BE cho tiếng Việt), checkbox/radio đặt /V + /AS về on-state, bật NeedAppearances (portable mọi viewer) - create_form_fields: tạo widget + field text/checkbox/combo, gắn vào /Annots + /AcroForm/Fields (tạo AcroForm nếu chưa có) - flatten_form: PDFium page.flatten() in field vào nội dung, bỏ tương tác - export_fdf/parse_fdf/import_fdf (literal ASCII + hex UTF-16BE round-trip tiếng Việt), export_csv Tauri + UI (thanh 📝 Form): - Commands: form_list, form_fill, form_flatten, form_create, form_export (FDF/CSV theo đuôi), form_import_fdf; pick_save_data/pick_fdf; tự chuẩn hoá qua qpdf khi lopdf không đọc thẳng trailer phi chuẩn (thêm dep lopdf cho app) - 📋 Điền form (modal liệt kê field theo loại: text→ô nhập, checkbox→tick, combo→select), ➕ Thêm field (dialog loại/tên/trang/vị trí/cỡ/options), 🧷 Flatten, ⬆ Xuất FDF/CSV, ⬇ Nhập FDF — không prompt/alert Test (5 mới → engine 67/67 xanh ngoài 2 fixture qpdf Linux): - create_and_list_fields; fill_text_checkbox_combo_round_trips (tiếng Việt); fdf_export_import_round_trip; export_csv_has_rows; flatten_removes_interactive_fields Docs: 17-phase6-summary (+ giới hạn), 18-phase6-user-tests (F1–F10), README, roadmap (Phase 6 HOÀN TẤT lõi), 14 (kế tiếp: Phase 7 OCR/Convert). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HgnbPwSoRGNH7LHsDUmRpk
180 lines
6.6 KiB
Rust
180 lines
6.6 KiB
Rust
//! Test Form AcroForm (Phase 6): tạo field → liệt kê → điền → export/import
|
|
//! FDF & CSV → flatten. Cần binary `qpdf` để chuẩn hoá fixture cho lopdf.
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use ff_engine::{FieldKind, FieldValue, NewField};
|
|
|
|
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", "/usr/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
|
|
}
|
|
|
|
/// Dựng fixture form: chuẩn hoá sample rồi thêm 1 text + 1 checkbox + 1 combo.
|
|
fn make_form_fixture(out: &std::path::Path) {
|
|
std::env::set_var("FOFREEXIT_QPDF_PATH", "/usr/bin");
|
|
let norm = tmp("ff_form_norm.pdf");
|
|
ff_engine::repair(&workspace_root().join("corpus").join("sample-multipage.pdf"), &norm)
|
|
.expect("normalize");
|
|
let fields = vec![
|
|
NewField {
|
|
name: "hoTen".into(),
|
|
kind: FieldKind::Text,
|
|
page_index: 0,
|
|
rect: [80.0, 700.0, 320.0, 720.0],
|
|
value: String::new(),
|
|
options: vec![],
|
|
},
|
|
NewField {
|
|
name: "dongY".into(),
|
|
kind: FieldKind::Checkbox,
|
|
page_index: 0,
|
|
rect: [80.0, 660.0, 96.0, 676.0],
|
|
value: "off".into(),
|
|
options: vec![],
|
|
},
|
|
NewField {
|
|
name: "gioiTinh".into(),
|
|
kind: FieldKind::Combo,
|
|
page_index: 0,
|
|
rect: [80.0, 620.0, 240.0, 640.0],
|
|
value: String::new(),
|
|
options: vec!["Nam".into(), "Nữ".into(), "Khác".into()],
|
|
},
|
|
];
|
|
ff_engine::create_form_fields(&norm, &fields, out).expect("create fields");
|
|
}
|
|
|
|
#[test]
|
|
fn create_and_list_fields() {
|
|
let _ = pdfium();
|
|
let fx = tmp("ff_form_fx.pdf");
|
|
make_form_fixture(&fx);
|
|
|
|
let fields = ff_engine::list_form_fields(&fx).expect("list");
|
|
assert_eq!(fields.len(), 3, "phải có 3 field: {:?}", fields.iter().map(|f| &f.name).collect::<Vec<_>>());
|
|
let by_name = |n: &str| fields.iter().find(|f| f.name == n).unwrap_or_else(|| panic!("thiếu {n}"));
|
|
assert_eq!(by_name("hoTen").kind, FieldKind::Text);
|
|
assert_eq!(by_name("dongY").kind, FieldKind::Checkbox);
|
|
let combo = by_name("gioiTinh");
|
|
assert_eq!(combo.kind, FieldKind::Combo);
|
|
assert_eq!(combo.options, vec!["Nam".to_string(), "Nữ".into(), "Khác".into()]);
|
|
// Field nằm đúng trang 0 và có rect.
|
|
assert_eq!(by_name("hoTen").page_index, Some(0));
|
|
assert!(by_name("hoTen").rect.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn fill_text_checkbox_combo_round_trips() {
|
|
let _ = pdfium();
|
|
let fx = tmp("ff_form_fill_fx.pdf");
|
|
let out = tmp("ff_form_filled.pdf");
|
|
make_form_fixture(&fx);
|
|
|
|
let values = vec![
|
|
FieldValue { name: "hoTen".into(), value: "Nguyễn Văn A".into() },
|
|
FieldValue { name: "dongY".into(), value: "on".into() },
|
|
FieldValue { name: "gioiTinh".into(), value: "Nữ".into() },
|
|
];
|
|
let n = ff_engine::fill_form_fields(&fx, &values, &out).expect("fill");
|
|
assert_eq!(n, 3, "phải điền 3 field");
|
|
|
|
let fields = ff_engine::list_form_fields(&out).expect("list out");
|
|
let get = |n: &str| fields.iter().find(|f| f.name == n).unwrap();
|
|
assert_eq!(get("hoTen").value.as_deref(), Some("Nguyễn Văn A"), "text (tiếng Việt) round-trip");
|
|
assert_eq!(get("gioiTinh").value.as_deref(), Some("Nữ"));
|
|
// Checkbox bật → /V là tên on-state (khác "Off").
|
|
let cb = get("dongY").value.clone().unwrap_or_default();
|
|
assert!(cb != "Off" && !cb.is_empty(), "checkbox bật phải có on-state, được {cb:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn fdf_export_import_round_trip() {
|
|
let _ = pdfium();
|
|
let fx = tmp("ff_form_fdf_fx.pdf");
|
|
let filled = tmp("ff_form_fdf_filled.pdf");
|
|
let fdf = tmp("ff_form_out.fdf");
|
|
let reimported = tmp("ff_form_reimport.pdf");
|
|
make_form_fixture(&fx);
|
|
|
|
let values = vec![
|
|
FieldValue { name: "hoTen".into(), value: "Trần Thị B".into() },
|
|
FieldValue { name: "gioiTinh".into(), value: "Khác".into() },
|
|
];
|
|
ff_engine::fill_form_fields(&fx, &values, &filled).expect("fill");
|
|
ff_engine::export_fdf(&filled, &fdf).expect("export fdf");
|
|
|
|
// FDF chứa tên + giá trị.
|
|
let parsed = ff_engine::parse_fdf(&fdf).expect("parse fdf");
|
|
let hoten = parsed.iter().find(|v| v.name == "hoTen").expect("hoTen trong fdf");
|
|
assert_eq!(hoten.value, "Trần Thị B");
|
|
|
|
// Import FDF vào fixture rỗng → giá trị được điền lại.
|
|
let n = ff_engine::import_fdf(&fx, &fdf, &reimported).expect("import fdf");
|
|
assert!(n >= 2, "import phải điền ≥2 field");
|
|
let fields = ff_engine::list_form_fields(&reimported).expect("list reimport");
|
|
assert_eq!(
|
|
fields.iter().find(|f| f.name == "hoTen").unwrap().value.as_deref(),
|
|
Some("Trần Thị B")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn export_csv_has_rows() {
|
|
let _ = pdfium();
|
|
let fx = tmp("ff_form_csv_fx.pdf");
|
|
let filled = tmp("ff_form_csv_filled.pdf");
|
|
let csv = tmp("ff_form_out.csv");
|
|
make_form_fixture(&fx);
|
|
ff_engine::fill_form_fields(
|
|
&fx,
|
|
&[FieldValue { name: "hoTen".into(), value: "CSV Test".into() }],
|
|
&filled,
|
|
)
|
|
.expect("fill");
|
|
ff_engine::export_csv(&filled, &csv).expect("export csv");
|
|
|
|
let text = std::fs::read_to_string(&csv).expect("đọc csv");
|
|
assert!(text.starts_with("name,value"), "CSV phải có header");
|
|
assert!(text.contains("hoTen,CSV Test"), "CSV phải chứa giá trị: {text}");
|
|
}
|
|
|
|
#[test]
|
|
fn flatten_removes_interactive_fields() {
|
|
let pdf = pdfium();
|
|
let fx = tmp("ff_form_flat_fx.pdf");
|
|
let filled = tmp("ff_form_flat_filled.pdf");
|
|
let flat = tmp("ff_form_flat.pdf");
|
|
make_form_fixture(&fx);
|
|
ff_engine::fill_form_fields(
|
|
&fx,
|
|
&[FieldValue { name: "hoTen".into(), value: "Flatten Me".into() }],
|
|
&filled,
|
|
)
|
|
.expect("fill");
|
|
|
|
ff_engine::flatten_form(&pdf, &filled, &flat, None).expect("flatten");
|
|
// Sau flatten không còn field tương tác.
|
|
let fields = ff_engine::list_form_fields(&flat).expect("list flat");
|
|
assert!(fields.is_empty(), "flatten phải bỏ hết field tương tác, còn: {:?}", fields.iter().map(|f| &f.name).collect::<Vec<_>>());
|
|
}
|