1) fontfix v2 (PDF tu Word/InDesign): tong hop bang name/post khi subset
thieu (OTS bat buoc); font CID/Identity-H KHONG co cmap -> dung cmap
(3,1) fmt4 tu /ToUnicode x /CIDToGIDMap (Identity: GID=CID; stream 2B/CID
parse tu descendant font); chan GID >= numGlyphs (maxp). tounicode.rs tra
FontMapping {code_to_uni, is_cid, cid_to_gid}.
2) Format TUNG CHU trong o sua (chuan Word/Foxit): boi den -> B/I (nut
toolbar khong cuop focus + Ctrl+B/I). UI tach span theo vung chon, dat
data-bold/italic-override; commit gui override tung doan (RichSegDto
bold/italic). Engine: bang style rich key theo (run nguon, override) -
doi bien the thi lay font BIEN THE THAT cung ho (fontmatch -> builtin ->
substitute), khong dung lai font goc. Test:
rich_seg_bold_italic_override_uses_variant_font.
3) Edit file OCR (scan + text an): khoi toan run render-mode INVISIBLE ->
ve rect TRANG che vung chu cu tren anh + ve text moi HIEN mau den (ca 4
duong ve: rich chinh/fallback tu, 1-style chinh/fallback). Test:
ocr_invisible_block_becomes_visible_with_cover.
4) Bu metrics kieu Adobe Sans MM trong o sua: span mang data-pdfw (be rong
that tren trang); fitEditLinesToPdf them pass letter-spacing de moi nhom
style chiem DUNG quang goc khi font thay the lech metrics (>2%); kep
+/-18% co chu.
5) Khoi dong het "do": cua so tao AN (visible:false), UI show() ngay khi
frontend san sang + man splash che phan nap dau; luoi an toan Rust hien
cua so sau 4s neu JS chet som.
6) Open with FoFreeXit.exe: co che argv (initial_file) van hoat dong - loi
nam o registry Applications tro exe CU (ban portable bi di chuyen/ghi
de) lam Windows hien lai hop chon app. Fix tu chua: moi lan khoi dong
dang ky lai HKCU (Applications command + ProgID FoFreeXit.pdf +
OpenWithProgids .pdf) tro exe hien tai, chay nen qua reg.exe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User yeu cau: khach hang khong co font chuan san — phan mem phai tu chuan bi
font, khong duoc thay bang font "gan giong". Truoc do font nhung cua PDF da
dung cho o sua (build 35), nhung con 3 khe ho roi ve font he thong: font
khong nhung trong PDF, font CFF khong va duoc, va KY TU MOI GO VAO khong co
trong subset (subset chi chua glyph da dung).
- scripts/fetch-fonts.ps1: tai 10 file Noto hinted static (Sans/Serif/Mono,
du Regular/Bold/Italic/BoldItalic, phu tieng Viet, ~6.7MB) + OFL license
vao fonts/ (gitignore; CI cache theo hash script).
- CI: fetch fonts + dong fonts/ vao zip portable (canh exe).
- Tauri: main() dat FOFREEXIT_FONTS_PATH (fonts/ canh exe, dev thi goc
workspace); command app_font_data(kind) tra base64 (whitelist 10 kind).
- UI: FontFace "ff-app-{sans|serif|mono}" dang ky theo weight/style, dung
GIUA stack: font nhung cua PDF -> Noto dong goi -> stack he thong (luoi
cuoi). Phan loai serif/sans/mono tu cssFontStack. Nap truoc cung font
nhung khi vao trang sua.
- Engine find_font_bytes (duong ghi file/reflow can font coverage): uu tien
tuyet doi fonts/ dong goi qua env — ket qua LUU ra giong nhau tren moi may
(truoc: lay Arial/Segoe cua Windows, may khach thieu font la khac ket qua).
E2E ke tiep (build 36): go ky tu ngoai subset trong o sua -> 100% glyph phai
tu font custom (nhung/dong goi), 0 glyph he thong.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Build 34 tra bytes font tho nhung OTS cua WebView tu choi het (bat qua CDP
Log): (1) "OS/2: missing required table", (2) "cmap: no supported subtables"
- subset LibreOffice chi co cmap (1,0) format 0, ma tuy bien: ASCII giu
nguyen ma, chu Viet co dau gan ma thap 1-31 (chi /ToUnicode giai nghia duoc).
- fontfix.rs: dung OS/2 v4 (96B) tu head/hhea + co dam/nghieng cua run
(fsSelection/usWeightClass - khong bi synthesize bold chong); convert cmap
(1,0) fmt 0/6 -> (3,1) format 4 (segment gop cap lien tiep, moi glyph 1
segment khi roi rac); ghep lai sfnt: danh muc sort theo tag, pad 4B,
checksum bang + head.checkSumAdjustment (tong = B1B0AFBA); tag 'true' ->
0x00010000. Khong phai sfnt (CFF tran) tra nguyen trang.
- tounicode.rs: doc /ToUnicode qua lopdf (bfchar/bfrange ca 2 dang, UTF-16BE
ke ca surrogate pair; ligature nhieu ky tu bo qua), match BaseFont theo
suffix sau prefix subset "ABCDEF+", tim ca trong resources cua Form
XObject (sau 3) cho file Canva. Ma khong co ToUnicode: ASCII identity +
MacRoman cho ma cao; ma thap khong giai nghia duoc thi bo (ky tu roi xuong
font ke trong stack CSS).
Kiem chung truoc CI: nhan ban logic sang Node, va bytes THAT cua
NotoSansMono-Bold subset (dump tu app) -> FontFace load OK trong WebView2
that (truoc: SyntaxError). Unit test: OS/2 + checksum + format 4 lookup
('A' giu glyph, 'a?' theo ToUnicode) + bfchar/bfrange parser.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User bao: chu mono von in dam (NotoSansMono-Bold), mo o sua thi "ve chu
thuong". Chan doan bang CSS.getPlatformFontsForNode: span da co
font-weight:700 dung, nhung may khong cai font cua tai lieu nen WebView thay
the — mono roi ve Consolas-Bold (net manh hon han NotoSansMono-Bold, nhin
nhu khong dam), chu thuong roi ve Arial.
Fix WYSIWYG font that:
- Engine: ff_engine::font_data(input, page, index) — bytes font nhung (sau
giai nen) cua 1 text run, index PHANG nhu list_objects (vao duoc run trong
Form XObject).
- Tauri: command edit_font_data tra base64.
- UI: nap luoi theo TEN font khi loadEditPage (dedup, 1 lan/font), dang ky
FontFace voi weight/style theo run (khong synthesize chong), dat family
nhung LEN DAU stack cua div/span trong o sua; glyph thieu trong subset tu
roi xuong stack he thong; doi phien sua thi bo face cu (subset khac
coverage). Loi (font khong nhung/khong parse) -> giu stack cu.
Smoke test tren app that (CDP): FontFace tu ArrayBuffer + gan vao span mono
=> getPlatformFontsForNode tra isCustomFont:true. E2E day du sau build CI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bug thu 2 cua "luu xong de len text khac" (bug 1 - de NGANG trong dong - da
fix bang mep muc that): vong no doc 8pt (sinh ra de bat dau tieng Viet ve
bang op roi) nuot ca run cao co chu cua DONG KE TREN khi gian dong sat
(~13-14pt): tam muc run dong tren lot vao bbox+8pt -> ca cau bi xoa,
first_baseline troi len 1 dong -> khoi ve lai de len vi tri dong tren, sot
manh mono dau dong (tam cao hon nguong chut).
Tai hien tren file that cua user (trang 2, khoi "Cac thuoc tinh HttpOnly..."
nuot dong "Path=/ khien refresh token..." ngay tren). Ban dau ket luan nham
la "file nhiem artifact" - DINH CHINH trong docs/12: file sach, Path=/ la
dau bullet hop le.
Fix: vung no 8pt chi duoc nuot op NHO (cao < 0.6 x median cao run trong
khoi - manh dau); op cao co chu phai co tam trong bbox LOI. Test hoi quy
reflow_tight_leading_does_not_swallow_line_above (gian dong 13pt, khoi 2
dong duoi, dong tren phai song sot + dong moi khong cham muc dong tren).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Slot trang active cao theo anh that (CSS :has -> height auto), nhung luc vua
vao che do sua anh CHUA load nen slot cao ~0 -> scrollIntoView ban dau lech
ca cot va listener cuon (debounce 200ms) tu chuyen nham trang active (vao
trang 2 nhay sang trang 3). sizeEditSlot dat them min-height = chieu cao
uoc tinh nen slot khong bao gio sap ve 0.
Kem ket qua kiem chung build 31 tren app that (docs/12): dong lan style sau
commit tang dan tuyet doi (worst overlap 0pt) - fix mep muc dat; mảnh
"Path=/" con de la artifact NHIEM SAN trong file test cua user tu lan luu
bang build loi cu (reflow bao ton nhu marker) - khong phai bug moi.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Engine (edit.rs): sau khi ve tung manh rich, tien con tro theo MAX(do hmtx,
MEP MUC THAT tu PDFium bounds) - subset font ttf-parser khong parse duoc roi
ve fallback 0.5em/ky tu uoc luong thieu ("HttpOnly" do 36pt vs muc that
42.6pt) lam manh sau de len manh truoc ("auth-storagetu"). Ca duong ve ca
manh lan duong be theo tung tu.
- UI nhieu trang: #editPagesCol chua 1 .edit-slot/trang; trang active giu
#editPageWrap (img+overlay cu, chi DI CHUYEN giua cac slot), trang khac
render read-only luoi (IntersectionObserver, stamp editBase|zoom tu
invalidate sau op/undo/zoom). Dup vao trang read-only = chuyen active + mo
o sua ngay tai diem dup (tai dung editPendingPoint). Cuon: so trang bam
theo trang giua khung nhin, tu doi active khi khong co o sua mo. Zoom co
gian moi slot ngay, render net sau.
- Nut "Huy thay doi" canh nut Luu: dong o sua, xoa file tam, editBase ve file
goc, xoa undo/redo, moi trang tu render lai noi dung goc.
- E2E CDP tren app that (tiem patch vao build 29): cot 3 trang, dup trang 3
read-only mo dung dong, Huy tra ve goc (text thu "KIEM" bien mat), zoom
130% ca 3 slot. Docs/12 vong 6.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 vấn đề user báo trong chế độ Sửa nội dung:
1. ZOOM: stage sửa trước đây cứng 820px — nút +/-, dropdown %, Vừa rộng/
Vừa trang và Ctrl+lăn chuột đều vô tác dụng. Giờ editZoom riêng (kế thừa
zoom viewer khi vào): co giãn ảnh + overlay NGAY (CSS, mượt), render nét
PDFium sau 220ms ngừng thao tác; mọi nút zoom/fit + Ctrl+wheel hoạt động;
fix flex-center overflow (margin auto) để zoom to không cắt mép trái.
2. CHUYỂN TRANG: nút ▲▼/ô nhập trang trước đây chỉ điều khiển viewer đang
ẩn. Giờ goToPage/gotoPrev/Next nhận biết edit mode → switchEditPage
(đóng ô sửa đang mở, giữ nguyên undo/thay đổi các trang khác vì dùng
chung editBase).
3. DÒNG LẪN STYLE (thường + đậm/mono): ô sửa trước áp 1 style/dòng nên
"windowsInitialPassword" đậm hiện thành chữ thường, commit còn ép cả
dòng về 1 font. Giờ:
- UI: mỗi nhóm run liền kề cùng style = 1 SPAN riêng (đúng font/cỡ/màu
từng phần khi gõ); commit đọc theo đoạn style (text node trần mượn
style span kề); >=2 style gửi kèm richLines.
- Engine: ReflowText thêm rich (Vec<Vec<RichSeg>>) — resolve thang
giữ-font RIÊNG cho từng style-run (probe token gốc/nhúng lại bytes/
builtin/cùng họ), token hoá theo từ giữ style + glue giữa đoạn dính
liền, bẻ dòng chung cả khối, vẽ mỗi cụm cùng-style thành 1 object
đúng font/cỡ/màu, tự kiểm + fallback từng từ như đường 1-style.
- fitEditLinesToPdf: đo text node sâu trong span; hiệu chỉnh cỡ scale
cả span. Tầng-0 token gốc tắt khi KHỐI có run nested (trước chỉ xét
anchor — token vô hiệu sau khi mở lại document hậu phẫu thuật).
- Test: fixture 1 dòng Helvetica + Courier-Bold → sửa phần thường,
phần mono giữ nguyên BaseFont Courier-Bold, cùng baseline đúng thứ tự.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Iteration 4b — giải pháp triệt để cho file Canva (thay cổng chặn của 4a):
module formsurgery (lopdf) xoá đúng op vẽ chữ (Tj/TJ/'/") của run bị thay
trong stream form — mọi byte khác giữ NGUYÊN (không đụng generator lossy
của PDFium); chữ mới vẽ ở CẤP TRANG như thường.
- Map PDFium ↔ stream: text-child thứ k của form ↔ show-op thứ k; form con
thứ n ↔ Do-trỏ-form thứ n. Mỗi mức có BẤT BIẾN kiểm đếm (số op == số
object PDFium thấy) — lệch là từ chối, không đoán mò. '/" thay bằng
T*/Tw+Tc+T* để giữ hiệu ứng vị trí cho chữ sau.
- apply_edits: run nested trong ReflowText/Delete → phẫu thuật ở pha A2 rồi
MỞ LẠI document (index cấp trang không đổi); tầng-0 reflow (token gốc)
tắt cho anchor nested (token vô hiệu sau mở lại) → đi tầng nhúng bytes/
cùng họ. SetText/Transform nested vẫn từ chối rõ (UI chặn với hint).
- CỔNG AN TOÀN mới: page_render_mismatch_masked — mọi thay đổi pixel phải
nằm TRONG vùng khối sửa (band reflow + rect run xoá); lệch ngoài vùng
>0.5% → huỷ output, giữ nguyên file gốc.
- UI: bỏ auto-flatten (không cần nữa), sửa nested trong suốt qua đúp chuột;
hint 1 dòng cố định + ellipsis + title (hết cảnh trang nhảy lên xuống do
thanh công cụ co giãn); chặn-với-thông-điệp cho đổi thuộc tính/kéo khối
trong form.
- Test: fixture form thêm dòng 2 + path đỏ; 2 test phẫu thuật (delete/reflow
giữ nguyên phần khác, form còn nguyên); SetText nested vẫn bị từ chối.
Flatten + cổng cũ giữ nguyên engine-side (có test riêng).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Kết luận sau 3 vòng CI + đọc nguyên văn source PDFium: PDFium hiện KHÔNG ghi
lại được stream của Form XObject — CPDF_PageContentManager chỉ biết key
/Contents (trang), form là stream tự thân nên nội dung regenerate rơi vào
key /Contents vô nghĩa trên dict form, stream thật giữ nguyên. Mọi cách sửa
"trong form" đều không thể lưu.
- Engine mới: flatten_form_xobjects — rút từng con khỏi form
(FPDFFormObj_RemoveObject), nhân ma trận form vào con, chèn lại vào TRANG,
xoá form rỗng; lặp <=4 vòng cho form lồng form. Hiển thị y hệt, mọi object
thành cấp trang, sửa bằng đường page-level đã test kỹ.
- Tauri: command edit_flatten_to_temp; UI loadEditPage thấy object nested →
tự mở gói một lần rồi nạp lại (bản mở gói không tính là "có thay đổi").
- apply_edits: gỡ toàn bộ đường sửa-trong-form (NestedRecreate, rút con,
đánh thức trang — đã chứng minh không thể lưu); op nhắm vào object trong
form khi chưa flatten → lỗi rõ "trang chưa được mở gói" (có test).
- Test viết lại theo flow flatten: 6 test (liệt kê nested, flatten giữ vị
trí/nội dung, settext/reflow/delete sau flatten, từ chối op nested).
Docs 12-phase4-summary.md cập nhật kết luận + giới hạn (mất clip/ExtGState
mức form khi flatten; cấu trúc file đổi khi lưu).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI vòng 1 bắt đúng lỗi thiết kế: FPDFText_SetText trên con trong form KHÔNG
đánh dấu dirty_streams của form (chỉ RemovePageObject/InsertPageObjectAtIndex
mới làm — xem cpdf_pageobjectholder.cpp) nên mọi sửa in-place trong form
không bao giờ được ghi ra file. Test canary fail chính xác ở điểm này.
- Đổi feature pdfium-render pdfium_latest(7543) -> pdfium_7350 (cả ff-engine
lẫn app): mở remove_object cho Form XObject qua blanket impl
PdfPageObjectsCommon (wrapper 0.8.37 sót gate 7543 dù binding có đủ);
API 7350 là subset của DLL bblanchon mới, đã quét 0 hàm 7543-only.
- Mọi sửa/xoá con trong form = RÚT THẬT khỏi form (FPDFFormObj_RemoveObject,
thứ tự path sâu trước + cùng cha con lớn trước) -> form dirty -> ProcessForm
regenerate stream khi lưu. SetText con trong form = SetTextMode mới
NestedRecreate: rút khỏi form + tạo lại ở cấp trang giữ font gốc qua token
(probe như tầng 0 reflow; thiếu glyph -> nhúng lại bytes/cùng họ), matrix
đã compose.
- CI test thêm --test-threads=1: PDFium serialize qua 1 mutex toàn cục,
1 test panic khi giữ lock poison mutex làm 17 test song song chết oan.
Docs cập nhật theo (12-phase4-summary.md Iteration 4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
Trước đây phải bấm nút '✏️ Sửa nội dung' rồi mới double-click được. Giờ:
- Đang xem bình thường, đúp chuột vào chữ → hit-test qua edit_list_objects
tại điểm đúp (toạ độ PDF qua cssToPdf); có text object → tự vào chế độ Sửa
nội dung đúng trang + mở NGAY ô sửa tại đoạn đó (startTextEdit: đoạn nhiều
dòng ra textarea reflow, 1 dòng ra ô sửa dòng)
- Đúp vùng trống: không làm gì (như Foxit). Đang cầm công cụ chú thích /
organize / redact: bỏ qua. Element chú thích (freetext/note/markup) giữ
handler đúp riêng của chúng
- pickTextAt: ưu tiên box chứa điểm (nới 3pt, chọn box nhỏ nhất khi chồng),
fallback box gần nhất trong 12pt; enterEditMode nhận pageIndex tuỳ chọn;
editPendingPoint tiêu thụ sau loadEditPage; bỏ selection 1 từ do browser
tự bôi khi đúp trên text layer
Docs: 13 (E31–E33), README.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgnbPwSoRGNH7LHsDUmRpk
.github/workflows/build-windows.yml chạy trên windows-latest:
- Fetch pdfium.dll + qpdf.exe (script sẵn có scripts/fetch-*.ps1)
- cargo build --release cho app Tauri (không cần WiX/NSIS vì đóng gói
portable thủ công, không dùng bundler cài đặt)
- Gộp FoFreeXit.exe + pdfium.dll + qpdf.exe (+ dll phụ thuộc) + hướng dẫn
vào 1 thư mục, nén FoFreeXit-portable-win64.zip
- Đăng làm workflow artifact (mọi lần chạy) + GitHub Release (khi trigger
thủ công workflow_dispatch) — người dùng tải trực tiếp từ GitHub, không
cần cài Rust/Visual Studio/Node để build hay chạy thử
scripts/portable-readme.txt: hướng dẫn chạy bằng tiếng Việt kèm trong zip,
ghi rõ OCR/LibreOffice là tuỳ chọn cài thêm (app vẫn chạy đủ tính năng
Viewer/Annotate/Organize/Edit/Bảo mật/Form không cần chúng).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgnbPwSoRGNH7LHsDUmRpk
Engine:
- ocr.rs MỚI: find_tesseract (sidecar như qpdf, env FOFREEXIT_TESSERACT_PATH);
ocr_page_words (render 300 DPI → tesseract tsv → lọc confidence ≥30 → quy
đổi pixel→điểm PDF); ocr_add_text_layer — thêm text object render-mode
INVISIBLE đúng khung từng từ lên CHÍNH trang gốc (searchable PDF chuẩn,
ảnh không đổi), font Unicode nhúng 1 lần, mặc định vie+eng
- convert.rs MỚI: export_images (PNG mỗi trang theo DPI), export_text,
export_docx tự viết (gom char boxes thành dòng thị giác → đoạn Word với cỡ
chữ xấp xỉ + ngắt trang; DOCX = zip STORE tự ghi kèm CRC-32, không thêm
dependency); find_soffice + office_to_pdf + pdf_to_docx_via_soffice
(LibreOffice headless, profile riêng -env:UserInstallation)
Tauri + UI (thanh 🔁 Chuyển đổi):
- Commands: ocr_run, convert_images, convert_txt, convert_docx (tự chọn
LibreOffice nếu có → fallback bộ cơ bản, báo engine đã dùng),
office_convert, convert_tools_status (UI disable nút + báo thiếu công cụ),
pick_office_file, pick_save_as
- Nút: OCR (select Việt+Anh/Việt/Anh), Xuất PNG 150 DPI, Xuất TXT, Xuất Word,
Office→PDF (mở luôn kết quả)
Test (6 mới → engine 73/73 xanh ngoài 2 fixture qpdf Linux):
- OCR tiếng Anh trên PDF-scan giả lập THẬT (xoá text chỉ còn ảnh render):
extract + search thấy từ, toạ độ lớp ẩn khớp vùng chữ
- OCR tiếng Việt đúng dấu ("Việt Nam đất nước")
- export PNG 3 trang / TXT / DOCX (unzip hệ thống giải được, có ngắt trang)
- LibreOffice round-trip 2 chiều (tự bỏ qua nếu máy không có soffice)
Docs: 19-phase7-summary (+ giới hạn v-sau), 20-phase7-user-tests (C1–C10 kèm
hướng dẫn cài Tesseract/LibreOffice trên Windows), README, roadmap (Phase 7
HOÀN TẤT lõi), 14 (kế tiếp: Phase 8 phát hành).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgnbPwSoRGNH7LHsDUmRpk
Engine — chữ ký số (sign.rs MỚI):
- generate_self_signed_id: Digital ID tự ký RSA-2048 + X.509 (rcgen), ghi PEM
- sign_pdf: đúng cơ chế Adobe — lopdf dựng sig dict (/SubFilter
adbe.pkcs7.detached) + Widget field + AcroForm/SigFlags, ByteRange/Contents
placeholder; serialize → vá ByteRange theo offset thật → SHA-256 trên 2 đoạn
→ CMS SignedData detached (crate cms, signed attrs messageDigest+contentType,
RSA-SHA256) nhét DER vào Contents
- verify_signatures: parse ByteRange/Contents, băm lại, kiểm chữ ký RSA trên
signed attrs + messageDigest khớp + phủ toàn bộ file → phát hiện GIẢ MẠO
- Bài học: cắt DER về đúng độ dài (bỏ padding hex '0'); bỏ qua /Contents của
trang (tham chiếu), chỉ nhận /Contents <hex> của chữ ký
Engine — redact tỉa theo KÝ TỰ (redact.rs):
- Text chạm một phần: page_char_boxes xác định ký tự trong vùng, GIỮ các đoạn
ngoài vùng, dựng lại đúng vị trí/baseline/màu (font nhúng gốc → cùng họ →
mặc định). Guard: text xoay/không đọc được font → xoá nguyên (an toàn)
- qpdf::optimize_save: --object-streams=generate + nén, dọn object rác
Tauri + UI:
- Commands: sig_create_id, sig_sign, sig_verify, security_optimize,
pick_any_file, pick_save_pem
- Thanh Bảo mật thêm: Tạo Digital ID, Ký số (dialog + tự kiểm tra sau khi ký),
Kiểm tra chữ ký (modal bảng trạng thái + lý do), Lưu tối ưu
Test (7 mới → 62/62 engine xanh ngoài 2 fixture qpdf Linux):
- sign_then_verify_is_valid; tampering_after_signing_is_detected (phát hiện
sửa sau khi ký); verify_reports_none_for_unsigned
- redact tỉa ký tự: cụm mật biến mất, phần cùng dòng ngoài vùng vẫn còn
- optimize_save không phình + giữ nội dung
Deps: rsa, sha2, cms, x509-cert, rcgen, pkcs8, der, spki, const-oid.
Docs: 15 mục 6 (iter 2 + giới hạn), 16 (S13–S19), README, roadmap (Phase 5
HOÀN TẤT), 14 (kế tiếp: Phase 6 Form).
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
PDF viewer (render/search/text-select/thumbnails/outline) and annotation
engine (Highlight/Underline/Strikeout/Square/FreeText/Note) over PDFium +
lopdf, with Unicode font embedding for Vietnamese text and undo/redo.