From b09bd7d75072052bd28df070dbedf2839ab5d541 Mon Sep 17 00:00:00 2001 From: trandinhnamuet Date: Thu, 16 Jul 2026 01:33:57 +0000 Subject: [PATCH] =?UTF-8?q?Phase=207:=20OCR=20&=20Convert=20=E2=80=94=20Te?= =?UTF-8?q?sseract=20l=E1=BB=9Bp=20text=20=E1=BA=A9n=20(Vi=E1=BB=87t+Anh),?= =?UTF-8?q?=20PDF=E2=86=92PNG/TXT/DOCX,=20Office=E2=86=94PDF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Engine: - ocr.rs MỚI: find_tesseract (sidecar như qpdf, env FOFREEXIT_TESSERACT_PATH); ocr_page_words (render 300 DPI → tesseract tsv → lọc confidence ≥30 → quy đổi pixel→điểm PDF); ocr_add_text_layer — thêm text object render-mode INVISIBLE đúng khung từng từ lên CHÍNH trang gốc (searchable PDF chuẩn, ảnh không đổi), font Unicode nhúng 1 lần, mặc định vie+eng - convert.rs MỚI: export_images (PNG mỗi trang theo DPI), export_text, export_docx tự viết (gom char boxes thành dòng thị giác → đoạn Word với cỡ chữ xấp xỉ + ngắt trang; DOCX = zip STORE tự ghi kèm CRC-32, không thêm dependency); find_soffice + office_to_pdf + pdf_to_docx_via_soffice (LibreOffice headless, profile riêng -env:UserInstallation) Tauri + UI (thanh 🔁 Chuyển đổi): - Commands: ocr_run, convert_images, convert_txt, convert_docx (tự chọn LibreOffice nếu có → fallback bộ cơ bản, báo engine đã dùng), office_convert, convert_tools_status (UI disable nút + báo thiếu công cụ), pick_office_file, pick_save_as - Nút: OCR (select Việt+Anh/Việt/Anh), Xuất PNG 150 DPI, Xuất TXT, Xuất Word, Office→PDF (mở luôn kết quả) Test (6 mới → engine 73/73 xanh ngoài 2 fixture qpdf Linux): - OCR tiếng Anh trên PDF-scan giả lập THẬT (xoá text chỉ còn ảnh render): extract + search thấy từ, toạ độ lớp ẩn khớp vùng chữ - OCR tiếng Việt đúng dấu ("Việt Nam đất nước") - export PNG 3 trang / TXT / DOCX (unzip hệ thống giải được, có ngắt trang) - LibreOffice round-trip 2 chiều (tự bỏ qua nếu máy không có soffice) Docs: 19-phase7-summary (+ giới hạn v-sau), 20-phase7-user-tests (C1–C10 kèm hướng dẫn cài Tesseract/LibreOffice trên Windows), README, roadmap (Phase 7 HOÀN TẤT lõi), 14 (kế tiếp: Phase 8 phát hành). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HgnbPwSoRGNH7LHsDUmRpk --- README.md | 8 +- app/src-tauri/src/main.rs | 112 ++++++ app/src/index.html | 18 + app/src/main.js | 101 ++++++ crates/ff-engine/src/convert.rs | 325 ++++++++++++++++++ crates/ff-engine/src/lib.rs | 6 + crates/ff-engine/src/ocr.rs | 198 +++++++++++ .../ff-engine/tests/ocr_convert_roundtrip.rs | 190 ++++++++++ docs/03-roadmap.md | 8 +- docs/14-foxit-gap-analysis.md | 21 +- docs/19-phase7-summary.md | 66 ++++ docs/20-phase7-user-tests.md | 37 ++ 12 files changed, 1075 insertions(+), 15 deletions(-) create mode 100644 crates/ff-engine/src/convert.rs create mode 100644 crates/ff-engine/src/ocr.rs create mode 100644 crates/ff-engine/tests/ocr_convert_roundtrip.rs create mode 100644 docs/19-phase7-summary.md create mode 100644 docs/20-phase7-user-tests.md diff --git a/README.md b/README.md index 5a10a31..a603173 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,13 @@ - [x] UI thanh "📝 Form": Điền form (modal liệt kê field theo loại), Thêm field, Flatten, Xuất FDF/CSV, Nhập FDF - [ ] Follow-up: radio-group/list/pushbutton khi tạo, XFDF, vẽ appearance riêng, điền overlay trực tiếp trên trang — xem [docs/17-phase6-summary.md](docs/17-phase6-summary.md) - **Checklist test cho bạn**: [docs/18-phase6-user-tests.md](docs/18-phase6-user-tests.md) -- [ ] **Phase 7 — OCR & Convert** (kế tiếp) — xem [docs/03-roadmap.md](docs/03-roadmap.md) +- [x] **Phase 7 — OCR & Convert** ✅ lõi hoàn tất (73/73 test engine xanh) + - [x] Engine `ocr.rs`: OCR Tesseract (sidecar, **Việt + Anh**) → lớp text ẨN khớp toạ độ trên trang gốc (searchable/copy được, ảnh không đổi) — test dựng PDF-scan giả lập thật + - [x] Engine `convert.rs`: PDF→PNG (theo DPI), PDF→TXT, PDF→DOCX tự viết (dòng thị giác + cỡ chữ, zip tự ghi); Office↔PDF qua LibreOffice headless (tự phát hiện, fallback rõ ràng) + - [x] UI thanh "🔁 Chuyển đổi": OCR (chọn ngôn ngữ), Xuất PNG/TXT/Word (tự chọn engine), Office→PDF; báo thiếu công cụ ngoài + - [ ] Follow-up: tiền xử lý ảnh scan (deskew), OCR theo vùng, PDF→Excel — xem [docs/19-phase7-summary.md](docs/19-phase7-summary.md) + - **Checklist test cho bạn**: [docs/20-phase7-user-tests.md](docs/20-phase7-user-tests.md) +- [ ] **Phase 8 — Hoàn thiện & Phát hành** (kế tiếp) — xem [docs/03-roadmap.md](docs/03-roadmap.md) Build & chạy: xem [docs/05-dev-setup.md](docs/05-dev-setup.md). diff --git a/app/src-tauri/src/main.rs b/app/src-tauri/src/main.rs index 36f2cc6..bb50f66 100644 --- a/app/src-tauri/src/main.rs +++ b/app/src-tauri/src/main.rs @@ -1160,6 +1160,110 @@ fn form_import_fdf(input: String, fdf: String, output: String) -> Result Result { + let pdfium = pdfium()?; + ff_engine::ocr_add_text_layer( + &pdfium, + std::path::Path::new(&input), + &[], + &lang, + std::path::Path::new(&output), + None, + ) + .map_err(|e| e.to_string()) +} + +/// PDF → PNG mỗi trang theo DPI, trả danh sách file. +#[tauri::command] +fn convert_images(input: String, out_dir: String, dpi: f32) -> Result, String> { + let pdfium = pdfium()?; + ff_engine::export_images( + &pdfium, + std::path::Path::new(&input), + std::path::Path::new(&out_dir), + dpi.clamp(36.0, 600.0), + None, + ) + .map(|v| v.into_iter().map(|p| p.to_string_lossy().into_owned()).collect()) + .map_err(|e| e.to_string()) +} + +/// PDF → TXT. +#[tauri::command] +fn convert_txt(input: String, output: String) -> Result<(), String> { + let pdfium = pdfium()?; + ff_engine::export_text(&pdfium, std::path::Path::new(&input), std::path::Path::new(&output), None) + .map_err(|e| e.to_string()) +} + +/// PDF → DOCX. Ưu tiên LibreOffice (layout tốt hơn) nếu máy có; không có thì +/// dùng bộ chuyển tự viết (text + bố cục cơ bản). Trả tên engine đã dùng. +#[tauri::command] +fn convert_docx(input: String, output: String) -> Result { + let inp = std::path::Path::new(&input); + let out = std::path::PathBuf::from(&output); + if ff_engine::find_soffice().is_ok() { + let dir = out.parent().map(|p| p.to_path_buf()).unwrap_or_else(std::env::temp_dir); + match ff_engine::pdf_to_docx_via_soffice(inp, &dir) { + Ok(produced) => { + if produced != out { + std::fs::rename(&produced, &out) + .or_else(|_| std::fs::copy(&produced, &out).map(|_| ())) + .map_err(|e| e.to_string())?; + let _ = std::fs::remove_file(&produced); + } + return Ok("libreoffice".into()); + } + Err(_) => { /* rơi xuống bộ tự viết */ } + } + } + let pdfium = pdfium()?; + ff_engine::export_docx(&pdfium, inp, &out, None).map_err(|e| e.to_string())?; + Ok("basic".into()) +} + +/// Office (docx/xlsx/pptx/odt...) → PDF qua LibreOffice. Trả đường dẫn PDF. +#[tauri::command] +fn office_convert(input: String, out_dir: String) -> Result { + ff_engine::office_to_pdf(std::path::Path::new(&input), std::path::Path::new(&out_dir)) + .map(|p| p.to_string_lossy().into_owned()) + .map_err(|e| e.to_string()) +} + +/// Kiểm tra công cụ ngoài (cho UI hiện trạng thái). +#[tauri::command] +fn convert_tools_status() -> serde_json::Value { + serde_json::json!({ + "tesseract": ff_engine::find_tesseract().is_ok(), + "soffice": ff_engine::find_soffice().is_ok(), + }) +} + +/// Hộp thoại chọn file Office để chuyển sang PDF. +#[tauri::command] +fn pick_office_file(app: tauri::AppHandle) -> Option { + app.dialog() + .file() + .add_filter("Tài liệu Office", &["docx", "doc", "xlsx", "xls", "pptx", "ppt", "odt", "ods", "odp", "rtf"]) + .blocking_pick_file() + .map(|fp| fp.to_string()) +} + +/// Hộp thoại lưu file theo đuôi tuỳ ý. +#[tauri::command] +fn pick_save_as(app: tauri::AppHandle, ext: String, name: String) -> Option { + app.dialog() + .file() + .add_filter(ext.to_uppercase(), &[ext.as_str()]) + .set_file_name(&name) + .blocking_save_file() + .map(|fp| fp.to_string()) +} + /// Hộp thoại lưu file FDF. #[tauri::command] fn pick_save_data(app: tauri::AppHandle) -> Option { @@ -1266,6 +1370,14 @@ fn main() { form_create, form_export, form_import_fdf, + ocr_run, + convert_images, + convert_txt, + convert_docx, + office_convert, + convert_tools_status, + pick_office_file, + pick_save_as, pick_any_file, pick_save_pem, pick_save_data, diff --git a/app/src/index.html b/app/src/index.html index b1f1c6c..dbc2192 100644 --- a/app/src/index.html +++ b/app/src/index.html @@ -38,6 +38,7 @@ +
@@ -117,6 +118,23 @@ + +