Form XObject vòng 4 — kiến trúc cuối: MỞ GÓI form ra cấp trang trước khi sửa
Build Windows portable / build (push) Canceled after 0s

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>
This commit is contained in:
2026-08-08 02:26:30 +07:00
co-authored by Claude Fable 5
parent 1b3cf15664
commit d4b09e0ed8
6 changed files with 306 additions and 224 deletions
+35 -35
View File
@@ -23,28 +23,28 @@ Type0/CID nhúng), trong khi `list_objects` chỉ duyệt object cấp trang.
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).
- **Áp op cho con trong form — bài học đắt giá từ CI vòng 1**: `dirty_streams_`
của form CHỈ được đánh dấu bởi `RemovePageObject`/`InsertPageObjectAtIndex`
trên CHÍNH form (xem cpdf_pageobjectholder.cpp) — **`FPDFText_SetText` trên
con KHÔNG làm form dirty → sửa in-place trong form không bao giờ được ghi
ra file** (test canary bắt được đúng lỗi này: extract sau lưu vẫn thấy text
cũ). Cách làm đúng: mọi sửa/xoá con trong form = **RÚT THẬT khỏi form qua
`FPDFFormObj_RemoveObject`** (đánh dấu dirty → `ProcessForm` regenerate
stream form khi lưu) **+ với SetText thì 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ọ)
với matrix đã compose (`SetTextMode::NestedRecreate`). Thứ tự rút: path sâu
trước, cùng cha con lớn trước (rút làm tụt index các em). Transform
(kéo/resize) con trong form: bỏ qua, UI chặn kéo.
- **Đổi feature pdfium-render `pdfium_latest`(7543) → `pdfium_7350`** (cả
ff-engine lẫn app): wrapper 0.8.37 gate `remove_object` của form chỉ mở cho
7350/future dù binding có đủ cho 7543 (sót gate upstream); API 7350 là
subset của DLL bblanchon mới → không mất gì (đã quét: 0 hàm 7543-only).
- **Bài học CI vòng 2 — "đánh thức" trang**: `GenerateContent` chỉ ghi lại
stream trang khi có object CẤP TRANG dirty; rút con chỉ làm FORM dirty →
op thuần trong form (Delete) không kích hoạt gì → `ProcessForm` không chạy.
Fix: sau khi rút con, nhân identity matrix vào form tổ tiên cấp trang
(Transform luôn SetDirty, giá trị không đổi) → trang regenerate → form
regenerate theo.
- **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
@@ -60,17 +60,17 @@ Type0/CID nhúng), trong khi `list_objects` chỉ duyệt object cấp 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)
### 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.
- `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.
- `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
@@ -78,12 +78,12 @@ 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_RemoveObject` cho 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ê.
- Flatten làm MẤT clip path/ExtGState/transparency group đặt Ở MỨC FORM
(file Canva wrapper toàn trang thường không có — hiển thị y hệt; file có
clip ở form sẽ khác biệt, sẽ xử lý khi gặp thật).
- Cấu trúc file sau khi SỬA thay đổi (form được mở ra trang) — nội dung và
hiển thị giữ nguyên; chỉ xảy ra khi người dùng thật sự lưu thay đổi.
- 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.