Engine:
- EditOp::ReflowText { indices, text }: engine tự suy hình học từ các run
(lề trái/bề rộng từ bounds, baseline từ matrix.f gom cụm ±1pt, khoảng cách
dòng = median hiệu baseline); bẻ dòng greedy đo bằng hmtx của font thật
(fontmatch::wrap_lines + char_advance, ttf-parser); \n = ngắt dòng cứng,
từ quá dài cắt theo ký tự, dòng rỗng giữ nhịp baseline
- Giữ font theo thang 4 mức nhất quán với iteration 2: (1) font nhúng parse
được + phủ đủ glyph → nhúng lại chính bytes đó; (2) family base-14 + ASCII
→ font chuẩn PDF qua FPDFText_LoadStandardFont (BaseFont giữ tên chuẩn,
không nhúng, file không phình; đo bằng font metric-compatible); (3) font
hệ thống cùng họ; (4) fallback. FontLoad enum Bytes/Builtin cho pha nạp font
- Dòng mới giữ phần tuyến tính matrix của run neo (scale/nghiêng), Tf =
unscaled run neo → cỡ hiển thị không đổi
UI + Tauri:
- paragraphLines: gom đoạn nhiều dòng (baseline cách đều ±25%, cỡ chữ tương
đồng, giao ngang ≥30%); double-click đoạn ≥2 dòng → textarea sửa CẢ ĐOẠN
WYSIWYG (đúng font/cỡ/màu/kiểu + line-height đúng nhịp baseline)
- Enter = xuống dòng cứng · Ctrl+Enter/bấm ra ngoài = áp dụng · Esc = huỷ;
1 commit = 1 op reflowText = 1 nấc undo
- DTO reflowText (indices) trong Tauri
Test: 3 integration mới (wrap giữ font nhúng + tiếng Việt + baseline đều;
hard break; base-14 giữ BaseFont Helvetica chuẩn) + 3 unit wrap_lines.
Toàn bộ suite engine ngoài qpdf: 52/52 xanh.
Docs: 12 (Iteration 3 + giới hạn v1), 13 (E26–E30), 14 (mục 3.1 → đã giải,
cập nhật thứ tự việc tiếp theo), README.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgnbPwSoRGNH7LHsDUmRpk
148 lines
12 KiB
Markdown
148 lines
12 KiB
Markdown
# 14 — Phân tích khoảng cách so với Foxit: định hướng, công nghệ, tiến độ
|
||
|
||
> Trả lời câu hỏi chiến lược: *"Với tham vọng thay thế Foxit, đạt chất lượng
|
||
> tương đương hoặc hơn về trải nghiệm và tính năng, thì định hướng, công nghệ
|
||
> và tiến độ hiện tại có đáp ứng được không? Có vấn đề gì khiến các tính năng
|
||
> không đáp ứng được như Foxit không?"*
|
||
>
|
||
> Cập nhật lần đầu: sau Phase 4 Iteration 2 (giữ font khi sửa text).
|
||
|
||
## 1. Kết luận ngắn
|
||
|
||
**Định hướng và công nghệ: ĐÁP ỨNG ĐƯỢC, không có rào cản chết người.**
|
||
Stack Rust + Tauri + PDFium + QPDF đủ trần năng lực để đạt (và ở vài điểm vượt)
|
||
Foxit cho toàn bộ lộ trình Phase 1–8. Lý do tin được điều đó:
|
||
|
||
- **PDFium là engine của Chrome** — render/parse text/font ngang hàng engine
|
||
thương mại; Foxit bán chính engine của họ cho Google làm gốc PDFium. Ta đứng
|
||
trên cùng một nền engine với đối thủ.
|
||
- Những chỗ PDFium *không* expose đủ (ví dụ annotation appearance, /W width
|
||
của font Type0, coverage cmap khi sửa text) đều đã xử lý được bằng cách
|
||
**tự thao tác tầng PDF object qua `lopdf`/`ttf-parser`** (annot.rs,
|
||
fontmatch.rs) — đúng thứ tự leo thang của FINAL TARGET & RULE, và đã chứng
|
||
minh trong code chứ không phải giả định.
|
||
- Các bài toán "khó nhất" của một PDF editor (sửa text giữ font, tổ chức trang
|
||
an toàn, file hỏng/mã hoá) đã có lời giải chạy được + test.
|
||
|
||
**Tiến độ: đúng hướng nhưng mới đi ~40% quãng đường tính năng.** Phase 1–4
|
||
(viewer, annotate, organize, edit lõi) là "core thay Foxit cho cá nhân" và đã
|
||
xong ở mức lõi. Phần "đáng tiền doanh nghiệp" (bảo mật/chữ ký số, form, OCR,
|
||
convert) chưa bắt đầu — đó là các phase 5–7 theo roadmap, không phải rủi ro
|
||
công nghệ mà là khối lượng việc.
|
||
|
||
## 2. Những vấn đề TỪNG khiến tính năng không đạt chuẩn Foxit — và cách đã giải
|
||
|
||
| Vấn đề | Hậu quả với người dùng | Giải pháp đã áp dụng (đã có test) |
|
||
|---|---|---|
|
||
| PDFium `set_text` re-encode theo font subset → mất glyph ngoài subset | Sửa text là **đổi font toàn bộ run** (Times → Arial), lệch hẳn tài liệu; chỉ đổi cỡ/màu cũng đổi font; mất đậm/nghiêng | **Iteration 2**: quyết định 3 tầng — (1) sửa TẠI CHỖ giữ nguyên font khi chắc chắn an toàn (charset-subset / base-14 + ASCII / cmap của font bytes phủ đủ); (2) thiếu glyph thật → thay font **cùng họ** metric-compatible (bảng family + fc-match + alias base-14); (3) mới đến font mặc định. Đổi cỡ = scale matrix, không đụng font |
|
||
| Cỡ chữ hiển thị vs Tf size lẫn lộn khi matrix có scale | Đặt cỡ 20 trên text scale ×2 → chữ 40pt (phóng đại kép) | Quy ước cỡ = "hiển thị"; in-place scale matrix neo baseline; tạo lại thì quy đổi ngược Tf trước khi áp matrix. Test hồi quy riêng |
|
||
| PDF cắt 1 dòng nhìn thấy thành nhiều text run | Double-click chỉ sửa được 1 mảnh chữ — trải nghiệm vụn | UI gom run cùng baseline liền kề → sửa **cả dòng**; commit 1 batch (SetText + Delete) = 1 nấc undo |
|
||
| Ô sửa inline dùng font mặc định của WebView | Đang gõ không giống kết quả (không WYSIWYG) | Ô sửa nhận đúng family/cỡ/màu/kiểu của run (CSS xấp xỉ theo `font_family` engine trả về) |
|
||
| Kéo-thả không có phản hồi trực tiếp | Cảm giác "lag", không giống Foxit | Khung đi theo con trỏ ngay khi kéo; thả mới commit |
|
||
| Annotation FreeText hiển thị sai width tiếng Việt ở viewer khác | File ghi ra "xấu" ở Acrobat | Tự dựng /W (widths) cho font Type0 nhúng bằng `ttf-parser` (Phase 2) |
|
||
| PDFium segfault khi Drop object đã remove | Crash khi xoá object | `std::mem::forget` object đã tách (đã ghi memory + comment) |
|
||
| File hỏng/mã hoá làm hỏng luồng lưu | Mất file người dùng | QPDF repair + mã hoá lại an toàn (Phase 3), test riêng |
|
||
|
||
**Bài học chung:** chưa gặp vấn đề nào *không giải được* trên stack này. Mọi
|
||
"thư viện không hỗ trợ" đến nay đều xử lý được bằng đọc source thư viện + thao
|
||
tác tầng PDF object trực tiếp.
|
||
|
||
## 3. Khoảng cách CÒN LẠI so với Foxit + giải pháp cụ thể
|
||
|
||
Xếp theo mức độ ảnh hưởng trải nghiệm:
|
||
|
||
### 3.1 Reflow đoạn văn nhiều dòng ("sửa như Word") — ✅ ĐÃ GIẢI (Iteration 3)
|
||
- **Đã làm đúng kế hoạch:** UI gom block theo baseline cách đều + giao ngang;
|
||
engine `EditOp::ReflowText` tự suy hình học từ matrix/bounds các run, đo
|
||
width bằng `hmtx` (ttf-parser), bẻ dòng greedy (`\n` = ngắt cứng), tạo lại
|
||
các dòng đúng baseline spacing; font giữ theo thang 4 mức (bytes nhúng →
|
||
font chuẩn base-14 qua `FPDFText_LoadStandardFont` → cùng họ → fallback).
|
||
3 test integration + 3 unit — xem `docs/12` mục Iteration 3.
|
||
- **Còn lại của mục này (v2):** đoạn justify reflow về căn trái; chưa kerning;
|
||
khối text xoay chưa reflow theo hướng xoay; đoạn lẫn nhiều font thống nhất
|
||
theo run neo. Đây là các case Foxit xử lý tốt hơn — nâng khi gặp tài liệu
|
||
thực tế, không chặn trải nghiệm chính.
|
||
|
||
### 3.2 Hiệu năng phiên sửa trên file rất lớn
|
||
- **Hiện tại:** mô hình "materialize tức thì" — mỗi thao tác lưu toàn bộ tài
|
||
liệu ra file tạm mới rồi render lại. Đúng tuyệt đối (WYSIWYG thật, undo chắc
|
||
chắn), nhưng file vài trăm MB sẽ trễ nhìn thấy được ở mỗi thao tác; Foxit
|
||
chỉnh trong RAM và lưu 1 lần.
|
||
- **Giải pháp theo nấc (không phá kiến trúc):**
|
||
1. (rẻ) Giữ document PDFium mở suốt phiên sửa trong Tauri state, áp op
|
||
in-memory, chỉ render lại trang — bỏ chu kỳ load-từ-đĩa mỗi op; save ra
|
||
temp chỉ để làm mốc undo (có thể async).
|
||
2. (vừa) Undo bằng danh sách op thay vì snapshot file: replay từ gốc khi
|
||
undo (op rẻ vì in-memory).
|
||
3. (nếu cần) Incremental save của QPDF cho file khổng lồ.
|
||
- **Ghi chú:** với file văn phòng thông thường (<20MB) mô hình hiện tại đã đủ
|
||
mượt; nấc 1 chỉ là việc tổ chức lại Tauri command, không đổi engine.
|
||
|
||
### 3.3 Lưu file sau sửa: font nhúng full + object rác
|
||
- **Hiện tại:** khi phải thay font (tầng 2/3), font mới nhúng **nguyên cả file
|
||
TTF** (Arial ~1MB); object bị thay vẫn nằm lại file dưới dạng rác (đã tách
|
||
khỏi trang nên không hiển thị, nhưng tốn dung lượng). Foxit subset font khi
|
||
lưu và dọn object rác.
|
||
- **Giải pháp:** bước "Lưu tối ưu" chạy khi save: (1) subset font nhúng theo
|
||
đúng glyph dùng thật — tự viết được trên `ttf-parser` (đã dựng /W thủ công ở
|
||
annot.rs, subset TTF là bài quen thuộc: lọc glyf/loca/cmap/hmtx); (2) chạy
|
||
`qpdf --object-streams=generate` (đã có sẵn qpdf.rs) để dọn rác + nén object
|
||
stream. Đưa vào Phase 5 cùng nhóm "lưu file vững".
|
||
|
||
### 3.4 Độ phủ font hệ thống khi thay thế
|
||
- **Hiện tại:** bảng family phổ biến (Windows ~16 họ, macOS, Linux
|
||
Liberation + fc-match). Font ngoài bảng → fallback mặc định.
|
||
- **Giải pháp:** Windows dùng DirectWrite/GDI enumeration (crate `font-kit`
|
||
hoặc đọc registry Fonts) để match theo TÊN THẬT của mọi font đã cài thay vì
|
||
bảng tĩnh; giữ bảng tĩnh làm fallback offline. Việc nhỏ, độc lập, làm khi
|
||
chạm phải file thực tế đầu tiên không match.
|
||
|
||
### 3.5 Các tính năng Foxit chưa bắt đầu (không phải rào cản kỹ thuật)
|
||
| Nhóm | Đường đi đã chốt trong roadmap | Rủi ro công nghệ |
|
||
|---|---|---|
|
||
| Bảo mật, redaction, chữ ký số PAdES (Phase 5) | QPDF (mã hoá AES-256 sẵn), redaction = xoá object thật (đã có nền edit.rs), ký số qua `cryptographic-message-syntax`/`rustls` + incremental update | Trung bình — byte-range + incremental update phải chuẩn; có spec rõ, test bằng Acrobat |
|
||
| Form AcroForm (Phase 6) | PDFium có FPDF_FORMFILL đầy đủ (fill/flatten); tạo field mới bằng lopdf | Thấp-trung bình |
|
||
| OCR (Phase 7) | Tesseract (tiếng Việt có traineddata chuẩn) + lớp text ẩn — kỹ thuật giống watermark.rs đã làm | Thấp, chủ yếu là chất lượng tiền xử lý ảnh |
|
||
| Convert Office↔PDF (Phase 7) | LibreOffice headless; PDF→Word "đủ dùng" trước | PDF→Word giữ layout hoàn hảo là bài khó với CẢ Foxit — đặt kỳ vọng "tương đương Foxit" chứ không hơn |
|
||
| So sánh PDF, in ấn, installer (Phase 8) | render diff đã có nền; Tauri bundler | Thấp |
|
||
|
||
## 4. Đánh giá định hướng công nghệ (giữ hay đổi?)
|
||
|
||
**GIỮ.** Đánh giá lại từng lựa chọn sau 4 phase:
|
||
|
||
- **PDFium qua `pdfium-render`**: đúng đắn. Đã 2 lần tưởng "không làm được"
|
||
(annotation appearance, giữ font khi sửa) và cả 2 lần đều giải được bằng
|
||
API sẵn có + tự bù tầng PDF object. Trần năng lực còn xa.
|
||
*Lưu ý vận hành:* `pdfium-render` 0.8.37 có API deprecated đổi nghĩa
|
||
(`set_matrix` = apply, không phải replace) — đã ghi chú trong code; khi nâng
|
||
version cần đọc CHANGELOG cẩn thận.
|
||
- **Tauri/WebView UI**: đủ mượt cho viewer (lazy render 1000+ trang đã chứng
|
||
minh ở Phase 1). Điểm cần canh: thao tác chuột mô phỏng trong WebView2 khó
|
||
test tự động → bù bằng checklist test tay (docs/13) — chấp nhận được.
|
||
- **QPDF sidecar**: gọn, đúng việc (repair/mã hoá/nén), không cần thay.
|
||
- **Rust workspace tách engine/UI**: cho phép test engine không cần UI — chính
|
||
là lý do 50 test tự động chạy được trong CI/Linux dù app nhắm Windows.
|
||
|
||
**Điều KHÔNG nên làm:** tự viết engine PDF từ đầu, hay chuyển C++/Qt để "giống
|
||
Foxit hơn" — chi phí khổng lồ, không giải quyết gap nào ở mục 3.
|
||
|
||
## 5. Tiến độ so với đích + thứ tự việc đề xuất
|
||
|
||
Đã xong (lõi, có test): Viewer ✅ · Annotate ✅ · Organize + lưu vững ✅ ·
|
||
**Edit text/object giữ font ✅ (iteration 2) · Reflow đoạn "như Word" ✅
|
||
(iteration 3)**.
|
||
|
||
Thứ tự đề xuất tiếp theo, bám giá trị người dùng:
|
||
1. **Phase 5 Bảo mật + "Lưu tối ưu"** (mục 3.3 gộp vào đây): mã hoá UI (nợ
|
||
Phase 3), redaction thật, subset font, dọn rác, chữ ký số.
|
||
2. Phase 6 Form → Phase 7 OCR/Convert → Phase 8 phát hành (theo roadmap).
|
||
3. Song song, rải theo từng phase: mở rộng font matching (3.4), nấc 1 của
|
||
hiệu năng phiên sửa (3.2) khi chạm file lớn thực tế, và v2 của reflow
|
||
(justify/xoay — mục 3.1).
|
||
|
||
## 6. Checklist FINAL TARGET & RULE cho chính tài liệu này
|
||
- [x] Đối chiếu từng tính năng với hành vi Foxit làm chuẩn nghiệm thu.
|
||
- [x] Không đề xuất cắt giảm tính năng nào; mọi gap đều kèm giải pháp cụ thể.
|
||
- [x] Giải pháp theo đúng thứ tự: dùng API thư viện → bù tầng PDF object → tự viết.
|
||
- [x] Mỗi gap có tiêu chí nghiệm thu so với Foxit (mục 3).
|