Files
FoFreeXit/crates/ff-engine/src/fontfix.rs
T
adminandClaude Fable 5 f77092bff2
Build Windows portable / build (push) Canceled after 0s
fontfix: va subset PDF cho FontFace - them OS/2 + cmap (3,1) format 4 tu /ToUnicode
Build 34 tra bytes font tho nhung OTS cua WebView tu choi het (bat qua CDP
Log): (1) "OS/2: missing required table", (2) "cmap: no supported subtables"
- subset LibreOffice chi co cmap (1,0) format 0, ma tuy bien: ASCII giu
nguyen ma, chu Viet co dau gan ma thap 1-31 (chi /ToUnicode giai nghia duoc).

- fontfix.rs: dung OS/2 v4 (96B) tu head/hhea + co dam/nghieng cua run
  (fsSelection/usWeightClass - khong bi synthesize bold chong); convert cmap
  (1,0) fmt 0/6 -> (3,1) format 4 (segment gop cap lien tiep, moi glyph 1
  segment khi roi rac); ghep lai sfnt: danh muc sort theo tag, pad 4B,
  checksum bang + head.checkSumAdjustment (tong = B1B0AFBA); tag 'true' ->
  0x00010000. Khong phai sfnt (CFF tran) tra nguyen trang.
- tounicode.rs: doc /ToUnicode qua lopdf (bfchar/bfrange ca 2 dang, UTF-16BE
  ke ca surrogate pair; ligature nhieu ky tu bo qua), match BaseFont theo
  suffix sau prefix subset "ABCDEF+", tim ca trong resources cua Form
  XObject (sau 3) cho file Canva. Ma khong co ToUnicode: ASCII identity +
  MacRoman cho ma cao; ma thap khong giai nghia duoc thi bo (ky tu roi xuong
  font ke trong stack CSS).

Kiem chung truoc CI: nhan ban logic sang Node, va bytes THAT cua
NotoSansMono-Bold subset (dump tu app) -> FontFace load OK trong WebView2
that (truoc: SyntaxError). Unit test: OS/2 + checksum + format 4 lookup
('A' giu glyph, 'a?' theo ToUnicode) + bfchar/bfrange parser.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 11:32:17 +07:00

515 lines
22 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Vá font subset trích từ PDF cho WebView dùng được qua FontFace.
//!
//! OTS (OpenType Sanitizer của Chromium) BẮT BUỘC font TrueType phải có bảng
//! `OS/2`, nhưng subset nhúng trong PDF thường bị trình tạo file bỏ bảng này
//! (PDF không cần) → `new FontFace(...)` ném "OTS parsing error: OS/2:
//! missing required table". Đã bắt được đúng thông điệp đó qua CDP Log trên
//! font NotoSansMono-Bold subset (12 bảng, đủ name/post/cmap, thiếu mỗi
//! OS/2).
//!
//! Ở đây dựng OS/2 version 4 tối thiểu từ `head` (unitsPerEm) + `hhea`
//! (ascender/descender/lineGap) + cờ đậm/nghiêng lấy từ chính text object,
//! rồi ghép lại sfnt: danh mục bảng sắp theo tag, offset pad 4 byte,
//! checksum từng bảng + head.checkSumAdjustment chuẩn. Sfnt tag `true`
//! (TrueType kiểu Apple) được chuẩn hoá về 0x00010000. Dữ liệu không phải
//! sfnt (CFF trần của font Type1C...) trả nguyên trạng — UI tự rơi về stack
//! font hệ thống.
use std::collections::HashMap;
/// 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.
pub(crate) fn web_compatible_font(
data: &[u8],
bold: bool,
italic: bool,
code_to_uni: Option<&HashMap<u32, u32>>,
) -> Vec<u8> {
try_fix(data, bold, italic, code_to_uni).unwrap_or_else(|| data.to_vec())
}
const TAG_TRUE: u32 = u32::from_be_bytes(*b"true");
const TAG_OTTO: u32 = u32::from_be_bytes(*b"OTTO");
fn try_fix(
data: &[u8],
bold: bool,
italic: bool,
code_to_uni: Option<&HashMap<u32, u32>>,
) -> 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
}
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 has_os2 = false;
for i in 0..num {
let o = 12 + i * 16;
let tag: [u8; 4] = data[o..o + 4].try_into().ok()?;
let off = u32::from_be_bytes(data[o + 8..o + 12].try_into().ok()?) as usize;
let len = u32::from_be_bytes(data[o + 12..o + 16].try_into().ok()?) as usize;
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;
}
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 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;
}
}
if has_os2 && 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));
}
tables.sort_by(|a, b| a.0.cmp(&b.0));
Some(assemble(ver, tables))
}
/// MacRoman 0x80–0xFF → Unicode (bảng chuẩn Apple) — fallback cho mã cao khi
/// không có /ToUnicode.
const MACROMAN_HI: [u16; 128] = [
0x00C4, 0x00C5, 0x00C7, 0x00C9, 0x00D1, 0x00D6, 0x00DC, 0x00E1, 0x00E0, 0x00E2, 0x00E4,
0x00E3, 0x00E5, 0x00E7, 0x00E9, 0x00E8, 0x00EA, 0x00EB, 0x00ED, 0x00EC, 0x00EE, 0x00EF,
0x00F1, 0x00F3, 0x00F2, 0x00F4, 0x00F6, 0x00F5, 0x00FA, 0x00F9, 0x00FB, 0x00FC, 0x2020,
0x00B0, 0x00A2, 0x00A3, 0x00A7, 0x2022, 0x00B6, 0x00DF, 0x00AE, 0x00A9, 0x2122, 0x00B4,
0x00A8, 0x2260, 0x00C6, 0x00D8, 0x221E, 0x00B1, 0x2264, 0x2265, 0x00A5, 0x00B5, 0x2202,
0x2211, 0x220F, 0x03C0, 0x222B, 0x00AA, 0x00BA, 0x03A9, 0x00E6, 0x00F8, 0x00BF, 0x00A1,
0x00AC, 0x221A, 0x0192, 0x2248, 0x2206, 0x00AB, 0x00BB, 0x2026, 0x00A0, 0x00C0, 0x00C3,
0x00D5, 0x0152, 0x0153, 0x2013, 0x2014, 0x201C, 0x201D, 0x2018, 0x2019, 0x00F7, 0x25CA,
0x00FF, 0x0178, 0x2044, 0x20AC, 0x2039, 0x203A, 0xFB01, 0xFB02, 0x2021, 0x00B7, 0x201A,
0x201E, 0x2030, 0x00C2, 0x00CA, 0x00C1, 0x00CB, 0x00C8, 0x00CD, 0x00CE, 0x00CF, 0x00CC,
0x00D3, 0x00D4, 0xF8FF, 0x00D2, 0x00DA, 0x00DB, 0x00D9, 0x0131, 0x02C6, 0x02DC, 0x00AF,
0x02D8, 0x02D9, 0x02DA, 0x00B8, 0x02DD, 0x02DB, 0x02C7,
];
/// cmap chỉ có subtable OTS không hỗ trợ ((1,0) format 0/6 của subset PDF) →
/// dựng lại thành (3,1) format 4 theo Unicode. Đã có subtable Unicode/Windows
/// → None (giữ nguyên). Mã→Unicode: ưu tiên /ToUnicode; thiếu thì ASCII
/// identity (0x20–0x7E) + MacRoman (≥0x80); mã thấp không giải nghĩa được thì
/// bỏ — ký tự đó rơi xuống font kế trong stack CSS.
fn convert_cmap(cmap: &[u8], code_to_uni: Option<&HashMap<u32, u32>>) -> Option<Vec<u8>> {
if cmap.len() < 4 {
return None;
}
let n = u16::from_be_bytes(cmap[2..4].try_into().ok()?) as usize;
if cmap.len() < 4 + n * 8 {
return None;
}
let mut src: Option<(usize, u16)> = None;
for i in 0..n {
let o = 4 + i * 8;
let plat = u16::from_be_bytes(cmap[o..o + 2].try_into().ok()?);
let enc = u16::from_be_bytes(cmap[o + 2..o + 4].try_into().ok()?);
let off = u32::from_be_bytes(cmap[o + 4..o + 8].try_into().ok()?) as usize;
if off + 2 > cmap.len() {
return None;
}
let fmt = u16::from_be_bytes(cmap[off..off + 2].try_into().ok()?);
if plat == 0 || (plat == 3 && (enc == 1 || enc == 10)) {
return None; // đã có subtable Unicode — OTS chấp nhận
}
if plat == 1 && enc == 0 && (fmt == 0 || fmt == 6) {
src = Some((off, fmt));
}
}
let (off, fmt) = src?;
let mut pairs: Vec<(u32, u16)> = Vec::new();
let mut push = |code: u32, glyph: u16| {
if glyph == 0 {
return;
}
let uni = match code_to_uni.and_then(|m| m.get(&code)) {
Some(&u) => u,
None => {
if (0x20..=0x7E).contains(&code) {
code
} else if (0x80..=0xFF).contains(&code) {
u32::from(MACROMAN_HI[(code - 0x80) as usize])
} else {
return; // mã thấp không giải nghĩa được
}
}
};
if uni == 0 || uni > 0xFFFF {
return; // format 4 chỉ chứa BMP
}
pairs.push((uni, glyph));
};
if fmt == 0 {
if off + 6 + 256 > cmap.len() {
return None;
}
for c in 0..256u32 {
push(c, u16::from(cmap[off + 6 + c as usize]));
}
} else {
if off + 10 > cmap.len() {
return None;
}
let first = u32::from(u16::from_be_bytes(cmap[off + 6..off + 8].try_into().ok()?));
let cnt = u16::from_be_bytes(cmap[off + 8..off + 10].try_into().ok()?) as usize;
if off + 10 + cnt * 2 > cmap.len() {
return None;
}
for k in 0..cnt {
let g = u16::from_be_bytes(cmap[off + 10 + k * 2..off + 12 + k * 2].try_into().ok()?);
push(first + k as u32, g);
}
}
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))
}
/// cmap trọn vẹn: 1 subtable (3,1) format 4 từ các cặp (unicode, glyph) đã
/// sort + khử trùng. Cặp liên tiếp cả 2 chiều gộp thành 1 segment.
fn build_cmap_format4(pairs: &[(u32, u16)]) -> Vec<u8> {
struct Seg {
start: u32,
end: u32,
g_start: u16,
delta_fixed: Option<u16>,
}
let mut segs: Vec<Seg> = Vec::new();
for &(u, g) in pairs {
if let Some(last) = segs.last_mut() {
let next_g = last.g_start as u32 + (u - last.start);
if u == last.end + 1 && next_g <= 0xFFFF && next_g == u32::from(g) {
last.end = u;
continue;
}
}
segs.push(Seg { start: u, end: u, g_start: g, delta_fixed: None });
}
segs.push(Seg { start: 0xFFFF, end: 0xFFFF, g_start: 0, delta_fixed: Some(1) });
let n = segs.len();
let sub_len = 16 + n * 8;
let mut pow = 1usize;
let mut es = 0u16;
while pow * 2 <= n {
pow *= 2;
es += 1;
}
let mut sub = vec![0u8; sub_len];
let w16 = |b: &mut [u8], o: usize, v: u16| b[o..o + 2].copy_from_slice(&v.to_be_bytes());
w16(&mut sub, 0, 4);
w16(&mut sub, 2, sub_len as u16);
w16(&mut sub, 4, 0); // language
w16(&mut sub, 6, (n * 2) as u16);
w16(&mut sub, 8, (pow * 2) as u16);
w16(&mut sub, 10, es);
w16(&mut sub, 12, (n * 2 - pow * 2) as u16);
for (i, s) in segs.iter().enumerate() {
w16(&mut sub, 14 + i * 2, s.end as u16); // endCode
w16(&mut sub, 16 + n * 2 + i * 2, s.start as u16); // startCode (sau pad)
let delta = s
.delta_fixed
.unwrap_or_else(|| (u32::from(s.g_start).wrapping_sub(s.start) & 0xFFFF) as u16);
w16(&mut sub, 16 + n * 4 + i * 2, delta); // idDelta
w16(&mut sub, 16 + n * 6 + i * 2, 0); // idRangeOffset
}
let mut cmap = vec![0u8; 12 + sub_len];
w16(&mut cmap, 0, 0); // version
w16(&mut cmap, 2, 1); // numTables
w16(&mut cmap, 4, 3); // platformID Windows
w16(&mut cmap, 6, 1); // encodingID Unicode BMP
cmap[8..12].copy_from_slice(&12u32.to_be_bytes());
cmap[12..].copy_from_slice(&sub);
cmap
}
/// Tổng checksum kiểu sfnt: cộng u32 big-endian, phần dư pad 0.
fn checksum(data: &[u8]) -> u32 {
let mut sum = 0u32;
let mut chunks = data.chunks_exact(4);
for c in &mut chunks {
sum = sum.wrapping_add(u32::from_be_bytes(c.try_into().unwrap()));
}
let rem = chunks.remainder();
if !rem.is_empty() {
let mut last = [0u8; 4];
last[..rem.len()].copy_from_slice(rem);
sum = sum.wrapping_add(u32::from_be_bytes(last));
}
sum
}
fn assemble(orig_ver: u32, tables: Vec<([u8; 4], Vec<u8>)>) -> Vec<u8> {
// 'true' → 0x00010000 (OTS chấp nhận cả hai nhưng chuẩn hoá cho chắc);
// OTTO (CFF) giữ nguyên tag.
let ver = if orig_ver == TAG_OTTO { orig_ver } else { 0x0001_0000 };
let num = tables.len() as u16;
let mut pow = 1u16;
let mut entry_selector = 0u16;
while pow * 2 <= num {
pow *= 2;
entry_selector += 1;
}
let search_range = pow * 16;
let range_shift = num * 16 - search_range;
let mut out = Vec::new();
out.extend_from_slice(&ver.to_be_bytes());
out.extend_from_slice(&num.to_be_bytes());
out.extend_from_slice(&search_range.to_be_bytes());
out.extend_from_slice(&entry_selector.to_be_bytes());
out.extend_from_slice(&range_shift.to_be_bytes());
let mut off = 12 + tables.len() * 16;
let mut dir: Vec<u8> = Vec::new();
let mut bodies: Vec<u8> = Vec::new();
let mut head_off = None;
for (tag, mut body) in tables {
if &tag == b"head" && body.len() >= 12 {
// checkSumAdjustment phải = 0 khi tính checksum — điền lại ở cuối.
body[8..12].fill(0);
head_off = Some(off);
}
let cs = checksum(&body);
dir.extend_from_slice(&tag);
dir.extend_from_slice(&cs.to_be_bytes());
dir.extend_from_slice(&(off as u32).to_be_bytes());
dir.extend_from_slice(&(body.len() as u32).to_be_bytes());
let padded = (body.len() + 3) & !3;
off += padded;
body.resize(padded, 0);
bodies.extend_from_slice(&body);
}
out.extend_from_slice(&dir);
out.extend_from_slice(&bodies);
if let Some(h) = head_off {
let adj = 0xB1B0_AFBAu32.wrapping_sub(checksum(&out));
out[h + 8..h + 12].copy_from_slice(&adj.to_be_bytes());
}
out
}
/// OS/2 version 4 (96 byte) với số liệu suy từ head/hhea — đủ cho OTS và cho
/// trình duyệt chọn đúng mặt đậm/nghiêng, không ảnh hưởng shaping.
fn build_os2(tables: &[([u8; 4], Vec<u8>)], bold: bool, italic: bool) -> Option<Vec<u8>> {
let find = |tag: &[u8; 4]| tables.iter().find(|(t, _)| t == tag).map(|(_, b)| b.as_slice());
let head = find(b"head")?;
let upm = if head.len() >= 20 {
i32::from(u16::from_be_bytes(head[18..20].try_into().ok()?))
} else {
1000
};
let (asc, desc, gap) = match find(b"hhea") {
Some(h) if h.len() >= 10 => (
i16::from_be_bytes(h[4..6].try_into().ok()?),
i16::from_be_bytes(h[6..8].try_into().ok()?),
i16::from_be_bytes(h[8..10].try_into().ok()?),
),
_ => ((upm * 4 / 5) as i16, -((upm / 5) as i16), 0),
};
let mut v: Vec<u8> = Vec::with_capacity(96);
fn u(v: &mut Vec<u8>, x: u16) { v.extend_from_slice(&x.to_be_bytes()); }
fn i(v: &mut Vec<u8>, x: i16) { v.extend_from_slice(&x.to_be_bytes()); }
fn d(v: &mut Vec<u8>, x: u32) { v.extend_from_slice(&x.to_be_bytes()); }
u(&mut v, 4); // version
i(&mut v, (upm / 2) as i16); // xAvgCharWidth (xấp xỉ — không dùng để shaping)
u(&mut v, if bold { 700 } else { 400 }); // usWeightClass
u(&mut v, 5); // usWidthClass = medium
u(&mut v, 0); // fsType: không hạn chế nhúng
let sub = (upm * 65 / 100) as i16;
i(&mut v, sub); // ySubscriptXSize
i(&mut v, sub); // ySubscriptYSize
i(&mut v, 0); // ySubscriptXOffset
i(&mut v, (upm * 14 / 100) as i16); // ySubscriptYOffset
i(&mut v, sub); // ySuperscriptXSize
i(&mut v, sub); // ySuperscriptYSize
i(&mut v, 0); // ySuperscriptXOffset
i(&mut v, (upm * 48 / 100) as i16); // ySuperscriptYOffset
i(&mut v, (upm * 5 / 100) as i16); // yStrikeoutSize
i(&mut v, (upm * 26 / 100) as i16); // yStrikeoutPosition
i(&mut v, 0); // sFamilyClass
v.extend_from_slice(&[0u8; 10]); // panose: any
d(&mut v, 0); d(&mut v, 0); d(&mut v, 0); d(&mut v, 0); // ulUnicodeRange 1–4
v.extend_from_slice(b"FFXT"); // achVendID
let mut fs = 0u16;
if italic { fs |= 0x0001; }
if bold { fs |= 0x0020; }
if fs == 0 { fs = 0x0040; } // REGULAR
u(&mut v, fs); // fsSelection
u(&mut v, 0x0020); // usFirstCharIndex
u(&mut v, 0xFFFD); // usLastCharIndex
i(&mut v, asc); // sTypoAscender
i(&mut v, desc); // sTypoDescender
i(&mut v, gap); // sTypoLineGap
u(&mut v, asc.max(0) as u16); // usWinAscent
u(&mut v, (-(desc.min(0))) as u16); // usWinDescent
d(&mut v, 1); // ulCodePageRange1: Latin 1
d(&mut v, 0); // ulCodePageRange2
i(&mut v, (upm / 2) as i16); // sxHeight
i(&mut v, (upm * 7 / 10) as i16); // sCapHeight
u(&mut v, 0); // usDefaultChar
u(&mut v, 0x0020); // usBreakChar
u(&mut v, 0); // usMaxContext
debug_assert_eq!(v.len(), 96);
Some(v)
}
#[cfg(test)]
mod tests {
use super::*;
/// Sfnt giả tối thiểu: head (54B, upm=1000) + hhea (36B) + glyf giả,
/// KHÔNG có OS/2 — đúng hình dạng subset PDF gây lỗi OTS.
fn fake_subset() -> Vec<u8> {
let mut head = vec![0u8; 54];
head[18..20].copy_from_slice(&1000u16.to_be_bytes()); // unitsPerEm
let mut hhea = vec![0u8; 36];
hhea[4..6].copy_from_slice(&800i16.to_be_bytes()); // ascender
hhea[6..8].copy_from_slice(&(-200i16).to_be_bytes()); // descender
hhea[8..10].copy_from_slice(&90i16.to_be_bytes()); // lineGap
let glyf = vec![1u8, 2, 3, 4, 5]; // lẻ 4 byte để thử pad
let tables: Vec<([u8; 4], Vec<u8>)> =
vec![(*b"glyf", glyf), (*b"head", head), (*b"hhea", hhea)];
assemble(TAG_TRUE, tables)
}
fn table_dir(font: &[u8]) -> Vec<(String, usize, usize)> {
let num = u16::from_be_bytes(font[4..6].try_into().unwrap()) as usize;
(0..num)
.map(|i| {
let o = 12 + i * 16;
(
String::from_utf8_lossy(&font[o..o + 4]).to_string(),
u32::from_be_bytes(font[o + 8..o + 12].try_into().unwrap()) as usize,
u32::from_be_bytes(font[o + 12..o + 16].try_into().unwrap()) as usize,
)
})
.collect()
}
#[test]
fn missing_os2_is_synthesized_with_valid_checksums() {
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.
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:?}");
// OS/2 đọc lại đúng nội dung suy ra.
let (_, off, len) = dir[0].clone();
let os2 = &fixed[off..off + len];
assert_eq!(len, 96);
assert_eq!(u16::from_be_bytes(os2[4..6].try_into().unwrap()), 700, "usWeightClass bold");
assert_eq!(u16::from_be_bytes(os2[62..64].try_into().unwrap()), 0x0020, "fsSelection BOLD");
assert_eq!(
i16::from_be_bytes(os2[68..70].try_into().unwrap()),
800,
"sTypoAscender từ hhea"
);
// Checksum toàn font phải ra hằng số ma thuật khi head.adjustment đã điền.
assert_eq!(checksum(&fixed), 0xB1B0_AFBA, "checkSumAdjustment chuẩn");
// Vá lần 2 phải là no-op (đã có OS/2 + tag chuẩn, không có cmap).
assert_eq!(web_compatible_font(&fixed, true, false, None), fixed);
}
#[test]
fn non_sfnt_data_passes_through() {
let cff = vec![0x01, 0x00, 0x04, 0x04, 0xAA, 0xBB];
assert_eq!(web_compatible_font(&cff, false, false, None), cff);
}
/// cmap (1,0) format 0 kiểu subset LibreOffice: mã ASCII giữ nguyên, mã
/// thấp (1) là chữ có dấu chỉ /ToUnicode giải nghĩa được.
fn fake_subset_with_cmap() -> Vec<u8> {
let mut sub = vec![0u8; 6 + 256];
sub[0..2].copy_from_slice(&0u16.to_be_bytes()); // format 0
sub[2..4].copy_from_slice(&262u16.to_be_bytes());
sub[6 + 0x41] = 5; // 'A' → glyph 5
sub[6 + 0x42] = 6; // 'B' → glyph 6 (liên tiếp → gộp segment)
sub[6 + 1] = 7; // mã 1 → glyph 7 (chữ 'ả' qua ToUnicode)
let mut cmap = vec![0u8; 12];
cmap[2..4].copy_from_slice(&1u16.to_be_bytes()); // numTables
cmap[4..6].copy_from_slice(&1u16.to_be_bytes()); // platform Mac
cmap[6..8].copy_from_slice(&0u16.to_be_bytes()); // encoding 0
cmap[8..12].copy_from_slice(&12u32.to_be_bytes());
cmap.extend_from_slice(&sub);
let mut head = vec![0u8; 54];
head[18..20].copy_from_slice(&1000u16.to_be_bytes());
let tables: Vec<([u8; 4], Vec<u8>)> = vec![(*b"cmap", cmap), (*b"head", head)];
assemble(TAG_TRUE, tables)
}
/// Tra format 4: quét segment tuyến tính.
fn lookup_f4(cmap: &[u8], uni: u16) -> u16 {
assert_eq!(u16::from_be_bytes(cmap[4..6].try_into().unwrap()), 3, "platform Windows");
assert_eq!(u16::from_be_bytes(cmap[6..8].try_into().unwrap()), 1, "encoding BMP");
let off = u32::from_be_bytes(cmap[8..12].try_into().unwrap()) as usize;
assert_eq!(u16::from_be_bytes(cmap[off..off + 2].try_into().unwrap()), 4, "format 4");
let n = (u16::from_be_bytes(cmap[off + 6..off + 8].try_into().unwrap()) / 2) as usize;
for i in 0..n {
let end = u16::from_be_bytes(cmap[off + 14 + i * 2..off + 16 + i * 2].try_into().unwrap());
let start =
u16::from_be_bytes(cmap[off + 16 + n * 2 + i * 2..off + 18 + n * 2 + i * 2].try_into().unwrap());
if uni >= start && uni <= end {
let delta =
u16::from_be_bytes(cmap[off + 16 + n * 4 + i * 2..off + 18 + n * 4 + i * 2].try_into().unwrap());
return uni.wrapping_add(delta);
}
}
0
}
#[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));
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;
let mut cm = None;
for i in 0..num {
let o = 12 + i * 16;
if &fixed[o..o + 4] == b"cmap" {
let off = u32::from_be_bytes(fixed[o + 8..o + 12].try_into().unwrap()) as usize;
let len = u32::from_be_bytes(fixed[o + 12..o + 16].try_into().unwrap()) as usize;
cm = Some(fixed[off..off + len].to_vec());
}
}
let cm = cm.expect("có cmap");
assert_eq!(lookup_f4(&cm, 0x41), 5, "'A' giữ glyph cũ");
assert_eq!(lookup_f4(&cm, 0x42), 6, "'B' gộp segment vẫn đúng");
assert_eq!(lookup_f4(&cm, 0x1EA3), 7, "'ả' theo ToUnicode");
assert_eq!(lookup_f4(&cm, 0x43), 0, "mã không map → notdef");
// OS/2 cũng được thêm (fake subset không có).
let tags: Vec<String> = (0..u16::from_be_bytes(fixed[4..6].try_into().unwrap()) as usize)
.map(|i| String::from_utf8_lossy(&fixed[12 + i * 16..16 + i * 16]).to_string())
.collect();
assert!(tags.contains(&"OS/2".to_string()), "tags: {tags:?}");
}
}