Fix cỡ chữ ÂM với file toạ độ lật (Canva): pdfium-render lấy vertical scale = matrix.d CÓ DẤU — chuẩn hoá .abs() tại mọi điểm đọc cỡ trong engine (list/geo/anchor/inplace/capture). Cỡ âm là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ữ mới văng khỏi trang (cổng pass vì ít pixel) → 'text biến mất'. Kèm 2 test fixture form MA TRẬN LẬT (cỡ dương ~24, reflow đặt chữ đúng vùng cũ)
Build Windows portable / build (push) Canceled after 0s
Build Windows portable / build (push) Canceled after 0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -550,7 +550,9 @@ pub fn list_objects(
|
||||
info.font_italic = Some(italic);
|
||||
info.font_embedded = t.font().is_embedded().ok();
|
||||
// Cỡ hiển thị = cỡ đã scale của run × scale dọc của form chứa nó.
|
||||
info.font_size = Some(t.scaled_font_size().value * mat_vscale(entry.acc));
|
||||
// .abs(): pdfium-render lấy scale = matrix.d CÓ DẤU — file Canva
|
||||
// dùng hệ toạ độ lật (d<0) làm cỡ chữ ÂM, vỡ mọi công thức phía sau.
|
||||
info.font_size = Some((t.scaled_font_size().value * mat_vscale(entry.acc)).abs());
|
||||
info.color = t
|
||||
.fill_color()
|
||||
.ok()
|
||||
@@ -874,8 +876,11 @@ pub fn apply_edits(
|
||||
return Ok(None);
|
||||
};
|
||||
let vs = mat_vscale(entry.acc);
|
||||
let (tf, scaled) =
|
||||
(t.unscaled_font_size().value, t.scaled_font_size().value * vs);
|
||||
// .abs(): matrix lật (Canva) cho cỡ ÂM — chuẩn hoá dương.
|
||||
let (tf, scaled) = (
|
||||
t.unscaled_font_size().value.abs(),
|
||||
(t.scaled_font_size().value * vs).abs(),
|
||||
);
|
||||
let m = obj.matrix().map_err(err)?;
|
||||
let (_, fy) = mat_apply(entry.acc, m.e(), m.f());
|
||||
let b_raw = obj.bounds().map(|q| quad_to_rect(&q)).unwrap_or(Rect {
|
||||
@@ -968,9 +973,9 @@ pub fn apply_edits(
|
||||
let m = anchor.matrix().map_err(err)?;
|
||||
let mc = mat_mul(mat_of(&m), entry.acc);
|
||||
let tok = t.font().token();
|
||||
let unscaled = t.unscaled_font_size().value;
|
||||
let unscaled = t.unscaled_font_size().value.abs();
|
||||
let scaled =
|
||||
(t.scaled_font_size().value * mat_vscale(entry.acc)).max(1.0);
|
||||
(t.scaled_font_size().value * mat_vscale(entry.acc)).abs().max(1.0);
|
||||
let color = t.fill_color().unwrap_or(PdfColor::new(0, 0, 0, 255));
|
||||
let embedded = t.font().is_embedded().unwrap_or(false);
|
||||
let data = t.font().data().ok();
|
||||
@@ -1234,7 +1239,7 @@ pub fn apply_edits(
|
||||
// Cỡ hiển thị so theo KHÔNG GIAN TRANG (nhân scale form) — target
|
||||
// từ UI cũng theo trang; hệ số k không đổi khi áp trong form.
|
||||
let (old_text, scaled) = match obj.as_text_object() {
|
||||
Some(t) => (t.text(), t.scaled_font_size().value * mat_vscale(entry.acc)),
|
||||
Some(t) => (t.text(), (t.scaled_font_size().value * mat_vscale(entry.acc)).abs()),
|
||||
None => continue,
|
||||
};
|
||||
if *text != old_text {
|
||||
@@ -1295,8 +1300,8 @@ pub fn apply_edits(
|
||||
let vs = mat_vscale(entry.acc);
|
||||
let (unscaled, scaled, color) = if let Some(t) = obj.as_text_object() {
|
||||
(
|
||||
t.unscaled_font_size().value,
|
||||
t.scaled_font_size().value * vs,
|
||||
t.unscaled_font_size().value.abs(),
|
||||
(t.scaled_font_size().value * vs).abs(),
|
||||
t.fill_color().unwrap_or(PdfColor::new(0, 0, 0, 255)),
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -1303,3 +1303,102 @@ fn flatten_is_visually_lossless_on_simple_form() {
|
||||
let mm = ff_engine::page_render_mismatch(&pdf, &input, &out, 0, 500).expect("mismatch");
|
||||
assert!(mm < 0.005, "mở gói phải giữ nguyên hiển thị, lệch {:.3}%", mm * 100.0);
|
||||
}
|
||||
|
||||
/// Fixture kiểu CANVA: form /Matrix lật trục Y, text bên trong cũng lật
|
||||
/// (net = xuôi). pdfium-render trả vertical scale CÓ DẤU → cỡ chữ từng bị ÂM.
|
||||
fn build_flipped_form_pdf(path: &std::path::Path) {
|
||||
use lopdf::{dictionary, Document, Object, Stream};
|
||||
let mut doc = Document::with_version("1.5");
|
||||
let pages_id = doc.new_object_id();
|
||||
let font_id = doc.add_object(dictionary! {
|
||||
"Type" => "Font",
|
||||
"Subtype" => "Type1",
|
||||
"BaseFont" => "Helvetica",
|
||||
});
|
||||
let form_id = doc.add_object(Stream::new(
|
||||
dictionary! {
|
||||
"Type" => "XObject",
|
||||
"Subtype" => "Form",
|
||||
"BBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
|
||||
"Matrix" => vec![1.into(), 0.into(), 0.into(), Object::Real(-1.0), 0.into(), 792.into()],
|
||||
"Resources" => dictionary! { "Font" => dictionary! { "F1" => font_id } },
|
||||
},
|
||||
b"BT /F1 24 Tf 1 0 0 -1 72 92 Tm (Hello flipped form) Tj ET BT /F1 18 Tf 1 0 0 -1 72 142 Tm (Second flipped stays) Tj ET".to_vec(),
|
||||
));
|
||||
let content_id = doc.add_object(Stream::new(dictionary! {}, b"q /Fx1 Do Q".to_vec()));
|
||||
let page_id = doc.add_object(dictionary! {
|
||||
"Type" => "Page",
|
||||
"Parent" => pages_id,
|
||||
"MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
|
||||
"Resources" => dictionary! { "XObject" => dictionary! { "Fx1" => form_id } },
|
||||
"Contents" => content_id,
|
||||
});
|
||||
doc.objects.insert(
|
||||
pages_id,
|
||||
Object::Dictionary(dictionary! {
|
||||
"Type" => "Pages",
|
||||
"Kids" => vec![page_id.into()],
|
||||
"Count" => 1,
|
||||
}),
|
||||
);
|
||||
let catalog_id = doc.add_object(dictionary! { "Type" => "Catalog", "Pages" => pages_id });
|
||||
doc.trailer.set("Root", catalog_id);
|
||||
doc.save(path).expect("lưu fixture flipped form");
|
||||
}
|
||||
|
||||
// Ma trận lật (Canva): cỡ chữ phải DƯƠNG và đúng ~24, rect đúng vùng y=700.
|
||||
#[test]
|
||||
fn flipped_form_reports_positive_font_size() {
|
||||
let pdf = pdfium();
|
||||
let input = tmp("ff_edit_flip_list.pdf");
|
||||
build_flipped_form_pdf(&input);
|
||||
let objs = ff_engine::list_objects(&pdf, &input, 0, None).expect("list");
|
||||
let t = objs
|
||||
.iter()
|
||||
.find(|o| o.kind == ObjectKind::Text && o.text.as_deref().map(|s| s.contains("Hello flipped")).unwrap_or(false))
|
||||
.unwrap_or_else(|| panic!("phải thấy text: {objs:?}"));
|
||||
let fs = t.font_size.unwrap_or(0.0);
|
||||
assert!(fs > 20.0 && fs < 28.0, "cỡ chữ phải DƯƠNG ~24, được {fs}");
|
||||
assert!(t.rect.bottom > 660.0 && t.rect.top < 740.0, "rect quanh y=700: {:?}", t.rect);
|
||||
}
|
||||
|
||||
// Reflow trong form LẬT: chữ mới hiện đúng (không văng khỏi trang do bề
|
||||
// rộng đo bằng cỡ âm), chữ cũ mất, dòng kia giữ nguyên.
|
||||
#[test]
|
||||
fn flipped_form_surgical_reflow() {
|
||||
let pdf = pdfium();
|
||||
let input = tmp("ff_edit_flip_reflow.pdf");
|
||||
build_flipped_form_pdf(&input);
|
||||
let out = tmp("ff_edit_flip_reflow_out.pdf");
|
||||
let idx = find_text_index(&pdf, &input, "Hello flipped");
|
||||
|
||||
ff_engine::apply_edits(
|
||||
&pdf,
|
||||
&input,
|
||||
0,
|
||||
&[EditOp::ReflowText {
|
||||
indices: vec![idx],
|
||||
text: "Đổi chữ trong form lật trục".into(),
|
||||
}],
|
||||
&out,
|
||||
None,
|
||||
)
|
||||
.expect("reflow trong form lật");
|
||||
|
||||
let text = ff_engine::extract_text(&pdf, &out, 0, None).expect("extract");
|
||||
let norm = text.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
assert!(norm.contains("Đổi chữ trong form"), "chữ mới phải có: {text:?}");
|
||||
assert!(!norm.contains("Hello flipped"), "chữ cũ phải mất: {text:?}");
|
||||
assert!(norm.contains("Second flipped stays"), "dòng kia phải nguyên: {text:?}");
|
||||
// Chữ mới phải nằm QUANH vị trí cũ (y~700) — không văng khỏi trang.
|
||||
let objs = ff_engine::list_objects(&pdf, &out, 0, None).expect("list out");
|
||||
let new_run = objs
|
||||
.iter()
|
||||
.find(|o| o.kind == ObjectKind::Text && o.text.as_deref().map(|s| s.contains("Đổi chữ")).unwrap_or(false))
|
||||
.unwrap_or_else(|| panic!("phải thấy run mới: {objs:?}"));
|
||||
assert!(
|
||||
new_run.rect.left > 50.0 && new_run.rect.left < 200.0 && new_run.rect.bottom > 650.0 && new_run.rect.top < 740.0,
|
||||
"run mới phải quanh chỗ cũ: {:?}",
|
||||
new_run.rect
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user