Phase 7: OCR & Convert — Tesseract lớp text ẩn (Việt+Anh), PDF→PNG/TXT/DOCX, Office↔PDF
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HgnbPwSoRGNH7LHsDUmRpk
This commit is contained in:
@@ -38,6 +38,7 @@
|
||||
<button id="editModeBtn" title="Sửa nội dung trang">✏️ Sửa nội dung</button>
|
||||
<button id="formModeBtn" title="Form: điền, tạo field, flatten, FDF/CSV">📝 Form</button>
|
||||
<button id="secModeBtn" title="Bảo mật: redact, mật khẩu, metadata">🔒 Bảo mật</button>
|
||||
<button id="convModeBtn" title="OCR & Chuyển đổi: OCR, PNG, TXT, Word, Office→PDF">🔁 Chuyển đổi</button>
|
||||
<div class="spacer"></div>
|
||||
<span id="status" class="status"></span>
|
||||
</header>
|
||||
@@ -117,6 +118,23 @@
|
||||
<span id="formHint" class="status"></span>
|
||||
</div>
|
||||
|
||||
<div id="convBar" class="editbar hidden">
|
||||
<span class="annolabel">Chuyển đổi:</span>
|
||||
<label class="ed-prop">OCR ngôn ngữ <select id="cvLang">
|
||||
<option value="vie+eng">Việt + Anh</option>
|
||||
<option value="vie">Tiếng Việt</option>
|
||||
<option value="eng">English</option>
|
||||
</select></label>
|
||||
<button id="cvOcr" title="Nhận dạng chữ (Tesseract) → thêm lớp text ẩn, file tìm kiếm/copy được">🔍 OCR tài liệu</button>
|
||||
<span class="sep"></span>
|
||||
<button id="cvPng" title="Xuất mỗi trang thành 1 ảnh PNG">🖼 Xuất PNG</button>
|
||||
<button id="cvTxt" title="Xuất toàn bộ text ra file .txt">📄 Xuất TXT</button>
|
||||
<button id="cvDocx" title="Xuất Word (.docx) — dùng LibreOffice nếu máy có, không thì bộ chuyển cơ bản">📘 Xuất Word</button>
|
||||
<button id="cvOffice" title="Chuyển file Office (docx/xlsx/pptx...) sang PDF — cần LibreOffice">📥 Office → PDF</button>
|
||||
<span class="spacer"></span>
|
||||
<span id="convHint" class="status"></span>
|
||||
</div>
|
||||
|
||||
<div id="secBar" class="editbar hidden">
|
||||
<span class="annolabel">Bảo mật:</span>
|
||||
<button id="secRedactTool" class="atool" data-tool="redact" title="Kéo chuột trên trang để đánh dấu vùng cần bôi đen — nội dung bị XOÁ THẬT khi áp dụng">⬛ Đánh dấu redact</button>
|
||||
|
||||
+101
@@ -43,6 +43,7 @@ const state = {
|
||||
redactMarks: [], // [{page, rect{left,bottom,right,top}}] chờ áp dụng
|
||||
formMode: false, // thanh Form (Phase 6) đang mở
|
||||
formFields: [], // field đọc từ tài liệu hiện tại
|
||||
convMode: false, // thanh Chuyển đổi (Phase 7) đang mở
|
||||
};
|
||||
const UNDO_LIMIT = 50;
|
||||
let annotIdSeq = 1;
|
||||
@@ -2678,6 +2679,98 @@ async function saveEdits() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Phase 7: OCR & Chuyển đổi ----------
|
||||
|
||||
async function toggleConvMode() {
|
||||
state.convMode = !state.convMode;
|
||||
$("convBar").classList.toggle("hidden", !state.convMode);
|
||||
$("convModeBtn").classList.toggle("active", state.convMode);
|
||||
if (state.convMode) {
|
||||
try {
|
||||
const st = await invoke("convert_tools_status");
|
||||
const miss = [];
|
||||
if (!st.tesseract) miss.push("Tesseract (OCR)");
|
||||
if (!st.soffice) miss.push("LibreOffice (Office↔PDF chất lượng cao)");
|
||||
$("convHint").textContent = miss.length
|
||||
? `Chưa cài: ${miss.join(", ")} — các nút liên quan sẽ báo lỗi kèm hướng dẫn`
|
||||
: "";
|
||||
$("cvOcr").disabled = !st.tesseract;
|
||||
$("cvOffice").disabled = !st.soffice;
|
||||
} catch (_) { /* trạng thái chỉ để gợi ý */ }
|
||||
} else {
|
||||
$("convHint").textContent = "";
|
||||
}
|
||||
}
|
||||
|
||||
async function runOcrAction() {
|
||||
const lang = $("cvLang").value || "vie+eng";
|
||||
const out = await invoke("pick_save_pdf");
|
||||
if (!out) return;
|
||||
$("convHint").textContent = "Đang OCR… (tài liệu dài có thể mất vài phút)";
|
||||
try {
|
||||
const n = await invoke("ocr_run", { input: state.path, lang, output: out });
|
||||
$("status").textContent = `OCR xong: ${n} từ, đã thêm lớp text ẩn → ${shortName(out)}`;
|
||||
$("convHint").textContent = "";
|
||||
loadDocument(out);
|
||||
} catch (e) {
|
||||
$("convHint").textContent = "Lỗi OCR: " + e;
|
||||
}
|
||||
}
|
||||
|
||||
async function exportPngAction() {
|
||||
const dir = await invoke("pick_dir");
|
||||
if (!dir) return;
|
||||
try {
|
||||
const files = await invoke("convert_images", { input: state.path, outDir: dir, dpi: 150 });
|
||||
$("status").textContent = `Đã xuất ${files.length} ảnh PNG (150 DPI) vào ${dir}`;
|
||||
} catch (e) {
|
||||
$("convHint").textContent = "Lỗi xuất PNG: " + e;
|
||||
}
|
||||
}
|
||||
|
||||
async function exportTxtAction() {
|
||||
const base = shortName(state.path).replace(/\.pdf$/i, "");
|
||||
const out = await invoke("pick_save_as", { ext: "txt", name: base + ".txt" });
|
||||
if (!out) return;
|
||||
try {
|
||||
await invoke("convert_txt", { input: state.path, output: out });
|
||||
$("status").textContent = `Đã xuất text → ${shortName(out)}`;
|
||||
} catch (e) {
|
||||
$("convHint").textContent = "Lỗi xuất TXT: " + e;
|
||||
}
|
||||
}
|
||||
|
||||
async function exportDocxAction() {
|
||||
const base = shortName(state.path).replace(/\.pdf$/i, "");
|
||||
const out = await invoke("pick_save_as", { ext: "docx", name: base + ".docx" });
|
||||
if (!out) return;
|
||||
$("convHint").textContent = "Đang chuyển sang Word…";
|
||||
try {
|
||||
const engine = await invoke("convert_docx", { input: state.path, output: out });
|
||||
const note = engine === "libreoffice" ? "LibreOffice (giữ layout tốt)" : "bộ chuyển cơ bản (text + bố cục)";
|
||||
$("status").textContent = `Đã xuất Word (${note}) → ${shortName(out)}`;
|
||||
$("convHint").textContent = "";
|
||||
} catch (e) {
|
||||
$("convHint").textContent = "Lỗi xuất Word: " + e;
|
||||
}
|
||||
}
|
||||
|
||||
async function officeToPdfAction() {
|
||||
const src = await invoke("pick_office_file");
|
||||
if (!src) return;
|
||||
const dir = await invoke("pick_dir");
|
||||
if (!dir) return;
|
||||
$("convHint").textContent = "Đang chuyển Office → PDF…";
|
||||
try {
|
||||
const out = await invoke("office_convert", { input: src, outDir: dir });
|
||||
$("status").textContent = `Đã chuyển → ${shortName(out)}`;
|
||||
$("convHint").textContent = "";
|
||||
loadDocument(out);
|
||||
} catch (e) {
|
||||
$("convHint").textContent = "Lỗi Office→PDF (máy cần LibreOffice): " + e;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Phase 6: Form (AcroForm) ----------
|
||||
|
||||
function toggleFormMode() {
|
||||
@@ -3225,6 +3318,14 @@ $("modalOverlay").addEventListener("click", (e) => {
|
||||
if (e.target.id === "modalOverlay") closeModal();
|
||||
});
|
||||
|
||||
// OCR & Chuyển đổi (Phase 7)
|
||||
$("convModeBtn").addEventListener("click", toggleConvMode);
|
||||
$("cvOcr").addEventListener("click", runOcrAction);
|
||||
$("cvPng").addEventListener("click", exportPngAction);
|
||||
$("cvTxt").addEventListener("click", exportTxtAction);
|
||||
$("cvDocx").addEventListener("click", exportDocxAction);
|
||||
$("cvOffice").addEventListener("click", officeToPdfAction);
|
||||
|
||||
// Form (Phase 6)
|
||||
$("formModeBtn").addEventListener("click", toggleFormMode);
|
||||
$("fmFill").addEventListener("click", openFillForm);
|
||||
|
||||
Reference in New Issue
Block a user