Cong an toan cho mo goi form: so render truoc/sau (page_render_mismatch), lech >0.5% pixel -> huy + chan sua voi thong diep ro. E2E tren CV Canva that phat hien generator PDFium lossy (font subset trung ten bi gop lam mat glyph, anh inline bi bo, z-order xao tron) - khong ship flatten mu quang; ke hoach iteration sau (phau thuat stream bang lopdf) ghi trong docs.
Build Windows portable / build (push) Canceled after 0s

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-08 03:22:41 +07:00
co-authored by Claude Fable 5
parent 93d49a67df
commit 5c53c1f987
6 changed files with 110 additions and 10 deletions
+20 -1
View File
@@ -1321,7 +1321,26 @@ fn edit_flatten_to_temp(input: String, page: u16, password: Option<String>) -> R
password.as_deref(),
)
.map_err(|e| e.to_string())?;
Ok(if n > 0 { Some(out.to_string_lossy().into_owned()) } else { None })
if n == 0 {
return Ok(None);
}
// CỔNG AN TOÀN: mở gói phải giữ nguyên hiển thị. Generator của PDFium
// lossy với file phức tạp (font subset TRÙNG TÊN bị gộp làm mất glyph,
// ảnh inline bị bỏ, z-order form lồng nhau xáo trộn) — so render 2 bản,
// lệch quá 0.5% điểm ảnh → HUỶ, giữ nguyên file gốc.
let mismatch =
ff_engine::page_render_mismatch(&pdfium, std::path::Path::new(&input), &out, page, 500)
.map_err(|e| e.to_string())?;
if mismatch > 0.005 {
let _ = std::fs::remove_file(&out);
return Err(format!(
"Trang gói nội dung trong Form XObject quá phức tạp — mở gói làm lệch hiển thị \
{:.1}% điểm ảnh nên đã huỷ để bảo toàn file. Xem/chú thích vẫn bình thường; \
SỬA NỘI DUNG file dạng này sẽ hỗ trợ ở bản sau.",
mismatch * 100.0
));
}
Ok(Some(out.to_string_lossy().into_owned()))
}
/// Dọn các file làm việc tạm của chế độ sửa (undo stack). Chỉ xoá file có tên
+16 -2
View File
@@ -40,6 +40,7 @@ const state = {
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
editFlattenFailed: null, // cổng an toàn từ chối mở gói — thông điệp chặn sửa
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ở
@@ -2056,6 +2057,7 @@ function enterEditMode(pageIndex) {
state.editArm = null;
state.editPendingImage = null;
state.editFlatBase = null;
state.editFlattenFailed = null;
$("annobar").classList.add("hidden");
$("editBar").classList.remove("hidden");
$("viewport").classList.add("hidden");
@@ -2140,10 +2142,14 @@ async function loadEditPage() {
state.editTemps.push(flat);
state.editBase = flat;
state.editFlatBase = flat;
state.editFlattenFailed = null;
return loadEditPage();
}
} catch (e) {
$("editHint").textContent = "Không mở gói được form: " + e;
// Cổng an toàn từ chối (mở gói làm lệch hiển thị) → vẫn xem được,
// nhưng chặn sửa các phần nằm trong form với thông điệp rõ.
state.editFlattenFailed = String(e);
$("editHint").textContent = String(e);
}
}
$("editImg").src = url;
@@ -2610,7 +2616,15 @@ function paragraphLines(o) {
// đường code duy nhất, giữ nguyên cấu trúc xuống dòng, commit qua reflow.
// `ev` (nếu có) = sự kiện chuột để đặt con trỏ đúng chỗ vừa đúp.
function startTextEdit(o, ev) {
startBlockTextEdit(o, paragraphLines(o), ev);
const lines = paragraphLines(o);
// Trang gói form phức tạp không mở gói được an toàn → text vẫn nằm TRONG
// form, engine không lưu sửa đổi được — báo rõ thay vì mở ô sửa vô dụng.
if (lines.some((l) => l.runs.some((r) => r.nested))) {
$("editHint").textContent =
state.editFlattenFailed || "Phần này nằm trong Form XObject — chưa sửa được trên file này.";
return;
}
startBlockTextEdit(o, lines, ev);
}
// Sửa cả đoạn WYSIWYG (chuẩn Foxit Edit Text): contenteditable phủ kín khối,