Dot 1 "edit chuan moi PDF": CID/Identity-H font, format TUNG CHU, edit OCR, bu metrics, splash, Open-with
Build Windows portable / build (push) Canceled after 0s

1) fontfix v2 (PDF tu Word/InDesign): tong hop bang name/post khi subset
   thieu (OTS bat buoc); font CID/Identity-H KHONG co cmap -> dung cmap
   (3,1) fmt4 tu /ToUnicode x /CIDToGIDMap (Identity: GID=CID; stream 2B/CID
   parse tu descendant font); chan GID >= numGlyphs (maxp). tounicode.rs tra
   FontMapping {code_to_uni, is_cid, cid_to_gid}.

2) Format TUNG CHU trong o sua (chuan Word/Foxit): boi den -> B/I (nut
   toolbar khong cuop focus + Ctrl+B/I). UI tach span theo vung chon, dat
   data-bold/italic-override; commit gui override tung doan (RichSegDto
   bold/italic). Engine: bang style rich key theo (run nguon, override) -
   doi bien the thi lay font BIEN THE THAT cung ho (fontmatch -> builtin ->
   substitute), khong dung lai font goc. Test:
   rich_seg_bold_italic_override_uses_variant_font.

3) Edit file OCR (scan + text an): khoi toan run render-mode INVISIBLE ->
   ve rect TRANG che vung chu cu tren anh + ve text moi HIEN mau den (ca 4
   duong ve: rich chinh/fallback tu, 1-style chinh/fallback). Test:
   ocr_invisible_block_becomes_visible_with_cover.

4) Bu metrics kieu Adobe Sans MM trong o sua: span mang data-pdfw (be rong
   that tren trang); fitEditLinesToPdf them pass letter-spacing de moi nhom
   style chiem DUNG quang goc khi font thay the lech metrics (>2%); kep
   +/-18% co chu.

5) Khoi dong het "do": cua so tao AN (visible:false), UI show() ngay khi
   frontend san sang + man splash che phan nap dau; luoi an toan Rust hien
   cua so sau 4s neu JS chet som.

6) Open with FoFreeXit.exe: co che argv (initial_file) van hoat dong - loi
   nam o registry Applications tro exe CU (ban portable bi di chuyen/ghi
   de) lam Windows hien lai hop chon app. Fix tu chua: moi lan khoi dong
   dang ky lai HKCU (Applications command + ProgID FoFreeXit.pdf +
   OpenWithProgids .pdf) tro exe hien tai, chay nen qua reg.exe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 16:44:06 +07:00
co-authored by Claude Fable 5
parent a3b00da10d
commit 74f161a627
9 changed files with 839 additions and 64 deletions
+144 -2
View File
@@ -1546,8 +1546,18 @@ fn rich_reflow_preserves_mixed_styles() {
indices: vec![idx_a, idx_b],
text: "Trang quan tri hien thi MOI windowsInitialPassword".into(),
rich: Some(vec![vec![
RichSeg { text: "Trang quan tri hien thi MOI ".into(), style: idx_a },
RichSeg { text: "windowsInitialPassword".into(), style: idx_b },
RichSeg {
text: "Trang quan tri hien thi MOI ".into(),
style: idx_a,
bold: None,
italic: None,
},
RichSeg {
text: "windowsInitialPassword".into(),
style: idx_b,
bold: None,
italic: None,
},
]]),
}],
&out,
@@ -1593,3 +1603,135 @@ fn rich_reflow_preserves_mixed_styles() {
plain.rect
);
}
/// Fixture kiểu file đã OCR: text render mode 3 (INVISIBLE — lớp chữ ẩn đè
/// trên ảnh scan để tìm kiếm/copy).
fn build_invisible_text_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 f1 = doc.add_object(dictionary! {
"Type" => "Font",
"Subtype" => "Type1",
"BaseFont" => "Helvetica",
});
let content_id = doc.add_object(Stream::new(
dictionary! {},
b"BT /F1 14 Tf 3 Tr 72 700 Td (Van ban OCR nhan dien tu anh) Tj ET".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! { "Font" => dictionary! { "F1" => f1 } },
"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 OCR");
}
/// Sửa khối text ẨN (file scan đã OCR): engine phải CHE vùng chữ cũ bằng rect
/// trắng và vẽ text mới HIỆN màu đen — như "edit scanned document" của Foxit.
#[test]
fn ocr_invisible_block_becomes_visible_with_cover() {
let pdf = pdfium();
let input = tmp("ff_edit_ocr_fx.pdf");
build_invisible_text_pdf(&input);
let out = tmp("ff_edit_ocr_out.pdf");
let idx = find_text_index(&pdf, &input, "Van ban OCR");
let paths_before = ff_engine::list_objects(&pdf, &input, 0, None)
.expect("list in")
.iter()
.filter(|o| o.kind == ObjectKind::Path)
.count();
ff_engine::apply_edits(
&pdf,
&input,
0,
&[EditOp::ReflowText {
indices: vec![idx],
text: "Van ban OCR DA SUA lai noi dung".into(),
rich: None,
}],
&out,
None,
)
.expect("reflow khối OCR");
let objs = ff_engine::list_objects(&pdf, &out, 0, None).expect("list out");
let paths_after = objs.iter().filter(|o| o.kind == ObjectKind::Path).count();
assert!(paths_after > paths_before, "phải có rect trắng che vùng chữ scan cũ");
let t = objs
.iter()
.find(|o| o.text.as_deref().map(|t| t.contains("DA SUA")).unwrap_or(false))
.unwrap_or_else(|| panic!("thiếu text mới: {objs:?}"));
assert_eq!(t.color, Some([0, 0, 0, 255]), "chữ mới phải màu đen (hiện)");
}
/// Format TỪNG CHỮ: bôi đen 1 từ bấm B → seg mang override bold=true; engine
/// phải vẽ từ đó bằng BIẾN THỂ đậm thật (không phải giả lập), các từ quanh
/// giữ nguyên; italic tương tự.
#[test]
fn rich_seg_bold_italic_override_uses_variant_font() {
use ff_engine::RichSeg;
let pdf = pdfium();
let input = tmp("ff_edit_rich_override.pdf");
build_mixed_style_pdf(&input);
let out = tmp("ff_edit_rich_override_out.pdf");
let idx_a = find_text_index(&pdf, &input, "Trang quan tri");
ff_engine::apply_edits(
&pdf,
&input,
0,
&[EditOp::ReflowText {
indices: vec![idx_a],
text: "Trang quan tri hien thi".into(),
rich: Some(vec![vec![
RichSeg { text: "Trang ".into(), style: idx_a, bold: None, italic: None },
RichSeg { text: "quan tri ".into(), style: idx_a, bold: Some(true), italic: None },
RichSeg { text: "hien ".into(), style: idx_a, bold: None, italic: Some(true) },
RichSeg { text: "thi".into(), style: idx_a, bold: None, italic: None },
]]),
}],
&out,
None,
)
.expect("apply_edits rich override");
let objs = ff_engine::list_objects(&pdf, &out, 0, None).expect("list out");
let find = |needle: &str| {
objs.iter()
.find(|o| o.text.as_deref().map(|t| t.contains(needle)).unwrap_or(false))
.unwrap_or_else(|| panic!("thiếu run '{needle}': {objs:?}"))
};
let plain = find("Trang");
let bolded = find("quan tri");
let italicized = find("hien");
assert_eq!(plain.font_bold, Some(false), "'Trang' giữ thường: {:?}", plain.font_name);
assert_eq!(
bolded.font_bold,
Some(true),
"'quan tri' phải thành ĐẬM thật: {:?}",
bolded.font_name
);
assert_eq!(
italicized.font_italic,
Some(true),
"'hien' phải thành NGHIÊNG thật: {:?}",
italicized.font_name
);
// Cùng baseline, thứ tự trái→phải giữ nguyên.
assert!((bolded.rect.bottom - plain.rect.bottom).abs() < 4.0);
assert!(bolded.rect.left > plain.rect.left && italicized.rect.left > bolded.rect.left);
}