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>
30 KiB
12 — Tổng kết Phase 4 (Chỉnh sửa nội dung — Edit) · Iteration 1 + 2 + 3 + 4
Trạng thái sau Iteration 4: sửa được cả text NẰM TRONG Form XObject (file Canva/Illustrator/InDesign gói cả trang vào form).
Iteration 4 — Sửa text trong Form XObject (file Canva...)
User mở CV xuất từ Canva: xem được nhưng không sửa được gì — trang chỉ có
10 path + 1 Form XObject phủ toàn trang, TOÀN BỘ chữ nằm trong form (16 font
Type0/CID nhúng), trong khi list_objects chỉ duyệt object cấp trang.
Engine (edit.rs)
- Danh sách PHẲNG đệ quy (
collect_flat): duyệt object trang theo thứ tự vẽ, đi sâu vào Form XObject quaFPDFFormObj_CountObjects/GetObject(pdfium-render 0.8.37:PdfPageXObjectFormObject::len()/get()public; match thẳng variantPdfPageObject::XObjectFormđể giữ lifetime trang — accessoras_x_object_form_object()bị rút ngắn lifetime theo&self). Mỗi mục cópath([i] cấp trang, [i,j,…] con trong form) +acc= tích ma trận form tổ tiên. Chặn depth 3 / 4000 mục.indexcủa ObjectInfo/EditOp = vị trí trong danh sách phẳng này — trang không có form thì trùng index trang như cũ (tương thích ngược 100%, UI không đổi cách gửi op). - Toạ độ/cỡ quy về trang: rect con = AABB của 4 góc qua
acc; cỡ hiển thị nhânmat_vscale(acc)(độ dài ảnh vector đơn vị Y). - ObjectInfo thêm
nested(con trong form — sửa text OK, kéo/resize chưa) +expanded(form đã liệt kê con — UI bỏ khung form để khỏi che con). - Áp op cho con trong form: SetText in-place resolve theo path (đổi cỡ:
so cỡ theo KHÔNG GIAN TRANG — target × vscale); SetText-substitute và
ReflowText tạo object mới ở CẤP TRANG với matrix/toạ độ đã compose;
"xoá" con trong form =
set_text("")(glyph biến mất) vìFPDFFormObj_RemoveObjectchưa mở ở wrapper cho pdfium_7543 — làm TRƯỚC khi xoá object cấp trang để path còn đúng. Transform (kéo/resize) con trong form: bỏ qua, UI chặn kéo. - Điểm sống còn đã xác minh trước khi code: PDFium (bản mới, bblanchon
latest) REGENERATE content stream của form khi con bị sửa —
CPDF_PageContentGenerator::ProcessForm→HasDirtyStreams→ chạy generator đệ quy cho form. Không có cái này thì sửa xong lưu ra vẫn thấy chữ cũ. - Vá kèm: đọc source crate pdfium-render tải từ crates.io để chốt API vì máy dev không compile được Rust (xem mục CI bên dưới).
UI (main.js)
- Bỏ khung overlay cho form
expanded(khung phủ kín trang chặn hết click) và cho ảnh/formnested; run text trong form tự chảy qua clusterTextLines như run thường (rect đã quy về trang). - Chặn kéo di chuyển dòng chứa run
nested(engine chưa hỗ trợ Transform trong form) — vẫn chọn/sửa text/xoá bình thường.
Test (4 mới — edit_roundtrip.rs, fixture tự dựng bằng lopdf)
- Fixture: PDF 1 trang, chữ "Hello inside form" Helvetica 24pt nằm TRONG Form XObject (trang chỉ có 1 object form) — đúng cấu trúc Canva.
form_xobject_children_are_listed— thấy text nested, form expanded, rect quy về trang đúng chỗ (72, ~700), cỡ ~24.form_xobject_settext_inplace_roundtrip— canary quan trọng nhất: sửa in-place con trong form → lưu → text mới có mặt, text cũ biến mất (chứng minh PDFium regenerate stream form).form_xobject_reflow_roundtrip— reflow tiếng Việt trong form: run cũ làm rỗng, dòng mới ở cấp trang quanh khối cũ.form_xobject_delete_clears_text— xoá = text biến mất khỏi extract.
CI chạy test engine
Máy dev không có Rust nên CI là nơi duy nhất chạy test: workflow thêm
bước cargo test -p ff-engine TRƯỚC khi build release (fail test = fail
build, không publish bản hỏng); cache thêm target/ gốc workspace.
Giới hạn ghi nhận (iteration 4 v1)
- Chưa di chuyển/resize object trong form (Transform bị bỏ qua, UI chặn kéo).
- Chưa sửa/thay ẢNH trong form (khung overlay ẩn).
- "Xoá" text trong form để lại text object rỗng trong form (vô hại, không
render); khi wrapper mở
FPDFFormObj_RemoveObjectcho pdfium_7543 sẽ xoá thật. - Form lồng sâu >3 cấp hoặc >4000 object con: phần vượt không liệt kê.
Các iteration trước: 52/52 test engine xanh ngoài qpdf (17 test edit + 6 unit fontmatch + 29 test cũ) — nay thêm 4 test form + 2 unit sfnt style.
Iteration 3 — Reflow đoạn văn "như Word" (mới nhất)
Đóng gap trải nghiệm cuối cùng của tính năng moat (mục 3.1 của
docs/14-foxit-gap-analysis.md): double-click vào đoạn nhiều dòng → sửa cả
đoạn trong 1 ô, chữ tự bẻ dòng lại theo bề rộng khối khi commit.
Engine — EditOp::ReflowText { indices, text }
- Engine TỰ suy hình học từ các run: lề trái/bề rộng khối từ bounds, baseline
từ
matrix.f(gom cụm ±1pt), khoảng cách dòng = median hiệu baseline (1 dòng → 1.25× cỡ hiển thị) — UI chỉ cần gửi indices + text mới. - Bẻ dòng đo bằng hmtx thật (
fontmatch::wrap_lines+char_advancequa ttf-parser): greedy theo từ,\n= ngắt cứng, từ quá dài cắt theo ký tự, dòng rỗng giữ nhịp baseline. Không kerning (chấp nhận v1, nới 2%). - Giữ font theo thang 4 mức (nhất quán triết lý iteration 2):
- Font NHÚNG parse được + phủ đủ glyph → nhúng lại chính bytes đó (glyph y hệt);
- Family nhóm base-14 (Helvetica/Times/Courier + Arial alias) + text ASCII →
font chuẩn PDF qua
FPDFText_LoadStandardFont— BaseFont giữ tên chuẩn, KHÔNG nhúng, file không phình; đo width bằng font metric-compatible (Liberation/Arial); - Font hệ thống cùng họ (coverage-checked); 4. fallback mặc định.
- Dòng mới giữ phần tuyến tính matrix của run neo (scale/nghiêng), gốc tại (lề trái, baseline thứ i); Tf = unscaled của run neo → cỡ hiển thị không đổi.
- Đoạn dài ra thì các dòng mới nối xuống dưới theo đúng nhịp baseline (hành vi tương tự Foxit khi khối text nở ra).
UI
- Double-click: đoạn ≥2 dòng → textarea sửa cả đoạn (
paragraphLinesgom dòng baseline cách đều ±25%, cỡ chữ tương đồng, giao ngang ≥30%); 1 dòng → ô sửa dòng như iteration 2. - Textarea WYSIWYG: đúng font/cỡ/màu/kiểu + line-height đúng nhịp baseline; nội dung khởi tạo = các dòng nối bằng khoảng trắng (đoạn chảy tự nhiên); Enter = ngắt cứng, Ctrl+Enter hoặc bấm ra ngoài = áp dụng, Esc = huỷ.
- 1 commit = 1 op
reflowText= 1 nấc undo.
Test mới (3 integration + 3 unit)
reflow_wraps_and_keeps_embedded_font— đoạn 3 dòng font nhúng + text Việt dài gấp mấy lần → bẻ >3 dòng, mọi dòng trong bề rộng khối, font nhúng giữ nguyên, baseline đều 15pt±2, text cũ biến mất.reflow_hard_break_creates_new_line—\nra đúng 2 dòng cách 1 nhịp.reflow_base14_ascii_keeps_standard_font— Helvetica không nhúng + ASCII dài → nhiều dòng, BaseFont vẫn "Helvetica" chuẩn (builtin, không nhúng).- Unit
wrap_lines: greedy theo từ / ngắt cứng + dòng rỗng / cắt từ quá dài.
Vá theo phản hồi file thật (Word-export, run cắt theo từ/ký tự)
Kiểm với PDF thật xuất từ Word (tiêu đề 2 dòng bị cắt thành 41 run per-word/ per-char xen run rỗng, cỡ 22.5pt + 20pt, bbox chữ có dấu tụt thấp):
- UI cluster DÒNG theo GIAO DỌC (≥50% chiều cao nhỏ) thay vì so baseline — miễn nhiễm dấu tiếng Việt; overlay 1 khung/dòng (file thật: 231 ô → 18 khung); chọn/kéo/xoá/đổi thuộc tính tác động CẢ DÒNG (batch ops).
- Đúp mở CẢ ĐOẠN giữ xuống dòng (
\ngiữa các dòng gốc — sửa được cả vị trí ngắt dòng); đúp trúng KHE giữa các từ vẫn mở (hit-test theo điểm); click trong ô sửa di chuyển con trỏ, không thoát edit; ô che kín chữ cũ. - Engine ReflowText tự NỞ danh sách run theo bbox khối (tâm nằm trong union + 2pt) — không bao giờ sót run rỗng/lệch bbox → hết cảnh chữ mới đè chữ cũ; phát hiện khối căn giữa (tâm các dòng trùng tâm khối) → dòng mới giữ căn giữa. 2 test hồi quy mới (nở indices, giữ căn giữa) — 19 test edit.
Vá vòng 2 theo phản hồi file thật (□ thay dấu cách, vỡ dòng, ô sửa lệch)
Ba lỗi user báo sau khi thử bản build 6 trên chính file Word-export:
- Dấu cách thành ô vuông □ sau khi lưu: PDFium
FPDFText_LoadFontnạp LẠI font subset (Word xuất) rồiSetTextsẽ map U+0020 ra.notdef(glyph hộp) dù cmap của font CÓ space — lỗi nằm ở PDFium, không phải font. Fix: tầng 0 mới của thang font reflow — dùng LẠI chính font object gốc trong PDF (lấyPdfFontTokentừ 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 thêm bản font nữa. 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ỡ). Khi ghi từng dòng cũng đọc lại ngay để tự kiểm — nếu PDFium vẫn âm thầm vứt ký tự thì gỡ và ghi lại theo 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. - Thêm 1 ký tự làm vỡ dòng ("HỢP"→"HỢPP" đẩy "LOG" rơi xuống dòng mới):
dòng cứng (
\ntừ ô sửa) giờ được NỞ tới +35% bề rộng khối (chặn mép trang) trước khi phải bẻ lại — hành vi hộp text tự nở của Foxit; khối căn giữa nở đều 2 phía quanh tâm. - Mỗi dòng giữ đúng CỠ CHỮ GỐC của dòng đó (tiêu đề 22.5/20pt không còn
bị ép cả khối về cỡ run neo): kế hoạch reflow ghi
line_stylestheo từng baseline gốc, dòng cứng thứ i ăn cỡ dòng gốc thứ i. - Ô sửa WYSIWYG: thay textarea bằng
contenteditable— mỗi dòng gốc là 1 div với đúng font/cỡ/màu/đậm-nghiêng CỦA DÒNG ĐÓ, khối căn giữa hiện căn giữa, khung đặt trùng bbox khối (bù lệch dọc line-box) → chữ trong ô nằm NGUYÊN vị trí chữ gốc khi bắt đầu sửa; con trỏ đặt đúng chỗ vừa đúp (caretRangeFromPoint). - Kiểm chứng trên file thật: sửa "HỢP"→"HỢPP" giữ đúng 2 dòng căn giữa cỡ
22.5/20pt, font
BAAAAA+TimesNewRomanPS-BoldMTgốc, dấu cách nguyên vẹn, render so khớp bản gốc. 2 test hồi quy mới (reflow_keeps_per_line_font_sizes,reflow_hard_line_grows_without_rewrap) — 21 test edit.
Vá vòng 3 — ô sửa trùng khít chữ gốc bằng ĐO–HIỆU CHỈNH (fix hồi quy vòng 2)
User báo (kèm ảnh): ô sửa hiện chữ SAI font + SAI cỡ + SAI vị trí. Ba nguyên nhân:
- Hồi quy vòng 2:
perLineAdvancestính line-height mỗi dòng bằng KHOẢNG HỞ giữa 2 bbox (bottom trên − top dưới≈ 0/âm → kẹp 8px) thay vì khoảng cách baseline → các dòng dồn cục. Fix: advance = hiệu mép DƯỚI 2 bbox liền kề (bbox PDFium ôm sát glyph nên bottom bám baseline). - Sai font: engine trả family PostScript CamelCase ("TimesNewRoman",
"SegoeUI") không khớp tên font cài trên Windows → CSS rơi về serif/sans mặc
định. Fix:
cssFontStacktách CamelCase thành tên có dấu cách + bảng alias (Helvetica→Arial, TimesNewRomanPS→Times New Roman…). - Sai cỡ với file lạ: cỡ px suy mở từ
font_sizeengine, không tự kiểm. Fix:fitEditLinesToPdf— vòng đo–hiệu chỉnh sau khi mount ô sửa: (1) CỠ: chiều cao MỰC của đúng chuỗi đó (canvasmeasureTextactualBoundingBox, đo ở cỡ tham chiếu 100px để né sai số hinting ~5% ở cỡ nhỏ) phải bằng chiều cao bbox dòng PDF (cùng là ink-bbox, PDFium bounds = union glyph box) — engine báo cỡ lệch bao nhiêu cũng tự sửa về đúng, lệch <12% coi là khác metric font → giữ cỡ engine (ổn định file chuẩn); (2) VỊ TRÍ: quy 2 phía về MÉP MỰC TRÊN (DOM: Range rect + (fontAscent − inkAscent) từ canvas; PDF: rect.top) rồi bùmargin-top/text-indenttừng dòng (dòng đầu bù vào khung để nền trắng che kín); khối căn giữa cũng bù ngang qua text-indent. Thêm: giữ thụt lề từng dòng; đúp từ viewer thường nay đặt con trỏ đúng điểm đúp (toạ độ giả lập từ điểm PDF, chờimg.decode()trước khi đo). - Kiểm chứng: harness HTML mô phỏng trang bằng canvas (chữ gốc đỏ, bbox mực thật làm ground truth) + Edge headless chụp 3 kịch bản: (A) engine báo cỡ sai MỘT NỬA + family CamelCase → tự sửa về đúng, lệch ≤0.4px dọc / 0px ngang; (B) engine báo đúng → dead-band giữ nguyên, trùng khít 100%; (C) khối căn giữa Times → lệch ngang ≤1.3px. (Không unit-test tự động được vì cần layout engine thật; harness lưu ở scratchpad phiên làm việc.)
Vá vòng 4 — khối sửa chuẩn Foxit: tách đoạn theo FORMAT, nền trong suốt, bullet, in đậm
User test build 11 trên file thật, báo 4 lỗi (kèm ảnh). Đối chiếu hành vi Foxit (help.foxit.com — Edit Text): mỗi ĐOẠN là 1 text block riêng, click vào đâu chỉ sửa block đó, reflow trong block; muốn sửa nhiều block phải Link thủ công. Sửa tương ứng:
- Gom đoạn quá tham (tiêu đề + dòng code + thân bài + nhãn đỏ dính 1 khối):
paragraphLinesgiờ chỉ gom dòng kề nhau CÙNG FORMAT — cùng font family- đậm/nghiêng + màu (của run chữ đầu dòng), cỡ chênh ≤1.25× (tiêu đề 22.5/20 vẫn 1 đoạn, tiêu đề ≠ thân bài) — cộng điều kiện hình học cũ (hở ≤1.3× cao dòng, giao ngang ≥30%, nhịp đều ±35%).
- Bullet thành ô vuông □ trong ô sửa: bullet Word/LibreOffice là run font
Symbol/Wingdings mã PUA (U+F0B7…) DOM không render được. Thêm
isMarkerRun(run chữ trái nhất của dòng, ≤2 ký tự, PUA/ký tự bullet/font symbol; "-","*", "o" chỉ nhận khi có khoảng hở rõ với chữ sau) +stripLineMarkers: marker bị LOẠI khỏi ô sửa/commit/rect khối (thụt lề tính từ CHỮ) — bullet giữ nguyên trên trang như Foxit. Đúp trúng chính bullet vẫn mở khối chữ của dòng đó. - Ô sửa nền trắng che nền gốc: giờ mở ô xong render NGẦM bản trang đã ẨN
các run đang sửa (
edit_apply_to_tempvới Delete — chỉ lấy ảnh, commit vẫn tính trên editBase gốc) → đổi ảnh stage + nền ô chuyển TRONG SUỐT: thấy dải màu nền/bullet/kẻ bảng dưới chữ đang gõ, đúng cảm giác Foxit. Chờ render thì tạm nền trắng; huỷ/không đổi → trả ảnh gốc; file tạm vào editTemps để dọn. - Mất in đậm dù gốc đậm: file Word/Chrome xuất subset không khai
/FontWeight và tên không chứa "Bold" → engine
text_object_stylethêm fallback đọc OS/2 usWeightClass ≥600 + cờ italic từ bytes font nhúng (ttf-parser);list_objectscache theo tên font (file 200+ run chung vài font, không đọc lại bytes). - Kiểm chứng: test Node trích HÀM THẬT từ main.js (11 case: tách khối theo format/màu, tiêu đề 2 cỡ vẫn 1 đoạn, bullet loại khỏi khối + rect từ chữ, đúp trúng marker, phân biệt "-" gạch đầu dòng vs "-5°C") — tất cả PASS. Phần render nền ẩn cần app thật → kiểm trên build CI.
Vá vòng 5 — TÌM RA GỐC RỄ: field IPC lệch tên; kiểm chứng E2E trên app thật
User test build 12 với file thật (LibreOffice export, NotoSans): vẫn mất in
đậm. Lần này debug TRÊN CHÍNH APP đang chạy (WebView2 remote debugging qua
WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port + CDP) và
phát hiện nguyên nhân gốc của cả chuỗi lỗi font: DTO Tauri serialize
rename_all = "camelCase" (fontBold, fontFamily, fontSize…) nhưng toàn
bộ code edit-mode trong main.js đọc snake_case (font_bold…) → mọi thuộc
tính font trong UI luôn undefined từ đầu: không đậm, sai family, cỡ mặc
định 12 (giải thích luôn ảnh lỗi "chữ nhỏ đều nhau" ban đầu — vòng 3 đã chữa
TRIỆU CHỨNG cỡ chữ bằng đo mực, vòng này chữa đúng BỆNH). Fix: đổi hết sang
camelCase (nhất quán với phần còn lại của app: widthPt, pageIndex…).
- Engine đọc kiểu chữ từ bytes font — vá tiếp cho LibreOffice: subset của
LibreOffice VỨT bảng OS/2 (kiểm bằng qpdf trích FontFile2: chỉ còn cmap/
glyf/head/hhea/…) nên ttf-parser
weight()trả 400 mặc định. Thêmfontmatch::style_from_font_bytesparse sfnt thủ công: OS/2 usWeightClass/ fsSelection nếu còn, fallbackhead.macStyle(bảng bắt buộc, bit0=bold bit1=italic — file thật: 0x1 cho NotoSans-Bold, 0x0 cho Regular). 2 unit test sfnt tự dựng. (File này BaseFont có "-Bold" nên tên đã bắt được; vá để chống file tên không nói gì.) - Kéo-thả mở file: nghe
tauri://drag-drop(Tauri v2, dragDropEnabled mặc định) → thả .pdf từ Explorer vào cửa sổ là mở (thoát edit/organize mode trước); thả file khác báo "Chỉ mở được file .pdf". - Ẩn khung dòng overlay khi ô sửa đang mở (nền trong suốt làm khung chấm lộ qua gây nhiễu) — khôi phục khi đóng.
- Kiểm chứng E2E trên app thật + file thật (không cần rebuild): inject
hàm đã sửa vào app build-12 đang chạy qua CDP
Runtime.evaluate(main.js là classic script nên function declaration đè được binding global), gọiedit_list_objectslấy run thật, điều khiển mở ô sửa và chụp màn hình: tiêu đề 2 dòng bold 25.48px (=18.5pt đúng engine) 1 khối nền trong suốt; khối bullet 8 dòng không PUA, bullet gốc còn nguyên trên trang. Test Node (hàm thật + dữ liệu run thật từ engine): tiêu đề gom [27,28], khối bullet loại sạch OpenSymbol, nhãn đậm màu tách riêng — tất cả PASS.
Giới hạn ghi nhận (v1, sẽ nâng ở vòng sau)
- Đoạn justify (giãn đều 2 lề) reflow về căn trái; chưa kerning; khối text XOAY chưa reflow theo hướng xoay (dòng mới đặt theo trục ngang).
- Khối lẫn NHIỀU font/cỡ trong 1 ĐOẠN: cỡ giữ theo TỪNG DÒNG; font/màu theo run neo (dòng lẫn nhiều font sẽ thống nhất font).
Iteration 1: 43/43 test, đã chạy app thật + chụp ảnh xác minh.
Iteration 2 — Giữ font gốc + trải nghiệm Foxit (mới)
Đóng khoảng cách lớn nhất so với Foxit được chỉ ra khi review: mọi lần sửa text đều bị đổi sang font hệ thống (Helvetica → Arial/DejaVu), kể cả khi chỉ đổi cỡ/màu; bold/italic gốc bị mất; sửa theo mảnh run thay vì cả dòng.
Engine (edit.rs viết lại + fontmatch.rs mới)
SetText3 tầng, ưu tiên GIỮ FONT GỐC (sửa tại chỗ bằngFPDFText_SetText, không xoá/tạo lại):- In-place an toàn chắc chắn: text mới chỉ dùng ký tự đã có trong run; hoặc
font không nhúng (base-14 Helvetica/Times…) + text mới toàn ASCII
(BaseFont khai báo giữ nguyên trong file — đúng hành vi Foxit); hoặc cmap
của font (đọc qua
FPDFFont_GetFontData+ttf-parser) phủ đủ ký tự mới → font giữ nguyên 100%, gồm cả font NHÚNG với tiếng Việt. - Thiếu glyph thật sự → thay bằng font hệ thống CÙNG HỌ, đúng đậm/nghiêng
(
fontmatch::find_family_font_bytes: bảng family Windows/macOS, Liberation metric-compatible +fc-matchtrên Linux; alias Helvetica→Arial, Times→Times New Roman…), có kiểm coverage trước khi nhận. - Bất đắc dĩ mới rơi về font mặc định (
find_font_bytes— nay có đủ biến thể đậm/nghiêng trên cả Linux).
- In-place an toàn chắc chắn: text mới chỉ dùng ký tự đã có trong run; hoặc
font không nhúng (base-14 Helvetica/Times…) + text mới toàn ASCII
(BaseFont khai báo giữ nguyên trong file — đúng hành vi Foxit); hoặc cmap
của font (đọc qua
- Đổi cỡ chữ tại chỗ không đụng font: nhân matrix
[k,0,0,k, e(1−k), f(1−k)](neo baseline như Foxit). Fix bug phóng đại kép: cỡ chữ nghĩa "hiển thị" (đã nhân matrix scale) — trước đây đặt cỡ 20 lên text có matrix scale ×2 ra 40pt. ObjectInfothêmfont_family(đã làm sạch tên PostScript/subset),font_bold,font_italic,font_embedded— nguồn từname()(BaseFont) vìfamily()của font không nhúng trả tên stub nội bộ PDFium ("Chrom Sans OTF").SetText/AddTextnhậnfont_family/bold/italicdạng Option — None = giữ nguyên; Some = chủ động đổi (đổi font qua tầng 2).- Bẫy mới ghi nhận:
set_matrixcủa pdfium-render 0.8.37 là alias deprecated củaapply_matrix(NHÂN DỒN, không thay thế) — phải tự dựng matrix delta.
UI (main.js + toolbar)
- Sửa CẢ DÒNG như Foxit: double-click gom các run cùng baseline liền kề (PDF hay cắt 1 dòng thành nhiều run) → 1 ô sửa cho cả dòng; commit = SetText run đầu + Delete các run còn lại (1 batch, 1 nấc undo).
- WYSIWYG khi gõ: ô sửa dùng đúng family (CSS xấp xỉ theo
font_family), cỡ, màu, đậm/nghiêng của run gốc; nền trắng che text cũ. - Giữ nguyên mặc định: mọi commit sửa text gửi
nullcho font/cỡ/màu/kiểu — chỉ field người dùng chủ động đổi mới được gửi. - Toolbar Format kiểu Foxit: dropdown Font (mặc định "(giữ nguyên: X)"), nút B/I toggle theo kiểu thật của run (đổi biến thể cùng họ), Cỡ chữ, Màu — đổi thuộc tính KHÔNG đổi font.
- Kéo-thả live: khung đi theo con trỏ ngay khi kéo (move + resize), thả chuột mới commit; resize neo góc trên-trái đúng như preview.
- Dọn file tạm: mọi file
ff_edit_*.pdfcủa phiên sửa được xoá khi thoát chế độ/lưu (commandedit_cleanup— chỉ xoá đúng pattern trong %TEMP%). - Hint hiển thị font đang chọn:
Tên font · cỡ pt · font nhúng/hệ thống.
Test mới (7): edit_roundtrip.rs 14 test + fontmatch 3 unit
set_text_keeps_original_font— sửa ASCII trên Helvetica base-14 GIỮ NGUYÊN "Helvetica" (ép ký tự ngoài charset cũ để không ăn may).set_text_preserves_embedded_font_vietnamese— font NHÚNG + tiếng Việt giữ nguyên font (case quan trọng nhất với tài liệu Việt).vietnamese_on_base14_uses_matched_family— tiếng Việt trên base-14 (không có glyph Việt để giữ) match đúng họ metric-compatible (Liberation/Arial), không rơi bừa về generic.font_size_change_keeps_font_and_anchors;font_size_change_respects_matrix_scale(hồi quy bug phóng đại kép);bold_override_substitutes_font_and_keeps_text;line_merge_batch_set_text_plus_delete(luồng UI gộp dòng).
Đối chiếu FINAL TARGET & RULE
- Vướng thư viện (PDFium
set_textre-encode theo subset) → không cắt giảm: kiểm tra coverage bằng chính font bytes + luật charset/ASCII để tận dụngset_textan toàn, chỉ thay font khi về mặt vật lý không còn glyph để giữ — và khi thay thì match cùng họ như Foxit. Đúng thứ tự a→c của luật.
Iteration 1 (giữ nguyên bên dưới để tham chiếu)
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
PdfPageTextObjectlà 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ớiEditOp: 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 quaapply_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_indexbị đánh dấu unowned →DropgọiFPDFPageObj_Destroygây SEGFAULT với PDFium build hiện tại. Phảistd::mem::forgetobject đã 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 theoscale = 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_applyghi file mới rồiloadDocument.
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" — khoảng cách chính còn lại so với Foxit
(Iteration 2 đã sửa được CẢ DÒNG; bước tiếp: gom block nhiều dòng theo khoảng
cách baseline + đo width bằng hmtx của font (ttf-parser đã có) + tự bẻ dòng).
Xem kế hoạch ở
docs/14-foxit-gap-analysis.md. - Xoay/lật/shear/clip object; tab Format nâng cao (viền, opacity, căn lề); z-order arrange; convert text→path. (Đổi font-family/B/I cho object cũ: ĐÃ XONG ở Iteration 2.)
- Đặ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).