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
+216 -19
View File
@@ -17,17 +17,18 @@
use std::collections::HashMap;
use crate::tounicode::FontMapping;
/// Bytes font sẵn sàng cho FontFace: vá nếu cần, còn lại giữ nguyên.
/// `code_to_uni`: map mã→Unicode từ /ToUnicode của PDF (cho các mã tuỳ biến
/// ngoài ASCII — chữ Việt trong subset LibreOffice); None → chỉ ASCII
/// identity + MacRoman.
/// `mapping`: giải mã từ PDF (/ToUnicode + CIDToGIDMap) — cho mã tuỳ biến của
/// subset LibreOffice lẫn font CID/Identity-H (Word/InDesign) KHÔNG có cmap.
pub(crate) fn web_compatible_font(
data: &[u8],
bold: bool,
italic: bool,
code_to_uni: Option<&HashMap<u32, u32>>,
mapping: Option<&FontMapping>,
) -> Vec<u8> {
try_fix(data, bold, italic, code_to_uni).unwrap_or_else(|| data.to_vec())
try_fix(data, bold, italic, mapping).unwrap_or_else(|| data.to_vec())
}
const TAG_TRUE: u32 = u32::from_be_bytes(*b"true");
@@ -37,21 +38,23 @@ fn try_fix(
data: &[u8],
bold: bool,
italic: bool,
code_to_uni: Option<&HashMap<u32, u32>>,
mapping: Option<&FontMapping>,
) -> Option<Vec<u8>> {
if data.len() < 12 {
return None;
}
let ver = u32::from_be_bytes(data[0..4].try_into().ok()?);
if ver != 0x0001_0000 && ver != TAG_TRUE && ver != TAG_OTTO {
return None; // không phải sfnt (CFF trần...) — không đụng vào
return None; // không phải sfnt — CFF trần đi đường wrap_bare_cff
}
let num = u16::from_be_bytes(data[4..6].try_into().ok()?) as usize;
if num == 0 || data.len() < 12 + num * 16 {
return None;
}
let mut tables: Vec<([u8; 4], Vec<u8>)> = Vec::with_capacity(num + 1);
let mut tables: Vec<([u8; 4], Vec<u8>)> = Vec::with_capacity(num + 4);
let mut has_os2 = false;
let mut has_name = false;
let mut has_post = false;
for i in 0..num {
let o = 12 + i * 16;
let tag: [u8; 4] = data[o..o + 4].try_into().ok()?;
@@ -60,30 +63,164 @@ fn try_fix(
if off.checked_add(len)? > data.len() {
return None; // danh mục hỏng — trả nguyên trạng cho lành
}
if &tag == b"OS/2" {
has_os2 = true;
match &tag {
b"OS/2" => has_os2 = true,
b"name" => has_name = true,
b"post" => has_post = true,
_ => {}
}
tables.push((tag, data[off..off + len].to_vec()));
}
// cmap (1,0) format 0/6 — OTS không hỗ trợ → chuyển sang (3,1) format 4.
let num_glyphs = read_num_glyphs(&tables);
// cmap: (1,0) fmt 0/6 → chuyển sang (3,1) fmt 4; KHÔNG có cmap (font
// CID/Identity-H của Word) → dựng mới từ /ToUnicode × CIDToGIDMap.
let mut cmap_changed = false;
if let Some(ci) = tables.iter().position(|(t, _)| t == b"cmap") {
if let Some(converted) = convert_cmap(&tables[ci].1, code_to_uni) {
tables[ci].1 = converted;
cmap_changed = true;
match tables.iter().position(|(t, _)| t == b"cmap") {
Some(ci) => {
if let Some(converted) =
convert_cmap(&tables[ci].1, mapping.map(|m| &m.code_to_uni))
{
tables[ci].1 = converted;
cmap_changed = true;
} else if !cmap_has_unicode_subtable(&tables[ci].1) {
if let Some(cm) = mapping.and_then(|m| build_cid_cmap(m, num_glyphs)) {
tables[ci].1 = cm;
cmap_changed = true;
}
}
}
None => {
if let Some(cm) = mapping.and_then(|m| build_cid_cmap(m, num_glyphs)) {
tables.push((*b"cmap", cm));
cmap_changed = true;
}
}
}
if has_os2 && ver == 0x0001_0000 && !cmap_changed {
if has_os2 && has_name && has_post && ver == 0x0001_0000 && !cmap_changed {
return None; // đã chuẩn sẵn — giữ nguyên bytes gốc
}
if !has_os2 {
let os2 = build_os2(&tables, bold, italic)?;
tables.push((*b"OS/2", os2));
}
// OTS cũng BẮT BUỘC name + post — subset CID hay bỏ cả hai.
if !has_name {
tables.push((*b"name", build_name()));
}
if !has_post {
tables.push((*b"post", build_post()));
}
tables.sort_by(|a, b| a.0.cmp(&b.0));
Some(assemble(ver, tables))
}
/// numGlyphs từ maxp (chặn GID rác khi dựng cmap); 0 = không rõ, không chặn.
fn read_num_glyphs(tables: &[([u8; 4], Vec<u8>)]) -> u16 {
tables
.iter()
.find(|(t, _)| t == b"maxp")
.and_then(|(_, b)| b.get(4..6))
.and_then(|s| s.try_into().ok())
.map(u16::from_be_bytes)
.unwrap_or(0)
}
/// cmap đã có subtable Unicode OTS chấp nhận chưa ((0,*), (3,1), (3,10))?
fn cmap_has_unicode_subtable(cmap: &[u8]) -> bool {
let Some(n) = cmap.get(2..4).and_then(|s| s.try_into().ok()).map(u16::from_be_bytes) else {
return false;
};
for i in 0..n as usize {
let o = 4 + i * 8;
let Some(plat) = cmap.get(o..o + 2).and_then(|s| s.try_into().ok()).map(u16::from_be_bytes)
else {
return false;
};
let Some(enc) =
cmap.get(o + 2..o + 4).and_then(|s| s.try_into().ok()).map(u16::from_be_bytes)
else {
return false;
};
if plat == 0 || (plat == 3 && (enc == 1 || enc == 10)) {
return true;
}
}
false
}
/// cmap (3,1) format 4 cho font CID/Identity-H: Unicode ← /ToUnicode(mã),
/// GID ← CIDToGIDMap(mã) (Identity = chính mã). Bỏ mapping ngoài BMP/GID rác.
fn build_cid_cmap(m: &FontMapping, num_glyphs: u16) -> Option<Vec<u8>> {
if !m.is_cid || m.code_to_uni.is_empty() {
return None;
}
let mut pairs: Vec<(u32, u16)> = Vec::new();
for (&code, &uni) in &m.code_to_uni {
if uni == 0 || uni > 0xFFFF {
continue;
}
let gid = match &m.cid_to_gid {
Some(map) => *map.get(code as usize).unwrap_or(&0),
None => {
if code > 0xFFFF {
continue;
}
code as u16
}
};
if gid == 0 || (num_glyphs != 0 && gid >= num_glyphs) {
continue;
}
pairs.push((uni, gid));
}
if pairs.is_empty() {
return None;
}
pairs.sort_by_key(|p| p.0);
pairs.dedup_by_key(|p| p.0);
Some(build_cmap_format4(&pairs))
}
/// name tối thiểu (format 0, family + subfamily, Windows Unicode en-US) — OTS
/// bắt buộc có bảng name; nội dung không ảnh hưởng hiển thị (CSS đặt alias).
fn build_name() -> Vec<u8> {
let entries: [(u16, &str); 2] = [(1, "FFX Embedded"), (2, "Regular")];
let mut strings: Vec<u8> = Vec::new();
let mut records: Vec<u8> = Vec::new();
for (id, s) in entries {
let start = strings.len() as u16;
for u in s.encode_utf16() {
strings.extend_from_slice(&u.to_be_bytes());
}
let len = strings.len() as u16 - start;
records.extend_from_slice(&3u16.to_be_bytes()); // platform Windows
records.extend_from_slice(&1u16.to_be_bytes()); // encoding Unicode BMP
records.extend_from_slice(&0x0409u16.to_be_bytes()); // en-US
records.extend_from_slice(&id.to_be_bytes());
records.extend_from_slice(&len.to_be_bytes());
records.extend_from_slice(&start.to_be_bytes());
}
let mut out = Vec::with_capacity(6 + records.len() + strings.len());
out.extend_from_slice(&0u16.to_be_bytes()); // format 0
out.extend_from_slice(&(entries.len() as u16).to_be_bytes());
out.extend_from_slice(&((6 + records.len()) as u16).to_be_bytes()); // stringOffset
out.extend_from_slice(&records);
out.extend_from_slice(&strings);
out
}
/// post version 3.0 (32 byte, không tên glyph) — đủ cho OTS.
fn build_post() -> Vec<u8> {
let mut v = Vec::with_capacity(32);
v.extend_from_slice(&0x0003_0000u32.to_be_bytes()); // version 3.0
v.extend_from_slice(&0i32.to_be_bytes()); // italicAngle (Fixed)
v.extend_from_slice(&(-100i16).to_be_bytes()); // underlinePosition
v.extend_from_slice(&50i16.to_be_bytes()); // underlineThickness
v.extend_from_slice(&0u32.to_be_bytes()); // isFixedPitch
v.extend_from_slice(&[0u8; 16]); // min/max mem Type42/Type1
v
}
/// MacRoman 0x80–0xFF → Unicode (bảng chuẩn Apple) — fallback cho mã cao khi
/// không có /ToUnicode.
const MACROMAN_HI: [u16; 128] = [
@@ -412,11 +549,15 @@ mod tests {
let subset = fake_subset();
let fixed = web_compatible_font(&subset, true, false, None);
assert_ne!(fixed, subset, "phải được vá");
// Tag chuẩn hoá + có OS/2 + danh mục sắp theo tag.
// Tag chuẩn hoá + có OS/2 + name/post tổng hợp + danh mục sắp theo tag.
assert_eq!(&fixed[0..4], &0x0001_0000u32.to_be_bytes());
let dir = table_dir(&fixed);
let tags: Vec<&str> = dir.iter().map(|(t, _, _)| t.as_str()).collect();
assert_eq!(tags, vec!["OS/2", "glyf", "head", "hhea"], "sắp theo tag: {tags:?}");
assert_eq!(
tags,
vec!["OS/2", "glyf", "head", "hhea", "name", "post"],
"sắp theo tag: {tags:?}"
);
// OS/2 đọc lại đúng nội dung suy ra.
let (_, off, len) = dir[0].clone();
let os2 = &fixed[off..off + len];
@@ -482,12 +623,17 @@ mod tests {
0
}
fn mapping(uni: HashMap<u32, u32>, is_cid: bool, cid_to_gid: Option<Vec<u16>>) -> FontMapping {
FontMapping { code_to_uni: uni, is_cid, cid_to_gid }
}
#[test]
fn unsupported_cmap_is_converted_to_windows_format4() {
let subset = fake_subset_with_cmap();
let mut uni = HashMap::new();
uni.insert(1u32, 0x1EA3u32); // mã 1 = 'ả'
let fixed = web_compatible_font(&subset, false, false, Some(&uni));
let m = mapping(uni, false, None);
let fixed = web_compatible_font(&subset, false, false, Some(&m));
assert_ne!(fixed, subset);
// Tìm bảng cmap mới.
let num = u16::from_be_bytes(fixed[4..6].try_into().unwrap()) as usize;
@@ -511,4 +657,55 @@ mod tests {
.collect();
assert!(tags.contains(&"OS/2".to_string()), "tags: {tags:?}");
}
/// Font CID/Identity-H kiểu Word: KHÔNG có cmap, mã = CID, GID = CID
/// (Identity) hoặc qua bảng CIDToGIDMap → dựng cmap (3,1) fmt 4 mới.
fn fake_cid_subset() -> Vec<u8> {
let mut head = vec![0u8; 54];
head[18..20].copy_from_slice(&2048u16.to_be_bytes());
let mut hhea = vec![0u8; 36];
hhea[4..6].copy_from_slice(&1638i16.to_be_bytes());
hhea[6..8].copy_from_slice(&(-410i16).to_be_bytes());
let mut maxp = vec![0u8; 32];
maxp[4..6].copy_from_slice(&50u16.to_be_bytes()); // numGlyphs = 50
let tables: Vec<([u8; 4], Vec<u8>)> =
vec![(*b"glyf", vec![0u8; 8]), (*b"head", head), (*b"hhea", hhea), (*b"maxp", maxp)];
assemble(0x0001_0000, tables)
}
#[test]
fn cid_font_without_cmap_gets_synthesized_cmap() {
let subset = fake_cid_subset();
let mut uni = HashMap::new();
uni.insert(5u32, 0x1EA3u32); // CID 5 = 'ả'
uni.insert(6u32, 0x0041u32); // CID 6 = 'A'
uni.insert(99u32, 0x0042u32); // CID 99 ≥ numGlyphs → phải bị bỏ
// Identity: GID = CID.
let m = mapping(uni.clone(), true, None);
let fixed = web_compatible_font(&subset, false, false, Some(&m));
assert_ne!(fixed, subset, "phải được vá");
let dir = table_dir(&fixed);
let tags: Vec<&str> = dir.iter().map(|(t, _, _)| t.as_str()).collect();
for need in ["cmap", "OS/2", "name", "post"] {
assert!(tags.contains(&need), "thiếu {need}: {tags:?}");
}
let (_, off, len) = dir[tags.iter().position(|t| *t == "cmap").unwrap()].clone();
let cm = fixed[off..off + len].to_vec();
assert_eq!(lookup_f4(&cm, 0x1EA3), 5, "'ả' → GID 5 (Identity)");
assert_eq!(lookup_f4(&cm, 0x41), 6, "'A' → GID 6");
assert_eq!(lookup_f4(&cm, 0x42), 0, "GID ngoài numGlyphs bị bỏ");
// Có bảng CIDToGIDMap: CID 5 → GID 7.
let mut c2g = vec![0u16; 10];
c2g[5] = 7;
c2g[6] = 8;
let m2 = mapping(uni, true, Some(c2g));
let fixed2 = web_compatible_font(&subset, false, false, Some(&m2));
let dir2 = table_dir(&fixed2);
let tags2: Vec<&str> = dir2.iter().map(|(t, _, _)| t.as_str()).collect();
let (_, o2, l2) = dir2[tags2.iter().position(|t| *t == "cmap").unwrap()].clone();
let cm2 = fixed2[o2..o2 + l2].to_vec();
assert_eq!(lookup_f4(&cm2, 0x1EA3), 7, "'ả' → GID 7 theo CIDToGIDMap");
assert_eq!(lookup_f4(&cm2, 0x41), 8);
}
}