Files
FoFreeXit/docs/10-phase3-summary.md
admin e442a9f678 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.
2026-07-14 22:07:38 +07:00

11 KiB
Raw Permalink Blame History

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ựng plan khá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_footer dùng PdfPages::watermark(closure) + font TrueType nạp làm CID Unicode (tái dùng find_font_bytes từ 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ọi qpdf.exe (binary prebuilt, scripts/fetch-qpdf.ps1) qua std::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à loadDocument phía UI.
  • Bài học kỹ thuật quan trọng (đã ghi vào memory dự án):
    • qpdf.exe hiể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/PdfDocument còn sống sẽ làm "kẹt" (poison) global mutex của ThreadSafePdfiumBindings cho hết tiến trình test — mọi test PDFium sau đó báo lỗi thread_safe.rs:76 khô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 #viewport bằ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ào pagePlan (chỉ ghi ra file thật khi bấm Lưu).
  • Mở file vững hơn: loadDocument thử mở thẳng, lỗi thì gọi ensure_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_openable tự 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_features 6, big_doc 3, render_smoke 1, annot_roundtrip 6).
  • ff-cli khô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/orgRotateSelected gọi buildOrganizeGrid() → xoá sạch lưới và render_page lạ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 CSS transform của card liên quan; thêm cache thumbnail state.orgThumbs (khoá theo source#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 command organize_materialize(mainInput, plan, password) (tái dùng build_document có 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 khi planIsDirty() và dùng file tạm đó làm input cho 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 theo pagePlan.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.
  • [Hiệu năng] split_by_page_count mở file nguồn đúng 1 lần thay vì gọi build_document mỗ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. Test split_by_page_count_produces_correct_chunks vẫ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 --release chạy từ gốc repo chỉ build crates/*, KHÔNG build app (dễ verify nhầm trên exe cũ). Phải dùng cargo 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_openable nhưng frontend luôn truyền password: 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 khi ensure_openable/mở lỗi do mã hoá; sau khi organize_apply, nếu file gốc có mật khẩu thì gọi encrypt_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).