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.
This commit is contained in:
2026-07-14 22:07:38 +07:00
parent 274012f854
commit e442a9f678
26 changed files with 3924 additions and 40 deletions
+6 -6
View File
@@ -12,30 +12,30 @@ Một ứng dụng desktop có thể **xem, chú thích, tổ chức trang, và
---
## Phase 0 — Nghiên cứu & Kế hoạch ✅ (đang ở đây)
## Phase 0 — Nghiên cứu & Kế hoạch ✅ HOÀN TẤT
**DoD:** docs 01–03 hoàn tất; chốt ngôn ngữ/framework; dựng skeleton repo.
## Phase 1 — Nền tảng & Viewer (MVP xem được)
## Phase 1 — Nền tảng & Viewer (MVP xem được) ✅ HOÀN TẤT
**Mục tiêu:** Mở và hiển thị PDF chính xác, mượt.
- Tích hợp PDFium (prebuilt) qua FFI; dựng Core API tối thiểu (`open`, `pageCount`, `renderPage`).
- Viewer: scroll liên tục, zoom/fit, thumbnails, outline/bookmarks, **tìm kiếm text**, chọn & copy text.
- Mở file lớn (lazy render + cache tile).
**DoD:** mở 20 file mẫu (gồm file 1000+ trang, CJK, Việt) không crash; render khớp mắt thường với Chrome; search & copy hoạt động; có test render-hash cho trang mẫu.
## Phase 2 — Chú thích (Annotate)
## Phase 2 — Chú thích (Annotate) ✅ HOÀN TẤT (lõi) — xem [docs/08-phase2-summary.md](08-phase2-summary.md)
**Mục tiêu:** Bộ comment dùng hằng ngày.
- Highlight/underline/strikethrough, sticky note, text box, free draw, shapes, stamp.
- Lưu annotation vào PDF (annotation dict), import/export **XFDF**.
- Hiển thị & sửa/xoá annotation; danh sách comment ở sidebar.
**DoD:** tạo/sửa/xoá mọi loại trên; lưu & mở lại giữ nguyên; Acrobat/Foxit đọc được annotation ta ghi; export/import XFDF round-trip.
## Phase 3 — Tổ chức trang (Organize) + Lưu file vững
## Phase 3 — Tổ chức trang (Organize) + Lưu file vững ✅ HOÀN TẤT (lõi, 36/36 test) — xem [docs/10-phase3-summary.md](10-phase3-summary.md)
**Mục tiêu:** Quản lý trang & ghi file an toàn.
- Chèn/xoá/xoay/trích/thay/đảo trang; **merge & split**; crop; đánh số; header/footer; watermark/background.
- Engine ghi file: incremental update **và** full rewrite (qua QPDF); giữ không hỏng file; **undo/redo** toàn cục.
**DoD:** mọi thao tác trang round-trip đúng; merge 50 file OK; ghi không làm hỏng file mã hoá; undo/redo ổn định; test so khớp cấu trúc trang.
## Phase 4 — ⭐ Chỉnh sửa nội dung (Edit) — TÍNH NĂNG LÕI
## Phase 4 — ⭐ Chỉnh sửa nội dung (Edit) — TÍNH NĂNG LÕI ✅ Iteration 1 HOÀN TẤT (43/43 test) — xem [docs/12-phase4-summary.md](12-phase4-summary.md)
**Mục tiêu:** Sửa text & object trực tiếp — moat chính.
- **Page Object Model**: parse content stream → cây object (text/path/image/xobject).
- Sửa **ảnh/object**: di chuyển, resize, xoá, thêm ảnh (dễ hơn → làm trước).
@@ -43,7 +43,7 @@ Một ứng dụng desktop có thể **xem, chú thích, tổ chức trang, và
- Thêm text box mới, đổi font/size/màu.
**DoD:** sửa được text 1 dòng & 1 đoạn trên ≥80% file mẫu thường; thêm/xoá/di chuyển ảnh; lưu mở lại đúng; tiếng Việt hiển thị & sửa đúng dấu; có test corpus chuyên cho edit.
## Phase 5 — Bảo mật & Chữ ký
## Phase 5 — Bảo mật & Chữ ký ⏳ KẾ TIẾP (đang ở đây)
**Mục tiêu:** Tính năng đáng tiền cho doanh nghiệp.
- Mật khẩu mở/permission; mã hoá **AES-256**; xoá mật khẩu.
- **Redaction thật** (xoá nội dung + ảnh + metadata, không chỉ vẽ đè) + kiểm chứng đã xoá.
+20 -4
View File
@@ -7,6 +7,7 @@
- **Visual Studio Build Tools** + workload **Desktop development with C++** (cung cấp `cl.exe`/`link.exe` cho linker MSVC).
- **WebView2 Runtime** (cho Tauri; Windows 11 thường có sẵn).
- **PDFium**: `pdfium.dll` prebuilt — tải bằng `scripts/fetch-pdfium.ps1` (đặt vào gốc workspace + `pdfium/`).
- **QPDF** (từ Phase 3): `qpdf.exe` prebuilt — tải bằng `scripts/fetch-qpdf.ps1`. Dùng để sửa file hỏng (repair) trước khi PDFium mở + mã hoá lại file sau khi chỉnh sửa.
- (Tauri) Node không bắt buộc: app dùng frontend tĩnh, chạy thẳng bằng `cargo run`.
## Lấy PDFium
@@ -16,16 +17,29 @@ powershell -File scripts/fetch-pdfium.ps1
```
Engine tìm `pdfium.dll` theo thứ tự: `FOFREEXIT_PDFIUM_PATH` → cwd → `./pdfium` → thư mục exe → thư viện hệ thống.
## Lấy QPDF
```powershell
# Tải qpdf.exe (prebuilt) về workspace
powershell -File scripts/fetch-qpdf.ps1
```
Engine tìm `qpdf.exe` theo thứ tự: `FOFREEXIT_QPDF_PATH` → `<cwd>/qpdf/bin/qpdf.exe` → cạnh file exe → `qpdf` trong PATH hệ thống (xem `crates/ff-engine/src/qpdf.rs`).
## Cấu trúc
```
crates/ff-cos # COS layer (khung)
crates/ff-pdmodel # PD layer (khung)
crates/ff-engine # render qua PDFium (Phase 1: render.rs)
crates/ff-cli # CLI "ff" (info, render)
crates/ff-engine # render + chỉnh sửa qua PDFium/QPDF
render.rs # Phase 1: render trang, page_dims, extract_text, search, outline
annot.rs # Phase 2: chú thích (highlight/note/shape/free-draw...)
organize.rs # Phase 3: tổ chức trang (insert/delete/rotate/crop/merge/split)
watermark.rs # Phase 3: watermark + header/footer
qpdf.rs # Phase 3: repair file hỏng + mã hoá lại (QPDF CLI)
edit.rs # Phase 4: sửa nội dung (text run/object) trực tiếp
crates/ff-cli # CLI "ff" (info, render, pages, text, search, outline)
app/src # frontend tĩnh (HTML/CSS/JS)
app/src-tauri # backend Tauri (crate riêng, workspace tách)
corpus/ # PDF mẫu để test
scripts/ # tiện ích (fetch-pdfium)
scripts/ # tiện ích (fetch-pdfium, fetch-qpdf)
```
## CLI
@@ -46,8 +60,10 @@ cargo run -p ff-cli -- outline corpus\sample-multipage.pdf
## Test
```powershell
cargo test -p ff-engine # smoke test render (cần pdfium.dll)
cargo test -p ff-engine # toàn bộ test engine (cần pdfium.dll + qpdf.exe)
```
Bao gồm: render smoke, viewer features, big-doc, annot round-trip (Phase 1–2),
organize/watermark/qpdf-safety (Phase 3), edit round-trip (Phase 4). Hiện **43/43 xanh**.
## Chạy app desktop (Tauri)
```powershell
+60
View File
@@ -0,0 +1,60 @@
# 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).
+43
View File
@@ -0,0 +1,43 @@
# 11 — Checklist test thủ công Phase 3 (Tổ chức trang)
Phần kéo-thả/kéo-chuột không tự động test đầy đủ được trong WebView2 (xem ghi chú ở [08-phase2-summary.md](08-phase2-summary.md)), nên cần bạn thử bằng chuột thật, đặc biệt phần Lưu (đường GHI file là rủi ro nhất).
## Chạy
```powershell
cd c:\Project\FoFreeXit\app\src-tauri
cargo run
```
## Bảng test
| # | Thao tác | Kỳ vọng | KQ |
|---|----------|---------|----|
| O1 | Bấm **🗂 Tổ chức trang** | Viewport đổi thành lưới thumbnail lớn | |
| O2 | Click 1 trang, Ctrl+click thêm 1 trang | Cả 2 viền xanh (đã chọn) | |
| O3 | Shift+click trang khác | Chọn cả dải liên tục | |
| O4 | Kéo-thả 1 thumbnail sang vị trí khác | Thứ tự trang đổi ngay trên lưới | |
| O5 | Chọn 1 trang, bấm **Xoay phải/trái** | Thumbnail xoay 90° ngay | |
| O6 | Bấm **Hoàn tác** sau O5 | Xoay về như cũ | |
| O7 | Chọn trang, bấm **Xoá** | Trang biến mất khỏi lưới | |
| O8 | Chọn hết tất cả trang, bấm **Xoá** | Bị chặn (báo không thể xoá hết) | |
| O9 | Bấm **➕ Chèn** → Trang trắng → Cuối tài liệu | Thêm 1 trang trắng cuối lưới | |
| O10 | Bấm **➕ Chèn** → Từ file… → chọn 1 PDF khác | Trang từ file đó chèn đúng vị trí | |
| O11 | Chọn 1+ trang, bấm **📤 Trích** → chọn nơi lưu | Tạo file PDF mới chỉ chứa trang đã chọn | |
| O12 | Chọn 1+ trang, bấm **🔁 Thay** → chọn file khác | Nội dung trang đã chọn đổi thành trang từ file mới | |
| O13 | Bấm **🔀 Trộn file** → thêm 2-3 file, sắp thứ tự, Trộn | Tạo 1 file PDF mới ghép đúng thứ tự | |
| O14 | Bấm **✂ Tách file** → nhập số trang/file, chọn thư mục | Sinh nhiều file `_part1.pdf, _part2.pdf...` | |
| O15 | Bấm **💧 Watermark**, nhập chữ, bấm **Xem trước** | Ảnh xem trước hiện watermark đúng vị trí/màu/góc xoay | |
| O16 | Bấm **Áp dụng** ở Watermark → chọn nơi lưu | File mới có watermark trên các trang đã chọn | |
| O17 | Bấm **📋 Header/Footer**, gõ "Trang {page}/{total}" vào ô dưới-giữa, **Xem trước** | Ảnh xem trước hiện đúng số trang/tổng số trang | |
| O18 | Chọn tool **✂️ Crop** (thanh Chú thích), kéo 1 khung trên trang | Dialog Crop hiện 4 số margin đúng theo khung đã kéo | |
| O19 | Áp dụng Crop, vào lại Tổ chức trang | Thumbnail trang đó có dấu chấm crop ở góc | |
| O20 | Sau khi sửa vài thao tác, bấm **💾 Lưu thay đổi tổ chức trang** | Hỏi nơi lưu, tạo file mới đúng với mọi thay đổi đã làm | |
| O21 | Mở file đã lưu ở O20 bằng **Foxit/Adobe/Chrome** | Thứ tự/xoay/crop/nội dung trang đúng như trên lưới | |
| O22 | Mở 1 file PDF bị hỏng (xref lỗi) nếu có | App tự sửa (qua QPDF) và mở được, không báo lỗi cho người dùng | |
## Mẫu phản hồi
```
[Mã] (vd O21) — Phần mềm mở: <Foxit/Adobe/Chrome> — Hiện tượng: <mô tả> —
Kỳ vọng: <...> — Ưu tiên: cao/vừa/thấp
```
> Đặc biệt cần biết **O20/O21** (lưu tổ chức trang ra file mới, mở lại ở phần mềm khác có đúng không) vì đó là đường GHI file rủi ro nhất. Engine đã test round-trip nội bộ cho từng thao tác riêng lẻ, nhưng tổ hợp nhiều thao tác liên tiếp + cross-viewer chưa có test tự động.
+40
View File
@@ -0,0 +1,40 @@
# 12 — Tổng kết Phase 4 (Chỉnh sửa nội dung — Edit) · Iteration 1
> Trạng thái: **lõi MOAT hoạt động**. 43/43 test engine xanh (36 cũ + 7 mới cho edit). Đã chạy app thật, chụp ảnh xác minh: sửa text tiếng Việt WYSIWYG, thêm chữ, xoá đối tượng, undo/redo.
Phase 4 là tính năng lõi/moat theo `docs/03-roadmap.md`: sửa text & object trực tiếp trên trang — lý do quan trọng nhất để bỏ Foxit.
## 1. Khảo sát trước khi code
- **Foxit UX**: *Edit Text* (bấm đoạn → sửa như Word, reflow, đổi font/cỡ/màu); *Edit Object* (chọn text/ảnh/path → di chuyển/resize/xoay/xoá/thay ảnh, tab Format). Nguồn ở `docs/13-phase4-user-tests.md`.
- **pdfium-render 0.8.37** (đọc source vendored): PDFium đã expose page object cấp cao — KHÔNG cần tự parse content stream. Mỗi `PdfPageTextObject` là 1 **text run** sẵn để sửa (`text()`/`set_text()`/`font()`/`unscaled_font_size()`/fill color/transform). Có `create_text_object`/`create_image_object`/`remove_object_at_index`, `PdfFont::name/family/weight`, `regenerate_content()`. → khả thi cao, vượt kỳ vọng worst-case của roadmap.
## 2. Đã làm
### Engine (`crates/ff-engine/src/edit.rs`)
- `list_objects(input, page, password) -> Vec<ObjectInfo>`: liệt kê object (kind, AABB từ `bounds()`, và với text: nội dung/font/cỡ/màu) — cấp dữ liệu cho overlay UI.
- `apply_edits(input, page, ops, output, password)` với `EditOp`: **Transform** (di chuyển + scale quanh góc dưới-trái), **SetText** (sửa text run — tạo lại bằng FULL font nhúng để tiếng Việt đúng dấu, giữ matrix/cỡ/màu gốc qua `apply_matrix`), **Delete**, **ReplaceImage**, **AddText**, **AddImage**. Thứ tự xử lý giữ index gốc hợp lệ: Transform in-place → chụp dữ liệu object sắp thay → xoá theo index GIẢM DẦN → thêm bản thay thế → thêm object mới → `regenerate_content()` → lưu.
- Tái dùng `annot.rs::find_font_bytes` + `fonts_mut().load_true_type_from_bytes(bytes, true)` (như watermark.rs) cho sửa/thêm text Unicode.
- **Bài học quan trọng (đã ghi memory):** object trả về từ `remove_object_at_index` bị đánh dấu *unowned* → `Drop` gọi `FPDFPageObj_Destroy` gây **SEGFAULT** với PDFium build hiện tại. Phải `std::mem::forget` object đã xoá (đã tách khỏi trang, không cần destroy; rò rỉ nhỏ giải phóng khi đóng document).
### Tauri commands (`app/src-tauri/src/main.rs`)
`edit_list_objects`, `edit_apply` (ghi ra output), `edit_apply_to_temp` (áp ops → file tạm mới, trả path — cho mô hình materialize tức thì ở UI), `edit_preview` (render WYSIWYG), `pick_image`. DTO `ObjectInfoDto` (reuse `RectDto`), `EditOpDto` (tagged theo field `op`).
### UI (`app/src`) — chế độ "✏️ Sửa nội dung"
- Nút toolbar bật chế độ riêng: thay viewport bằng `#editStage` (ảnh trang lớn) + `#editOverlay` (1 box/đối tượng, map pdf→css theo `scale = STAGE_W/pageWidthPt`).
- **Mô hình "materialize tức thì"**: mỗi thao tác áp NGAY vào 1 file tạm mới (`edit_apply_to_temp`) rồi đọc lại object + render ảnh từ đó → index luôn khớp ảnh đang hiện (WYSIWYG thật), không cần tự suy đoán vị trí sau biến đổi.
- Thao tác: click chọn (viền xanh + handle); **double-click text → sửa tại chỗ** (ô input, commit Enter → SetText); đổi **cỡ chữ/màu** ở thanh công cụ cho text đang chọn; **Thêm chữ** (bấm nút → click lên trang → gõ); **Thêm ảnh**/**Thay ảnh** (chọn file → đặt); **Xoá** (nút/phím Delete); **kéo di chuyển / kéo handle resize** (chuột thật).
- **Undo/Redo riêng cho edit** (stack file tạm trước mỗi op), dùng chung 2 nút Hoàn tác/Làm lại + Ctrl+Z/Ctrl+Y khi đang ở chế độ sửa.
- **Lưu**: `edit_apply` ghi file mới rồi `loadDocument`.
## 3. Test tự động (7 mới, `tests/edit_roundtrip.rs`)
list_objects thấy text trang 1; SetText đổi nội dung (text cũ biến mất); SetText **tiếng Việt** round-trip đúng dấu; Delete giảm đúng 1 object; Transform translate dịch bounds ~+50; AddText xuất hiện trong extract_text; AddImage tăng 1 object kind Image. Tổng `ff-engine`: **43/43 xanh**.
## 4. Đã kiểm bằng ảnh (app build release thật)
Bật chế độ Sửa nội dung (overlay 2 đối tượng); double-click dòng 2 → sửa thành "Sửa: nội dung Tiếng Việt" → commit → trang render lại ĐÚNG (dấu chuẩn); Thêm chữ "Dòng chữ MỚI thêm" tại vị trí click; Hoàn tác bỏ chữ vừa thêm (giữ phần sửa); Xoá tiêu đề → còn 1 đối tượng. Ảnh: `tmp-out/phase4-*.png`.
## 5. Còn lại / follow-up (ghi nhận, không phải thiếu sót — Iteration sau)
- **Reflow đoạn nhiều dòng "như Word"** (hiện sửa ở mức text run/dòng — mỗi PdfPageTextObject; đây là điểm khác chính so với Foxit, cần gom run theo baseline + reflow).
- Xoay/lật/shear/clip object; tab Format nâng cao (viền, opacity, căn lề, đổi font-family object cũ); z-order arrange; convert text→path.
- Đặt ảnh theo đúng tỉ lệ gốc (hiện mặc định 150×112pt, resize bằng kéo handle); preview ảnh trước khi đặt.
- Sửa nhiều trang trong 1 phiên lưu (hiện lưu theo trang đang sửa); spell-check; sửa bảng; link text blocks.
- Mã hoá UI (nợ từ Phase 3).
+39
View File
@@ -0,0 +1,39 @@
# 13 — Checklist test thủ công Phase 4 (Sửa nội dung)
Kéo-chuột (di chuyển/resize) không tự động test đầy đủ trong WebView2 (xem `docs/08`), nên cần bạn thử bằng chuột thật. Đặc biệt cần xác nhận **lưu ra file mở lại đúng** và **tiếng Việt đúng dấu** ở phần mềm khác.
## Chạy
```powershell
cd c:\Project\FoFreeXit\app\src-tauri
cargo run
```
## Bảng test
| # | Thao tác | Kỳ vọng | KQ |
|---|----------|---------|----|
| E1 | Bấm **✏️ Sửa nội dung** | Viewport đổi thành trang lớn + khung mờ quanh từng đối tượng (text/ảnh) | |
| E2 | Bấm 1 lần vào 1 khung text | Khung viền xanh (đã chọn); ô Cỡ chữ + Màu bật lên | |
| E3 | Bấm đúp (double-click) 1 khung text | Hiện ô sửa tại chỗ với nội dung cũ | |
| E4 | Sửa thành chữ có dấu tiếng Việt, Enter | Trang render lại đúng nội dung mới, **đúng dấu** | |
| E5 | Chọn text rồi đổi **Cỡ chữ** | Chữ đổi kích thước | |
| E6 | Chọn text rồi đổi **Màu** | Chữ đổi màu | |
| E7 | Bấm **Thêm chữ** → click lên trang → gõ → Enter | Chữ mới xuất hiện đúng chỗ | |
| E8 | Bấm **Thêm ảnh** → chọn ảnh → click lên trang | Ảnh xuất hiện tại chỗ click | |
| E9 | Chọn 1 ảnh → **Thay ảnh** → chọn ảnh khác | Ảnh được thay, giữ khung cũ | |
| E10 | Chọn 1 đối tượng → **Kéo** để di chuyển | Đối tượng dời theo chuột | |
| E11 | Chọn 1 đối tượng → kéo **ô vuông góc** để resize | Đối tượng to/nhỏ lại | |
| E12 | Chọn 1 đối tượng → phím **Delete** (hoặc nút Xoá) | Đối tượng biến mất | |
| E13 | **Hoàn tác (Ctrl+Z)** sau vài thao tác | Lùi lại từng bước đúng | |
| E14 | **Làm lại (Ctrl+Y)** | Tiến lại bước vừa hoàn tác | |
| E15 | **Lưu thay đổi nội dung** → chọn nơi lưu | Tạo file PDF mới; app mở lại file đó | |
| E16 | Mở file đã lưu ở **Foxit/Adobe/Chrome** | Nội dung đã sửa hiển thị đúng (text, ảnh, **tiếng Việt đúng dấu**), file không hỏng | |
## Mẫu phản hồi
```
[Mã] (vd E16) — Phần mềm mở: <Foxit/Adobe/Chrome> — Hiện tượng: <mô tả> —
Kỳ vọng: <...> — Ưu tiên: cao/vừa/thấp
```
> Quan trọng nhất: **E4** (sửa tiếng Việt), **E15/E16** (lưu + mở lại cross-viewer) — đường GHI nội dung là rủi ro nhất. Engine đã test round-trip nội bộ (gồm tiếng Việt), nhưng xác nhận cross-viewer của bạn rất giá trị.
> Giới hạn đã biết (Iteration 1): sửa text ở mức **dòng/run** (chưa reflow cả đoạn như Word); thêm ảnh mặc định khung 150×112pt (kéo handle để chỉnh); lưu áp cho trang đang sửa. Xem `docs/12-phase4-summary.md` mục 5.