From c885567b269edfa074c7562eef89da086eba54da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20=C4=90=C3=ACnh=20Nam?= Date: Mon, 10 Aug 2026 10:33:54 +0700 Subject: [PATCH] Fix de chu DOC khi luu: vong no pad_y=8 nuot nham DONG KE TREN khoi reflow Bug thu 2 cua "luu xong de len text khac" (bug 1 - de NGANG trong dong - da fix bang mep muc that): vong no doc 8pt (sinh ra de bat dau tieng Viet ve bang op roi) nuot ca run cao co chu cua DONG KE TREN khi gian dong sat (~13-14pt): tam muc run dong tren lot vao bbox+8pt -> ca cau bi xoa, first_baseline troi len 1 dong -> khoi ve lai de len vi tri dong tren, sot manh mono dau dong (tam cao hon nguong chut). Tai hien tren file that cua user (trang 2, khoi "Cac thuoc tinh HttpOnly..." nuot dong "Path=/ khien refresh token..." ngay tren). Ban dau ket luan nham la "file nhiem artifact" - DINH CHINH trong docs/12: file sach, Path=/ la dau bullet hop le. Fix: vung no 8pt chi duoc nuot op NHO (cao < 0.6 x median cao run trong khoi - manh dau); op cao co chu phai co tam trong bbox LOI. Test hoi quy reflow_tight_leading_does_not_swallow_line_above (gian dong 13pt, khoi 2 dong duoi, dong tren phai song sot + dong moi khong cham muc dong tren). Co-Authored-By: Claude Fable 5 --- crates/ff-engine/src/edit.rs | 26 +++++++- crates/ff-engine/tests/edit_roundtrip.rs | 78 ++++++++++++++++++++++++ docs/12-phase4-summary.md | 15 +++-- 3 files changed, 112 insertions(+), 7 deletions(-) diff --git a/crates/ff-engine/src/edit.rs b/crates/ff-engine/src/edit.rs index d5fc5b3..926152e 100644 --- a/crates/ff-engine/src/edit.rs +++ b/crates/ff-engine/src/edit.rs @@ -957,17 +957,37 @@ pub fn apply_edits( // Nới DỌC rộng hơn ngang: dấu tiếng Việt (ngã của Ễ…) // nhiều file (Canva) vẽ bằng op riêng nằm TRÊN cap — // tâm ngoài bbox +2pt → sót lại dấu lơ lửng sau sửa. - // 8pt vẫn nhỏ hơn nhịp dòng (≥12pt) nên không nuốt - // nhầm dòng bên cạnh. + // NHƯNG vùng nở 8pt chỉ được nuốt op NHỎ (mảnh dấu + // thấp hơn hẳn chữ): với giãn dòng sát (~1.25×) tâm + // run của DÒNG KỀ cũng lọt vào bbox+8pt — nuốt nhầm + // cả dòng trên làm khối vẽ lại trồi lên 1 dòng, đè + // chữ (bug "lưu xong đè text khác"). Op cao cỡ chữ + // thường phải có tâm trong bbox LÕI mới bị nuốt. let pad_x = 2.0; let pad_y = 8.0; + let h_med = { + let mut hs: Vec = idxs + .iter() + .filter_map(|&i| text_rect(i)) + .map(|r| r.top - r.bottom) + .filter(|h| *h > 0.1) + .collect(); + hs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + if hs.is_empty() { 0.0 } else { hs[hs.len() / 2] } + }; for i in 0..entries.len() as u16 { if idxs.contains(&i) { continue; } let Some(r) = text_rect(i) else { continue }; let (cx, cy) = ((r.left + r.right) / 2.0, (r.bottom + r.top) / 2.0); - if cx >= bb.left - pad_x + let in_core = cx >= bb.left + && cx <= bb.right + && cy >= bb.bottom + && cy <= bb.top; + let small = (r.top - r.bottom) < (h_med * 0.6).max(2.0); + if (in_core || small) + && cx >= bb.left - pad_x && cx <= bb.right + pad_x && cy >= bb.bottom - pad_y && cy <= bb.top + pad_y diff --git a/crates/ff-engine/tests/edit_roundtrip.rs b/crates/ff-engine/tests/edit_roundtrip.rs index 1c11dee..dad8fc4 100644 --- a/crates/ff-engine/tests/edit_roundtrip.rs +++ b/crates/ff-engine/tests/edit_roundtrip.rs @@ -789,6 +789,84 @@ fn reflow_expands_indices_to_cover_block() { assert!(!norm.contains("Mảnh một"), "run đã đưa phải bị thay: {norm:?}"); } +/// Hồi quy bug "lưu xong đè text khác": vùng nở dọc 8pt (bắt dấu tiếng Việt +/// vẽ rời) KHÔNG được nuốt run cao cỡ chữ của DÒNG KỀ TRÊN khi giãn dòng sát +/// (~1.0–1.25×) — nuốt nhầm làm dòng trên bị xoá, first_baseline trồi lên và +/// khối mới vẽ đè lên vị trí dòng trên. +#[test] +fn reflow_tight_leading_does_not_swallow_line_above() { + let pdf = pdfium(); + let fx = tmp("ff_swallow_fx.pdf"); + let out = tmp("ff_swallow_out.pdf"); + let mk = |y: f32, s: &str| EditOp::AddText { + x: 60.0, + y, + text: s.into(), + font_size: 14.0, + color: [0, 0, 0, 255], + font_family: None, + bold: false, + italic: false, + }; + // Giãn dòng 13pt < 1.0× cỡ chữ (sát hơn cả file Word thật) — tâm mực của + // dòng trên lọt hẳn vào bbox khối + 8pt. + ff_engine::apply_edits( + &pdf, + &sample(), + 0, + &[ + mk(427.0, "Dòng trên phải sống sót"), + mk(414.0, "Dòng sửa thứ nhất trong khối"), + mk(401.0, "Dòng sửa thứ hai trong khối"), + ], + &fx, + None, + ) + .expect("fixture 3 dòng sát nhau"); + let b = find_text_index(&pdf, &fx, "Dòng sửa thứ nhất"); + let c = find_text_index(&pdf, &fx, "Dòng sửa thứ hai"); + + ff_engine::apply_edits( + &pdf, + &fx, + 0, + &[EditOp::ReflowText { + indices: vec![b, c], + text: "Nội dung mới dòng một\nnội dung mới dòng hai".into(), + rich: None, + }], + &out, + None, + ) + .expect("reflow khối 2 dòng dưới"); + + let text = ff_engine::extract_text(&pdf, &out, 0, None).expect("extract"); + let norm = text.split_whitespace().collect::>().join(" "); + assert!( + norm.contains("Dòng trên phải sống sót"), + "dòng kề trên bị nuốt nhầm vào khối reflow: {norm:?}" + ); + assert!(norm.contains("Nội dung mới dòng một"), "text mới thiếu: {norm:?}"); + + // Dòng mới thứ nhất phải nằm ở vị trí dòng B cũ (~414), KHÔNG trồi lên + // vị trí dòng trên (~428): mực của nó không được chạm mực dòng trên. + let objs = ff_engine::list_objects(&pdf, &out, 0, None).expect("list"); + let above = objs + .iter() + .find(|o| o.text.as_deref().map_or(false, |t| t.contains("Dòng trên phải"))) + .expect("dòng trên còn đó"); + let new1 = objs + .iter() + .find(|o| o.text.as_deref().map_or(false, |t| t.contains("Nội dung mới dòng một"))) + .expect("dòng mới 1"); + assert!( + new1.rect.top <= above.rect.bottom + 1.5, + "dòng mới trồi lên đè dòng trên: new1.top={} vs above.bottom={}", + new1.rect.top, + above.rect.bottom + ); +} + /// Khối gốc CĂN GIỮA (dòng ngắn thụt vào, tâm trùng nhau) → các dòng mới cũng /// đặt căn giữa theo tâm khối (không dồn hết về lề trái). #[test] diff --git a/docs/12-phase4-summary.md b/docs/12-phase4-summary.md index b1fb4ab..d52c128 100644 --- a/docs/12-phase4-summary.md +++ b/docs/12-phase4-summary.md @@ -528,7 +528,14 @@ các mảnh sau commit). 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í. + (worst overlap 0pt) — fix mép mực NGANG đạt. +- ĐÍNH CHÍNH + bug thứ 2 (đè DỌC): ban đầu tưởng file test của user "nhiễm" + mảnh `Path=/` lạc chỗ — SAI. File gốc sạch: `Path=/` là ĐẦU BULLET hợp lệ + của dòng "Path=/ khiến refresh token…" nằm NGAY TRÊN khối sửa. Thủ phạm là + vòng nở dọc `pad_y = 8` (sinh ra để bắt dấu tiếng Việt vẽ rời): với giãn + dòng sát (~13–14pt) tâm mực run của DÒNG KỀ TRÊN lọt vào bbox+8pt → cả câu + bị nuốt (xoá), baseline của nó thành `first_baseline` → khối vẽ lại trồi + lên 1 dòng đè chữ, còn sót mảnh mono `Path=/` (tâm cao hơn ngưỡng chút). + Fix: vùng nở 8pt chỉ nuốt op NHỎ (cao < 0.6 × median cao run trong khối — + mảnh dấu); op cao cỡ chữ phải có tâm trong bbox LÕI. Test hồi quy: + `reflow_tight_leading_does_not_swallow_line_above` (giãn dòng 13pt).