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
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:
@@ -1302,6 +1302,28 @@ fn pick_save_pem(app: tauri::AppHandle) -> Option<String> {
|
||||
.map(|fp| fp.to_string())
|
||||
}
|
||||
|
||||
/// "Mở gói" mọi Form XObject của trang ra cấp trang (file Canva/Illustrator
|
||||
/// gói cả trang vào form — PDFium không ghi lại được stream form nên phải
|
||||
/// flatten trước khi sửa). Trả None nếu trang không có form (không tạo file).
|
||||
#[tauri::command]
|
||||
fn edit_flatten_to_temp(input: String, page: u16, password: Option<String>) -> Result<Option<String>, String> {
|
||||
let pdfium = pdfium()?;
|
||||
let nanos = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos())
|
||||
.unwrap_or(0);
|
||||
let out = std::env::temp_dir().join(format!("ff_edit_flat_{}_{}.pdf", std::process::id(), nanos));
|
||||
let n = ff_engine::flatten_form_xobjects(
|
||||
&pdfium,
|
||||
std::path::Path::new(&input),
|
||||
page,
|
||||
&out,
|
||||
password.as_deref(),
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(if n > 0 { Some(out.to_string_lossy().into_owned()) } else { None })
|
||||
}
|
||||
|
||||
/// Dọn các file làm việc tạm của chế độ sửa (undo stack). Chỉ xoá file có tên
|
||||
/// `ff_edit_*` nằm đúng trong thư mục temp — không bao giờ đụng file người dùng.
|
||||
#[tauri::command]
|
||||
@@ -1362,6 +1384,7 @@ fn main() {
|
||||
edit_apply_to_temp,
|
||||
edit_preview,
|
||||
edit_cleanup,
|
||||
edit_flatten_to_temp,
|
||||
redact_apply,
|
||||
security_encrypt,
|
||||
security_decrypt,
|
||||
|
||||
+23
-1
@@ -39,6 +39,7 @@ const state = {
|
||||
editArm: null, // 'text' | 'image' khi đang chờ click đặt; null = không
|
||||
editPendingImage: null, // đường dẫn ảnh chờ đặt (Thêm ảnh)
|
||||
editPendingPoint: null, // điểm PDF chờ mở ô sửa (đúp chuột từ viewer thường)
|
||||
editFlatBase: null, // bản "mở gói" Form XObject — không tính là thay đổi
|
||||
editColor: [0, 0, 0], // màu chữ áp khi sửa/thêm text
|
||||
editTemps: [], // mọi file tạm đã materialize trong phiên sửa (để dọn)
|
||||
secMode: false, // thanh Bảo mật (Phase 5) đang mở
|
||||
@@ -2054,6 +2055,7 @@ function enterEditMode(pageIndex) {
|
||||
state.editSel = null;
|
||||
state.editArm = null;
|
||||
state.editPendingImage = null;
|
||||
state.editFlatBase = null;
|
||||
$("annobar").classList.add("hidden");
|
||||
$("editBar").classList.remove("hidden");
|
||||
$("viewport").classList.add("hidden");
|
||||
@@ -2125,6 +2127,25 @@ async function loadEditPage() {
|
||||
invoke("render_page", { path: state.editBase, page: state.editPage, width: EDIT_STAGE_W }),
|
||||
invoke("edit_list_objects", { path: state.editBase, page: state.editPage, password: null }),
|
||||
]);
|
||||
// Trang có object nằm TRONG Form XObject (file Canva/Illustrator gói cả
|
||||
// trang vào form): PDFium không ghi lại được stream form → "mở gói" form
|
||||
// ra cấp trang MỘT LẦN (hiển thị y hệt) rồi nạp lại — từ đó sửa như
|
||||
// trang thường. Bản mở gói không tính là "có thay đổi" cho tới khi sửa.
|
||||
if (objs.some((o) => o.nested)) {
|
||||
try {
|
||||
const flat = await invoke("edit_flatten_to_temp", {
|
||||
input: state.editBase, page: state.editPage, password: null,
|
||||
});
|
||||
if (flat) {
|
||||
state.editTemps.push(flat);
|
||||
state.editBase = flat;
|
||||
state.editFlatBase = flat;
|
||||
return loadEditPage();
|
||||
}
|
||||
} catch (e) {
|
||||
$("editHint").textContent = "Không mở gói được form: " + e;
|
||||
}
|
||||
}
|
||||
$("editImg").src = url;
|
||||
state.editObjects = objs;
|
||||
buildEditOverlay();
|
||||
@@ -2132,7 +2153,8 @@ async function loadEditPage() {
|
||||
} catch (e) {
|
||||
$("editHint").textContent = "Lỗi nạp trang sửa: " + e;
|
||||
}
|
||||
$("edSave").disabled = state.editBase === state.path; // chưa có thay đổi nào
|
||||
// Chưa có thay đổi nào (bản mở gói form không tính).
|
||||
$("edSave").disabled = state.editBase === state.path || state.editBase === state.editFlatBase;
|
||||
updateEditUndoButtons();
|
||||
|
||||
// Đúp chuột từ viewer thường (kiểu Foxit): vào thẳng ô sửa tại điểm đã đúp.
|
||||
|
||||
Reference in New Issue
Block a user