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.
11 KiB
10 — Tổng kết Phase 3 (Tổ chức trang + Lưu file vững)
Trạng thái: lõi HOÀN TẤT. 36/36 test tự động xanh (
ff-engine/ff-cos/ff-pdmodel). UI đã chạy thật và chụp ảnh xác nhận từng tính năng.
1. Khảo sát trước khi code
Đọc source pdfium-render 0.8.37 xác nhận PDFium đã có API cấp cao cho gần hết Phase 3 (copy/insert/delete trang, set_rotation, set_crop, watermark(closure), nạp font TrueType làm CID font Unicode ngay trong PDFium). Vì vậy QPDF không còn là động cơ thao tác trang chính như dự kiến ban đầu — vai trò của nó thu hẹp lại đúng như đã chốt ở docs/02-tech-stack.md: lớp an toàn sửa file hỏng + mã hoá lại, PDFium làm hết phần chỉnh trang.
2. Đã làm
Engine (crates/ff-engine/src)
organize.rs:PagePlanEntry/PageSource+build_document(plan)— 1 hàm lõi dựng document đích từ danh sách "lấy trang nào, từ đâu, xoay/crop ra sao". Insert/Delete/Reorder/Replace/Extract/Merge/Split chỉ là cách dựngplankhác nhau, không nhân bản logic. Cóidentity_plan,delete_pages,rotate_pages,extract_pages,merge_files,split_by_page_count.watermark.rs:add_watermark/add_header_footerdùngPdfPages::watermark(closure)+ font TrueType nạp làm CID Unicode (tái dùngfind_font_bytestừannot.rs) — không cần tự build Type0/CIDFontType2 dict tay như FreeText Phase 2. Token{page}/{total}/{date}cho header/footer; vị trí theo lưới 9 điểm (Anchor).qpdf.rs: gọiqpdf.exe(binary prebuilt,scripts/fetch-qpdf.ps1) quastd::process::Command.repair()sửa file hỏng trước khi PDFium mở;encrypt_with_password()áp lại mã hoá sau khi PDFium/lopdf chỉnh sửa ra bản tạm không mã hoá;ensure_openable()thử mở thẳng, lỗi thì tự repair rồi mở lại — dùng ở cả engine vàloadDocumentphía UI.- Bài học kỹ thuật quan trọng (đã ghi vào memory dự án):
qpdf.exehiểu nhầm tiền tố verbatim\\?\...(từPath::canonicalize()trên Windows) thành UNC path → phải tự strip trước khi đưa vào argv.- Panic trong khi 1
Pdfium/PdfDocumentcòn sống sẽ làm "kẹt" (poison) global mutex củaThreadSafePdfiumBindingscho hết tiến trình test — mọi test PDFium sau đó báo lỗithread_safe.rs:76không liên quan gì tới bug thật; luôn cô lập test đầu tiên bị fail để tìm nguyên nhân gốc.
Tauri commands (app/src-tauri/src/main.rs)
ensure_openable, organize_identity_plan, organize_apply, organize_extract, organize_merge, organize_split, watermark_add, header_footer_add, preview_watermark, preview_header_footer, pick_dir (chọn thư mục cho Tách file). DTO PagePlanEntryDto/RectDto dùng chung 2 chiều (gửi lên + trả về) nên đều có Serialize + Deserialize.
UI (app/src)
- Chế độ "Tổ chức trang": nút trên toolbar chính thay
#viewportbằng lưới thumbnail lớn (#organizeGrid), thanh công cụ riêng (#organizeBar): Chèn/Xoá/Xoay trái/Xoay phải/Trích/Thay/Trộn file/Tách file/Watermark/Header-Footer/Lưu thay đổi. - Multi-select (click/Ctrl-click/Shift-click) + kéo-thả đổi thứ tự (HTML5 drag-and-drop) trên lưới, xoay xem ngay bằng CSS transform trước khi lưu thật.
- Undo/Redo toàn cục: mở rộng cơ chế snapshot Phase 2 (chỉ có
annotSpecs) sang chụp cả{annotSpecs, pagePlan}— 1 stack Ctrl+Z/Ctrl+Y duy nhất cho cả chú thích và tổ chức trang. - Dialog dùng chung (
#modalOverlay/#modalBox) cho cả 8 thao tác: Chèn (trang trắng theo cỡ giấy, hoặc từ file + page range), Trích (kèm tuỳ chọn xoá sau khi trích), Thay (theo file + range), Trộn file (thêm/sắp thứ tự/xoá nhiều file), Tách file (theo số trang/file), Watermark (text/cỡ/màu/độ mờ/đậm-nghiêng/góc xoay/lưới 9 vị trí/phạm vi trang — có xem trước render PDFium thật), Header/Footer (6 ô trái-giữa-phải × trên-dưới, token chèn nhanh, có xem trước thật). - Crop: tái dùng cơ chế kéo-vẽ hình chữ nhật đã có cho tool "Khung" — thêm nhánh
tool === "crop"mở dialog 4 số margin (trái/phải/trên/dưới) áp cho trang này hoặc tất cả; áp trực tiếp vàopagePlan(chỉ ghi ra file thật khi bấm Lưu). - Mở file vững hơn:
loadDocumentthử mở thẳng, lỗi thì gọiensure_openable(tự repair qua QPDF) rồi mở lại — không cần người dùng tự sửa file. - Sửa lỗi giao diện phát sinh khi thêm nhiều nút: toolbar/annobar/organizebar tràn ngoài cửa sổ ở độ phân giải hẹp → thêm
flex-wrap: wrap.
3. Test tự động (36/36 xanh, crate ff-engine)
organize_roundtrip(8): xoá còn đúng nội dung/thứ tự; xoá hết bị chặn; xoay bền sau khi lưu; trích không đụng file gốc; trộn nhiều file đúng thứ tự; tách đúng số trang/file; crop ghi đúng/CropBox; chèn trang trắng đúng kích thước không có text thừa.watermark_roundtrip(5): watermark hiện trên mọi trang/đúng trang được lọc/tiếng Việt round-trip đúng; header/footer chèn đúng số trang + tổng số trang; ô rỗng bị bỏ qua (không vẽ chữ thừa).qpdf_safety(7): PDFium từ chối thẳng file bị cắt cụt; repair sửa được;ensure_openabletự repair / tự đi qua khi file vốn mở được; fixture mã hoá đòi đúng password; mã hoá round-trip giữ nguyên nội dung.- Cùng các test Phase 1/2 vẫn xanh (
viewer_features6,big_doc3,render_smoke1,annot_roundtrip6). ff-clikhông tự chạy test được trong môi trường này do Windows Smart App Control chặn thực thi binary test mới build (lỗi hệ điều hành, không phải lỗi code) — không ảnh hưởng coverage vì crate này không có test riêng.
4. Đã kiểm bằng tay/ảnh (build release, chạy app thật)
- Vào/ra chế độ Tổ chức trang; lưới hiển thị đúng 3 thumbnail thật.
- Chọn trang (viền xanh) + Xoay phải → thumbnail xoay 90° ngay; Hoàn tác → xoay + chọn về lại đúng trạng thái cũ (xác nhận undo/redo toàn cục hoạt động).
- Dialog Chèn trang: chèn trang trắng cỡ Letter vào cuối tài liệu → lưới hiện thêm trang 4 trắng.
- Dialog Watermark: nhập "CONFIDENTIAL", góc xoay 45°, vị trí giữa → bấm Xem trước → ảnh PDFium thật hiện chữ đỏ xoay đúng góc, đúng vị trí, đè lên nội dung trang.
- Dialog Header/Footer: 6 ô + token chèn nhanh hiển thị đúng; Xem trước trả về ảnh PDFium thật (nội dung số trang đã được test-gate ở engine).
5. Kiểm duyệt lại luồng hoạt động (2026-06-18) — đã tối ưu
Sau khi rà lại toàn bộ luồng người dùng dùng các chức năng Phase 3, đã sửa:
- [Hiệu năng — quan trọng] Lưới Tổ chức trang không còn render lại toàn bộ thumbnail khi click chọn/xoay. Trước đây
orgSelectCard/orgRotateSelectedgọibuildOrganizeGrid()→ xoá sạch lưới vàrender_pagelại cho MỌI card (tài liệu N trang → mỗi click = N lần render PDFium + N base64, kèm nhấp nháy). Nay: chọn chỉ đổi class.selected(refreshOrgSelection), xoay chỉ đổi CSStransformcủa card liên quan; thêm cache thumbnailstate.orgThumbs(khoá theosource#srcIndex) nên cả khi dựng lại lưới (chèn/xoá/đảo) cũng không render lại trang đã render. Reset cache khi mở file mới. - [Chưa hợp lý → ĐÃ SỬA THÀNH ĐÚNG, không chỉ cảnh báo] Watermark/Header-Footer giờ tự động áp lên đúng trạng thái
pagePlanđang xem, không phải file gốc. Thêm Tauri commandorganize_materialize(mainInput, plan, password)(tái dùngbuild_documentcó sẵn — không cần engine mới) dựng tạm 1 file PDF theo đúng plan hiện tại; frontend (materializeBaseInput()) tự gọi command này khiplanIsDirty()và dùng file tạm đó làminputcho cả Xem trước và Áp dụng của 2 dialog — thay cho cảnh báo "tự đi lưu trước" trước đây. Dialog hiện ghi chú thông tin (không phải cảnh báo chặn) khi đang dùng bản tạm. Đã verify ảnh: xoay trang 1 rồi mở Watermark → Xem trước hiện đúng nội dung trang đã XOAY 90° kèm watermark đè lên — đúng trạng thái đang thấy trên lưới Tổ chức trang, không phải file gốc chưa xoay.- Sửa kèm: phạm vi trang nhập trong 2 dialog (
Trang áp dụng) giờ tính theopagePlan.length(số trang ĐANG XEM) thay vìpages.length(số trang file gốc) — 2 số này khác nhau ngay khi đã chèn/xoá trang.
- Sửa kèm: phạm vi trang nhập trong 2 dialog (
- [Hiệu năng]
split_by_page_countmở file nguồn đúng 1 lần thay vì gọibuild_documentmỗi phần (mỗi lần mở/parse lại cả file). Tách file nhiều trang nhanh hơn rõ rệt. Testsplit_by_page_count_produces_correct_chunksvẫn xanh. - [Bẫy verify đã gặp, ghi vào memory dự án] App Tauri là workspace TÁCH RIÊNG khỏi
crates/*—cargo build --releasechạy từ gốc repo chỉ buildcrates/*, KHÔNG build app (dễ verify nhầm trên exe cũ). Phải dùngcargo build --release --manifest-path app/src-tauri/Cargo.toml.
6. Còn lại / follow-up (chưa làm)
- Mã hoá trong UI: engine có sẵn
encrypt_with_password/ensure_openablenhưng frontend luôn truyềnpassword: null, chưa có hộp nhập mật khẩu lúc mở, chưa nối lại mã hoá sau khi lưu. → File có mật khẩu thực tế chưa mở/lưu được qua UI (DoD "ghi không làm hỏng file mã hoá" mới đạt ở tầng engine/test, chưa ở tầng app). Cần: prompt mật khẩu khiensure_openable/mở lỗi do mã hoá; sau khiorganize_apply, nếu file gốc có mật khẩu thì gọiencrypt_with_passwordáp lại. Cân nhắc quy mô lớn (đụng tới mọi command mở/render/search) — để làm riêng, không gộp vào lần rà soát này. - Live preview render thật cho Insert/Extract/Replace/Crop (hiện Crop dùng số liệu margin nhập tay; Insert/Extract/Replace không có preview).
- Tách theo outline cấp 1 (hiện chỉ có "mỗi N trang").
- Watermark ảnh (hiện chỉ có watermark text).
- Incremental save thật giữ chữ ký số (để dành Phase 5 theo đúng kế hoạch ban đầu).