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>
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>
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>
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>
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.