File Canva gói toàn bộ nội dung trang vào 1 Form XObject nên chế độ sửa
không thấy gì để sửa. Engine giờ liệt kê PHẲNG đệ quy vào form
(FPDFFormObj_CountObjects/GetObject qua pdfium-render), mỗi mục có path
[i,j,...] + tích ma trận form để quy toạ độ/cỡ chữ về trang; index của
ObjectInfo/EditOp = vị trí trong danh sách phẳng (trang không form thì
trùng index trang — tương thích ngược, UI không đổi cách gửi op).
- SetText in-place/substitute + ReflowText + Delete hoạt động với run trong
form; xoá trong form = set_text chuỗi rỗng (FPDFFormObj_RemoveObject chưa
mở ở wrapper cho pdfium_7543), object mới vẽ ở cấp trang với matrix đã
compose.
- Đã xác minh trước: PDFium regenerate stream form khi con bị sửa
(ProcessForm / HasDirtyStreams) — canary test round-trip kèm theo.
- ObjectInfo thêm nested/expanded; UI bỏ khung form phủ trang + chặn kéo
run trong form (Transform chưa hỗ trợ).
- 4 test integration mới với fixture Form XObject tự dựng bằng lopdf
(dev-dependency mới); CI thêm bước cargo test -p ff-engine trước build
(máy dev không có Rust — CI là nơi duy nhất chạy test), cache thêm target/.
Chi tiết: docs/12-phase4-summary.md (Iteration 4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Debug trên app thật (WebView2 remote debugging + CDP) với file thật tìm ra
nguyên nhân gốc chuỗi lỗi font: DTO Tauri serialize camelCase (fontBold,
fontFamily, fontSize) nhưng code edit-mode đọc snake_case (font_bold...) →
mọi thuộc tính font trong UI luôn undefined (không đậm, sai family, cỡ mặc
định 12). Đổi hết sang camelCase nhất quán với app.
- Engine: fontmatch::style_from_font_bytes parse sfnt thủ công — OS/2
usWeightClass/fsSelection, fallback head.macStyle (LibreOffice subset vứt
OS/2 nhưng head luôn còn; kiểm bằng qpdf trích FontFile2 file thật). 2
unit test sfnt tự dựng.
- Kéo-thả .pdf từ Explorer vào cửa sổ → mở file (tauri://drag-drop).
- Ẩn khung dòng overlay khi ô sửa mở (nền trong suốt lộ khung gây nhiễu).
- Ignore /*.pdf ở root (file test thật của user không được commit).
Kiểm chứng E2E không cần rebuild: inject hàm mới vào app build-12 đang chạy
qua CDP, mở ô sửa trên file thật + chụp màn hình — tiêu đề 2 dòng bold đúng
cỡ/vị trí nền trong suốt; khối bullet 8 dòng sạch PUA, bullet gốc nguyên
trên trang. Test Node trên dữ liệu run thật từ engine: 6/6 PASS.
Chi tiết: docs/12-phase4-summary.md (Vá vòng 5).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 lỗi user báo khi test build 11 trên file thật:
- Gom đoạn quá tham (tiêu đề + code + thân bài + nhãn đỏ 1 khối): đối chiếu
Foxit (mỗi đoạn = 1 text block riêng) — paragraphLines chỉ gom dòng kề
CÙNG format (family + đậm/nghiêng + màu, cỡ chênh <=1.25x).
- Bullet hiện ô vuông trong ô sửa: bullet là run font Symbol mã PUA, DOM
không render được — isMarkerRun/stripLineMarkers loại marker khỏi ô
sửa/commit, bullet giữ nguyên trên trang (chuẩn Foxit); đúp trúng bullet
vẫn mở khối chữ.
- Ô sửa nền trắng: render ngầm bản trang đã ẩn run đang sửa rồi chuyển nền
trong suốt — thấy nền gốc (dải màu/bullet) dưới chữ đang gõ; huỷ trả ảnh
gốc; commit vẫn tính trên editBase.
- Mất in đậm: engine thêm fallback đọc OS/2 usWeightClass + cờ italic từ
bytes font nhúng (file Word xuất không khai /FontWeight, tên không có
Bold); list_objects cache kiểu chữ theo tên font.
Test Node trích hàm thật từ main.js: 11 case tách khối/bullet đều PASS.
Chi tiết: docs/12-phase4-summary.md (Vá vòng 4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ba lỗi làm chữ trong ô sửa khác hẳn chữ gốc:
- Hồi quy: line-height từng dòng tính bằng khoảng hở giữa 2 bbox (~0/âm,
kẹp 8px) thay vì khoảng cách baseline → các dòng dồn cục lên đầu ô.
Fix: advance = hiệu mép dưới 2 bbox liền kề.
- Sai font: family PostScript CamelCase (TimesNewRoman, SegoeUI) không
khớp tên font Windows → CSS rơi về serif/sans mặc định. Fix: cssFontStack
tách CamelCase + bảng alias (Helvetica→Arial...).
- Sai cỡ với file engine báo cỡ lệch: thêm fitEditLinesToPdf — vòng ĐO-HIỆU
CHỈNH sau khi mount: đo chiều cao mực thật của đúng chuỗi đó (canvas
actualBoundingBox ở cỡ tham chiếu 100px, né hinting) so với bbox dòng PDF
rồi tự sửa cỡ (dead-band 12% giữ ổn định file chuẩn); đo vị trí render
thật (Range + font/ink ascent) rồi bù margin/indent từng dòng cho trùng
mép mực — khối căn giữa cũng bù ngang qua text-indent.
Kèm: giữ thụt lề từng dòng; đúp từ viewer thường đặt con trỏ đúng điểm đúp
(chờ img.decode trước khi đo). Kiểm chứng bằng harness canvas + Edge
headless: 3 kịch bản (cỡ báo sai 1/2, cỡ đúng, căn giữa) đều trùng khít
≤1.3px. Chi tiết: docs/12-phase4-summary.md (Vá vòng 3).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Chẩn đoán □: PDFium FPDFText_LoadFont nạp LẠI font subset rồi SetText map
U+0020 ra .notdef (hộp vuông) dù cmap font CÓ glyph space (đã probe cả 2
subtable) — lỗi PDFium với font nạp động, không phải do font.
Engine (edit.rs):
- Tầng 0 mới của thang font reflow: DÙNG LẠI chính font object gốc trong
PDF (PdfFontToken của run neo, không nạp lại) — glyph + charcode map gốc
xử lý dấu cách chuẩn, file không phình; chỉ chọn sau khi PROBE ghi/đọc-lại
toàn bộ text mới thành công (tạo object nháp → so text → gỡ + mem::forget)
- Ghi từng dòng xong ĐỌC LẠI NGAY tự kiểm; PDFium vẫn vứt ký tự → gỡ và ghi
lại TỪNG TỪ tự đặt vị trí bằng advance hmtx (không bao giờ ghi glyph dấu
cách) — lưới an toàn cuối cho font lạ
- Dòng cứng (\n từ ô sửa) NỞ tới +35% bề rộng khối (chặn mép trang) trước
khi bẻ lại — hộp text tự nở như Foxit; khối căn giữa nở đều quanh tâm
- line_styles theo baseline gốc: dòng cứng thứ i giữ ĐÚNG CỠ dòng gốc thứ i
(tiêu đề 22.5/20pt không bị ép cả khối về 1 cỡ)
UI (main.js + styles.css):
- Ô sửa cả đoạn đổi textarea → contenteditable plaintext-only: MỖI DÒNG GỐC
= 1 div đúng font/cỡ/màu/đậm-nghiêng CỦA DÒNG ĐÓ, khối căn giữa hiện căn
giữa (nở đều 2 phía quanh tâm), khung bù lệch dọc line-box → chữ trong ô
TRÙNG vị trí chữ gốc khi mở; con trỏ đặt đúng điểm đúp (caretRangeFromPoint);
Enter xuống dòng, Ctrl+Enter/blur áp dụng, Esc huỷ, dán luôn chữ thuần
Kiểm chứng file thật: "HỢP"→"HỢPP" giữ đúng 2 dòng căn giữa cỡ 22.5/20,
font BAAAAA+TimesNewRomanPS-BoldMT gốc, dấu cách nguyên vẹn, render khớp
bản gốc. Test: +2 hồi quy (giữ cỡ từng dòng; dòng nở không bẻ) → edit 21/21,
các suite annot/organize/redact/form xanh. Docs 12 (mục vá vòng 2), 13
(E39–E42).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgnbPwSoRGNH7LHsDUmRpk
Chẩn đoán trên file thật (Word xuất): tiêu đề 2 dòng bị cắt thành 41 run
per-word/per-char xen run rỗng kích thước 0, cỡ 22.5pt + 20pt trộn nhau,
bbox chữ có dấu (Ạ, Ợ) tụt thấp hơn hẳn chữ thường.
UI (main.js):
- clusterTextLines: gom run thành DÒNG theo GIAO DỌC ≥50% chiều cao nhỏ
(miễn nhiễm dấu tiếng Việt) + tách cột theo hở ngang; gắn run rỗng vào dòng
chứa nó. Overlay vẽ 1 khung/DÒNG (file thật: 231 ô vụn → 18 khung);
chọn/kéo/xoá/đổi thuộc tính tác động CẢ DÒNG (batch ops, editSelRuns)
- paragraphLines mới trên nền cluster: dòng kề nhau (hở ≤1.3× chiều cao),
giao ngang ≥30%, cỡ chữ ≤1.8× (tiêu đề 22.5/20pt vẫn 1 đoạn), khoá nhịp ±35%
- Đúp LUÔN mở ô sửa CẢ ĐOẠN, mỗi dòng gốc = 1 dòng textarea (giữ và sửa được
cả xuống dòng); đúp trúng KHE giữa các từ vẫn mở (hit-test theo điểm trên
overlay); click/chọn chữ trong ô sửa di chuyển con trỏ, KHÔNG thoát edit
(stopPropagation + guard .edit-inline ở onEditStageClick); ô che kín chữ cũ,
line-height đúng nhịp dòng, scrollIntoView
Engine (edit.rs ReflowText):
- TỰ NỞ danh sách run: mọi text object có tâm trong union bbox (+2pt) của các
run được đưa đều bị thay — không bao giờ sót run rỗng/lệch bbox → hết cảnh
chữ mới chồng chữ cũ
- Phát hiện khối CĂN GIỮA (tâm mọi dòng ≈ tâm khối, có dòng thụt trái) →
dòng mới đặt x = tâm khối − w/2 (đo hmtx) — tiêu đề căn giữa giữ căn giữa
Kiểm chứng với file thật (cục bộ): 39 run tiêu đề xoá sạch 0 mảnh sót, 2 dòng
mới căn giữa lệch tâm trang <5pt, extract sạch chữ cũ; grouping đúng khi đúp
mọi từ của cả 2 dòng lẫn header 8pt per-char.
Test: 2 test hồi quy mới (nở indices bắt run giữa bị sót; giữ căn giữa đo
bằng fixture tự căn) → edit 19/19, toàn bộ engine 75/75 ngoài qpdf-fixture.
Docs: 12 (mục vá theo phản hồi), 13 (E34–E38).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgnbPwSoRGNH7LHsDUmRpk
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
Engine:
- fontmatch.rs mới: làm sạch tên PostScript/subset, kiểm coverage cmap bằng
ttf-parser, match font hệ thống CÙNG HỌ (bảng Windows/macOS, Liberation
metric-compatible + fc-match trên Linux, alias base-14 Helvetica→Arial...)
- edit.rs: SetText quyết định 3 tầng — (1) sửa TẠI CHỖ giữ nguyên font gốc
(charset-subset / base-14+ASCII / cmap phủ đủ, gồm font nhúng + tiếng Việt);
(2) thiếu glyph → thay font cùng họ đúng đậm/nghiêng; (3) mới đến fallback.
Đổi cỡ chữ = scale matrix neo baseline, không đụng font; fix bug phóng đại
kép khi text có matrix scale. ObjectInfo thêm font_family/bold/italic/embedded.
SetText/AddText nhận font_family + bold/italic dạng Option (None = giữ nguyên)
- annot.rs: fallback Linux có đủ biến thể đậm/nghiêng (DejaVu/Liberation)
UI (chuẩn trải nghiệm Foxit):
- Double-click sửa CẢ DÒNG (gộp run cùng baseline), commit 1 batch = 1 undo
- Ô sửa WYSIWYG: đúng font/cỡ/màu/đậm-nghiêng của run gốc khi gõ
- Toolbar Format: dropdown font "(giữ nguyên: X)", nút B/I theo kiểu thật,
đổi cỡ/màu/kiểu gửi đúng field đó (null = giữ nguyên → không đổi font)
- Kéo di chuyển/resize có phản hồi trực tiếp (live), resize neo góc trên-trái
- Dọn file tạm ff_edit_* khi thoát/lưu (command edit_cleanup)
Test: 14 test edit (7 mới: giữ font ASCII/base-14, giữ font NHÚNG tiếng Việt,
match cùng họ, cỡ chữ neo + hồi quy matrix, bold override, gộp dòng) + 3 unit
fontmatch. Toàn bộ suite engine ngoài qpdf: 50/50 xanh (chạy trên Linux).
Docs: 12 (iteration 2), 13 (E17–E25), 14 MỚI — phân tích định hướng/công nghệ/
tiến độ so với tham vọng thay Foxit, gap còn lại + giải pháp; README cập nhật.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgnbPwSoRGNH7LHsDUmRpk