Build Windows portable / build (push) Canceled after 0s
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>
535 lines
41 KiB
Markdown
535 lines
41 KiB
Markdown
# 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 qua `FPDFFormObj_CountObjects/GetObject`
|
||
(pdfium-render 0.8.37: `PdfPageXObjectFormObject::len()/get()` public; match
|
||
thẳng variant `PdfPageObject::XObjectForm` để giữ lifetime trang — accessor
|
||
`as_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. **`index` củ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ân `mat_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).
|
||
- **KẾT LUẬN QUAN TRỌNG NHẤT (3 vòng CI + đọc source PDFium): PDFium hiện
|
||
KHÔNG ghi lại được stream của Form XObject.** Chuỗi chứng cứ:
|
||
1. `FPDFText_SetText` trên con không làm form dirty (`dirty_streams_` chỉ
|
||
được đánh dấu bởi Remove/Insert trên chính form — cpdf_pageobjectholder.cpp);
|
||
2. rút con bằng `FPDFFormObj_RemoveObject` + làm trang dirty → `ProcessForm`
|
||
CÓ chạy generator cho form, NHƯNG `CPDF_PageContentManager` (nơi ghi kết
|
||
quả) chỉ biết key `/Contents` — form là stream TỰ THÂN, không có
|
||
/Contents → content mới rơi vào key /Contents vô nghĩa trên dict form,
|
||
stream thật giữ nguyên (cpdf_pagecontentmanager.cpp, constructor +
|
||
AddStream). Test round-trip bắt đúng triệu chứng: extract sau lưu vẫn
|
||
thấy text cũ.
|
||
- **Giải pháp cuối: MỞ GÓI (flatten) form ra cấp trang** —
|
||
`flatten_form_xobjects`: rút từng con khỏi form (`FPDFFormObj_RemoveObject`
|
||
— cần đổi feature pdfium-render `pdfium_latest`→`pdfium_7350` vì wrapper
|
||
0.8.37 sót gate; đã quét 0 hàm 7543-only bị mất), 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 (đã kiểm vị trí/nội dung), mọi object thành cấp trang → sửa
|
||
bằng đường page-level đã được test kỹ từ iteration 1-3. UI: `loadEditPage`
|
||
thấy object `nested` → gọi `edit_flatten_to_temp` một lần rồi nạp lại
|
||
(bản mở gói không tính là "có thay đổi" cho tới khi sửa thật).
|
||
- Op nhắm vào object trong form khi CHƯA flatten → engine trả lỗi rõ ràng
|
||
("trang chưa được mở gói") thay vì âm thầm ghi bản không đổi — có test.
|
||
- CI test chạy `--test-threads=1`: PDFium serialize qua 1 mutex toàn cục —
|
||
1 test panic (kể cả panic Ở ASSERT khi instance Pdfium còn sống — unwind
|
||
drop guard giữa panic) sẽ poison mutex, mọi test SAU đó trong cùng process
|
||
chết oan tại thread_safe.rs:76. Khi đọc log CI: chỉ failure ĐẦU TIÊN là
|
||
lỗi thật.
|
||
- 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/form `nested`; 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 (6 — `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.
|
||
- `flatten_form_xobjects_unwraps_page` — mở gói: hết form, text thành cấp
|
||
trang, vị trí giữ nguyên, nội dung không đổi.
|
||
- `form_xobject_settext_after_flatten_roundtrip` / `..._reflow_...` /
|
||
`..._delete_...` — sửa/reflow tiếng Việt/xoá SAU flatten round-trip chuẩn.
|
||
- `nested_target_without_flatten_is_rejected` — op vào object trong form khi
|
||
chưa flatten bị từ chối với thông điệp rõ.
|
||
|
||
### 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.
|
||
|
||
### Vòng 5 — zoom/chuyển trang trong chế độ sửa + dòng lẫn nhiều style
|
||
Ba vấn đề user báo (kiểm chứng E2E build 29 trên file dịch 3 trang):
|
||
- **Zoom**: stage sửa hết cứng 820px — `editZoom` riêng (kế thừa zoom viewer),
|
||
Ctrl+wheel/nút ±/dropdown %/Vừa rộng/Vừa trang đều chạy; co giãn CSS ngay,
|
||
render nét sau 220ms; `margin:auto` fix flex-center overflow. Kiểm: 794→1191px.
|
||
- **Chuyển trang**: ▲▼/ô nhập trang nhận biết edit mode (`switchEditPage` —
|
||
giữ undo/thay đổi trang khác vì chung editBase); guard `editLoadSeq` cho
|
||
loadEditPage/sharp-render: lượt nạp chậm không đè lượt mới (race khi
|
||
chuyển trang/zoom nhanh). Kiểm: Trang 2 · 114 đối tượng.
|
||
- **Dòng lẫn style (thường + mono-đậm…)**: ô sửa dựng SPAN theo nhóm run
|
||
cùng style (styleKeyOf/styleGroupsOfLine) — WYSIWYG từng phần; commit đọc
|
||
đoạn-style (`readRich`, text node trần mượn span kề), ≥2 style gửi
|
||
`richLines`. Engine: `ReflowText.rich` — resolve thang giữ-font RIÊNG cho
|
||
từng style-run, token hoá theo từ (giữ glue giữa đoạn dính liền), bẻ dòng
|
||
chung, vẽ mỗi cụm cùng-style 1 object đúng font/cỡ/màu + tự kiểm/fallback
|
||
từng từ. Tầng-0 token gốc tắt khi KHỐI có run nested (token vô hiệu sau
|
||
mở lại document hậu phẫu thuật — trước chỉ xét anchor). Kiểm trên dòng
|
||
"Hiển thị `vmDetails.vm.windowsInitialPassword`.": ô sửa 3 span đúng
|
||
style; sau commit mono giữ NotoSansMono-Bold, "." giữ NotoSans-Regular
|
||
(token gốc), phần sửa sang Arial hợp lệ (subset gốc thiếu glyph Ể hoa).
|
||
Test engine: fixture Helvetica + Courier-Bold cùng dòng.
|
||
|
||
### CỔNG AN TOÀN + giới hạn ghi nhận (iteration 4 v1)
|
||
Kiểm chứng E2E trên file Canva THẬT (CV, 1149 object sau mở gói) phát hiện:
|
||
**generator của PDFium ghi lại cả trang là LOSSY với file phức tạp**:
|
||
- Map font theo (type, BaseFont) — Canva nhúng 2+ subset TRÙNG TÊN
|
||
("AAAAAA+Now-Bold" ×2): run của subset 2 bị ép qua subset 1 → mất glyph
|
||
rải rác (PROFILE → "O ILE").
|
||
- `ProcessImage` BỎ ảnh inline (`IsInline() → return`) — mất ảnh chân dung.
|
||
- Mở form theo vòng append cuối trang → z-order xáo trộn (panel đè lên chữ).
|
||
|
||
→ **`edit_flatten_to_temp` có CỔNG AN TOÀN `page_render_mismatch`**: render
|
||
trang trước/sau mở gói (500px, dung sai 12/255/kênh), lệch >0.5% điểm ảnh →
|
||
HUỶ file tạm + trả lỗi rõ; UI hiển thị thông điệp và CHẶN mở ô sửa phần nằm
|
||
trong form (xem/chú thích/tổ chức trang vẫn bình thường). File gói form đơn
|
||
giản (đa số file xuất máy in ảo, fixture test) qua cổng → sửa thoải mái.
|
||
|
||
**Iteration 4b — PHẪU THUẬT stream (ĐÃ LÀM, thay thế flatten ở UI)**:
|
||
`formsurgery.rs` (lopdf): xoá đúng op vẽ chữ (Tj/TJ/'/") của run bị sửa
|
||
trong stream form — map text-child thứ k ↔ show-op thứ k, form con thứ n ↔
|
||
Do-trỏ-form thứ n (thứ tự parse PDFium tuần tự); 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); '/" thay bằng
|
||
T*/Tw+Tc+T* giữ hiệu ứng vị trí. Chữ mới vẽ ở CẤP TRANG (regenerate trang
|
||
không đụng stream form → không dính lỗi trùng tên font). `apply_edits`:
|
||
run nested trong ReflowText/Delete → phẫu thuật ở pha (A2) rồi MỞ LẠI
|
||
document (tầng-0 token gốc tắt cho anchor nested — token vô hiệu sau mở
|
||
lại); cổng an toàn `page_render_mismatch_masked`: pixel ngoài vùng khối sửa
|
||
lệch >0.5% → huỷ output. UI bỏ auto-flatten, sửa nested trong suốt qua đúp
|
||
chuột; SetText thuộc tính/kéo khối trong form chặn-với-thông-điệp.
|
||
Bẫy lopdf: `decompressed_content()` BÁO LỖI với stream không nén (thiếu
|
||
/Filter) → fallback bytes thô.
|
||
**Kiểm chứng E2E trên CV Canva thật (build 26)**: sửa 1 dòng trong form
|
||
(1155 object nested) → commit thành công, chữ mới đúng chỗ/đúng màu, ảnh
|
||
chân dung + toàn bộ chữ khác nguyên vẹn (PROFILE/CONTACT đủ glyph), cổng
|
||
masked pass, undo hoạt động. 2 test round-trip surgical (delete/reflow giữ
|
||
nguyên phần khác của form) + test SetText nested bị từ chối.
|
||
|
||
**Vá theo phản hồi user trên CV Canva (build 27-28)**:
|
||
- **Cỡ chữ ÂM**: pdfium-render lấy vertical scale = `matrix.d` CÓ DẤU; file
|
||
Canva hệ toạ độ lật (d<0) → mọi cỡ chữ âm → UI kẹp ô sửa về 6px (sai vị
|
||
trí/style) và reflow đo bề rộng âm → không bẻ dòng, chữ văng khỏi trang
|
||
("text biến mất" — cổng masked không bắt được vì chữ văng ra ít pixel).
|
||
Fix: chuẩn hoá `.abs()` tại 5 điểm đọc cỡ trong engine + 2 test fixture
|
||
form MA TRẬN LẬT.
|
||
- **"Text biến mất" phần còn lại là ẢO GIÁC hiển thị**: sau commit, app còn
|
||
hiện ảnh render-ẩn (chữ cũ đã xoá) suốt vài giây khi engine áp thay đổi
|
||
(file nặng). Fix: đóng ô sửa → trả ảnh GỐC ngay + hint "Đang áp dụng thay
|
||
đổi…" → ảnh mới thay khi xong.
|
||
- **Tàn dư dấu "~"** (ngã của Ễ): Canva vẽ dấu bằng op RIÊNG có tâm nằm
|
||
ngoài bbox khối +2pt → vòng nở không gom để xoá. Fix: pad nở DỌC 8pt
|
||
(vẫn < nhịp dòng 12pt+ nên không nuốt dòng cạnh).
|
||
- Kiểm chứng cuối (build 28, CV thật): ô sửa đè khít đúng cỡ 37px bold đúng
|
||
màu; commit ra "NGUYỄN VĂN TÀI X" đủ dấu, không tàn dư, trang nguyên vẹn.
|
||
|
||
Giới hạn khác:
|
||
- Flatten làm mất clip/ExtGState/transparency group đặt Ở MỨC FORM — các
|
||
trường hợp này thường cũng làm lệch render → cổng an toàn tự chặn.
|
||
- Cấu trúc file sau khi SỬA thay đổi (form mở ra trang) — hiển thị giữ
|
||
nguyên (đã có cổng kiểm); chỉ xảy ra khi người dùng thật sự lưu.
|
||
- Form lồng sâu >4 vòng mở hoặc >4000 object: phần vượt giữ nguyên trong form.
|
||
|
||
> 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_advance` qua
|
||
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):
|
||
1. Font NHÚNG parse được + phủ đủ glyph → nhúng lại chính bytes đó (glyph y hệt);
|
||
2. 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);
|
||
3. 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** (`paragraphLines` gom
|
||
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` — `\n` ra đú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** (`\n` giữ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_LoadFont` nạp
|
||
LẠI font subset (Word xuất) rồi `SetText` sẽ 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ấy `PdfFontToken` từ 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 (`\n` từ ô 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_styles` theo 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-BoldMT` gố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**: `perLineAdvances` tí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: `cssFontStack` tá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_size` engine, 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 đó (canvas `measureText` actualBoundingBox,
|
||
đ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-indent` từ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):
|
||
`paragraphLines` giờ 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_temp` vớ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_style` thêm
|
||
fallback đọc **OS/2 usWeightClass ≥600 + cờ italic từ bytes font nhúng**
|
||
(ttf-parser); `list_objects` cache 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êm
|
||
`fontmatch::style_from_font_bytes` parse sfnt thủ công: OS/2 usWeightClass/
|
||
fsSelection nếu còn, fallback **`head.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ọi
|
||
`edit_list_objects` lấ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)
|
||
- **`SetText` 3 tầng, ưu tiên GIỮ FONT GỐC** (sửa tại chỗ bằng `FPDFText_SetText`,
|
||
không xoá/tạo lại):
|
||
1. *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.
|
||
2. *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-match` trên Linux; alias Helvetica→Arial,
|
||
Times→Times New Roman…), có kiểm coverage trước khi nhận.
|
||
3. 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).
|
||
- **Đổ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.
|
||
- `ObjectInfo` thêm `font_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`/`AddText` nhận `font_family`/`bold`/`italic` dạng Option — **None =
|
||
giữ nguyên**; Some = chủ động đổi (đổi font qua tầng 2).
|
||
- Bẫy mới ghi nhận: `set_matrix` của pdfium-render 0.8.37 là alias deprecated
|
||
của `apply_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 `null` cho 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_*.pdf` của phiên sửa được xoá khi thoát
|
||
chế độ/lưu (command `edit_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_text` re-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ụng
|
||
`set_text` an 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 `PdfPageTextObject` là 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ới `EditOp`: **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 qua `apply_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_index` bị đánh dấu *unowned* → `Drop` gọi `FPDFPageObj_Destroy` gây **SEGFAULT** với PDFium build hiện tại. Phải `std::mem::forget` object đã 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 theo `scale = 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_apply` ghi file mới rồi `loadDocument`.
|
||
|
||
## 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).
|
||
|
||
|
||
---
|
||
|
||
# Vòng 6 (10/08/2026): sửa NHIỀU TRANG + nút Huỷ + fix đè chữ khi lưu dòng lẫn style
|
||
|
||
## 1. Fix đè chữ khi lưu dòng lẫn style (engine, `edit.rs`)
|
||
|
||
**Triệu chứng:** sửa dòng có nhiều style (thường + mono-bold, ví dụ "Các thuộc
|
||
tính `HttpOnly`, `Secure` và `SameSite`...") → sau commit các mảnh chữ đè lên
|
||
nhau ("auth-storagetừ", dấu "," nằm đè trong chữ trước).
|
||
|
||
**Nguyên nhân:** con trỏ x đặt mảnh KẾ TIẾP tiến theo bề rộng ĐO bằng hmtx
|
||
(ttf-parser). Với subset font mà ttf-parser không parse được (`measure=None`),
|
||
fallback 0.5em/ký tự ƯỚC LƯỢNG THIẾU (đo "HttpOnly" = 36pt trong khi mực thật
|
||
42.6pt) → mảnh sau bắt đầu bên trong mực mảnh trước.
|
||
|
||
**Fix:** sau khi vẽ từng mảnh rich, đọc `obj.bounds()` từ PDFium (mép mực
|
||
thật) và tiến con trỏ bằng `max(x + measured, ink_right)` — cả đường vẽ cả
|
||
mảnh lẫn đường fallback bẻ theo từng từ. Ước lượng thiếu bao nhiêu cũng không
|
||
đè được nữa; ước lượng thừa giữ nguyên hành vi cũ.
|
||
|
||
## 2. Chế độ sửa NHIỀU TRANG (UI)
|
||
|
||
**Yêu cầu user:** "khi edit text ở trang nào thì các trang còn lại biến mất,
|
||
chỉ xem được trang đó" → giờ chế độ sửa hiển thị CỘT MỌI TRANG như viewer.
|
||
|
||
Kiến trúc (`#editPagesCol` trong `#editStage`):
|
||
- Mỗi trang 1 `.edit-slot` kích thước theo zoom. Trang ACTIVE chứa
|
||
`#editPageWrap` (img + overlay sửa được — giữ nguyên toàn bộ code 1-trang
|
||
cũ, chỉ DI CHUYỂN wrap giữa các slot). Trang khác render ảnh read-only
|
||
LƯỜI bằng IntersectionObserver (rootMargin 300px).
|
||
- Stamp `editBase + "|" + zoom` trên mỗi slot: op/undo/redo đổi editBase,
|
||
zoom đổi mức — stamp lệch là render lại khi trang còn trong khung nhìn.
|
||
`refreshRoSlots()` gọi ở cuối `loadEditPage` + sau render nét debounce.
|
||
- **Đúp vào trang read-only** → trang đó thành active + mở ô sửa NGAY tại
|
||
điểm đúp (tái dùng cơ chế `editPendingPoint` của luồng đúp-từ-viewer).
|
||
- **Cuộn** (debounce 200ms): ô số trang bám trang giữa khung nhìn; nếu không
|
||
có ô sửa đang mở, trang đó tự thành active (toolbar/phím tắt luôn tác động
|
||
đúng trang đang xem). Không auto-switch khi ô sửa đang mở — không mất chữ.
|
||
- Zoom: mọi slot co giãn CSS ngay, render nét sau 220ms (trang active) +
|
||
refresh trang read-only đang thấy. Nav ▲▼/ô số trang → `switchEditPage`
|
||
cuộn `scrollIntoView` (start khi nav, nearest khi đúp — không giật).
|
||
|
||
## 3. Nút "↩ Huỷ thay đổi" (UI)
|
||
|
||
Cạnh nút Lưu; bật khi `editBase !== path` (đồng bộ cùng chỗ với edSave trong
|
||
`loadEditPage`). `discardEdits()`: đóng ô sửa đang mở → xoá file tạm
|
||
(`cleanupEditTemps` — editBase cũ nằm trong `editTemps`) → `editBase = path`,
|
||
xoá sạch undo/redo → `loadEditPage()` (stamp đổi nên mọi trang read-only tự
|
||
render lại về nội dung gốc).
|
||
|
||
## 4. Kiểm chứng E2E (CDP trên app thật, file dịch 3 trang)
|
||
|
||
Tiêm bản vá vào build 29 đang chạy (không cần rebuild) rồi đo:
|
||
- Cột 3 slot, wrap nằm đúng slot active, trang 1+2 read-only render khi cuộn.
|
||
- Cuộn tới trang 2 → `pageInput=2`, active tự đổi; slot cũ trả về read-only.
|
||
- Đúp lên trang 3 (read-only) → active=2, ô sửa mở đúng dòng "ĐÁNH GIÁ AN
|
||
TOÀN THÔNG TIN" tại điểm đúp.
|
||
- Huỷ thay đổi: `editBase === path`, edSave/edDiscard disabled, undo/redo
|
||
rỗng, text sửa thử ("KIỂM") biến mất khỏi object list.
|
||
- Zoom 130%: cả 3 slot 1032px, label cập nhật; ▲ về trang 2 cuộn đúng.
|
||
Ảnh: `mp-1-top` → `mp-6-ro-dblclick` (scratchpad phiên làm việc).
|
||
|
||
Fix đè chữ (mục 1) là code Rust — kiểm chứng lại trên build CI mới bằng đúng
|
||
kịch bản đã tái hiện bug (sửa span thường của dòng lẫn style rồi so bounds
|
||
các mảnh sau commit).
|
||
|
||
**Bổ sung sau kiểm chứng build 31 (E2E trên build thật):**
|
||
- Fix thêm: slot trang active phải giữ `min-height` ước tính — lúc vừa vào
|
||
chế độ sửa ảnh chưa load, CSS `:has` cho height auto làm slot cao ≈ 0 →
|
||
cuộn ban đầu lệch cả cột, listener cuộn tự chuyển nhầm trang active.
|
||
- Kiểm đè chữ trên build thật: dòng lẫn style sau commit tăng dần tuyệt đối
|
||
(worst overlap 0pt) — fix mép mực OK. Lưu ý: file test của user
|
||
(Ban_dich_tieng_Viet_...) đã NHIỄM sẵn mảnh "Path=/" lạc chỗ từ lần lưu
|
||
bằng build lỗi cũ — mảnh đó được reflow bảo tồn như marker, KHÔNG phải bug
|
||
mới; đo kiểm phải loại các mảnh giữ-nguyên-vị-trí.
|