From 57b8b84f7ddaf58c844ed49c1034616d253d7f25 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 16:55:06 +0700 Subject: [PATCH] Dot 2: CFF/Type1C tran boc thanh OTF (cffwrap parser toi thieu: CharStrings/Encoding/charset/FontMatrix; hmtx tu /Widths, asc/desc tu FontDescriptor); tounicode thu widths/DW/ascent/descent; goi CJK tuy chon (fetch-fonts-cjk.ps1 + ff-app-cjk lazy theo noi dung) Co-Authored-By: Claude Fable 5 --- crates/ff-engine/src/cffwrap.rs | 298 ++++++++++++++++++++++++++++++ crates/ff-engine/src/fontfix.rs | 165 ++++++++++++++++- crates/ff-engine/src/lib.rs | 1 + crates/ff-engine/src/tounicode.rs | 89 ++++++++- 4 files changed, 551 insertions(+), 2 deletions(-) create mode 100644 crates/ff-engine/src/cffwrap.rs diff --git a/crates/ff-engine/src/cffwrap.rs b/crates/ff-engine/src/cffwrap.rs new file mode 100644 index 0000000..132a4a6 --- /dev/null +++ b/crates/ff-engine/src/cffwrap.rs @@ -0,0 +1,298 @@ +//! Parser CFF (Compact Font Format) TỐI THIỂU — phục vụ bọc font Type1C trần +//! (FontFile3 trong PDF, không có vỏ sfnt) thành OTF cho FontFace của WebView. +//! +//! Chỉ đọc những gì cần cho vỏ OTF: số glyph (CharStrings INDEX), unitsPerEm +//! (FontMatrix), mã→GID (Encoding tuỳ biến của font đơn giản) hoặc CID→GID +//! (charset của font CID). KHÔNG diễn giải charstring — glyph outline giữ +//! nguyên trong bảng `CFF ` của OTF, trình duyệt tự vẽ. + +use std::collections::HashMap; + +pub(crate) struct CffInfo { + pub num_glyphs: u16, + pub upm: f32, + pub is_cid: bool, + /// Font đơn giản: mã byte → GID (từ Encoding tuỳ biến format 0/1). + pub code_to_gid: HashMap, + /// Font CID: CID → GID (nghịch đảo charset). + pub cid_to_gid: HashMap, +} + +/// Đọc 1 INDEX tại `pos`: trả (vị trí kết thúc, các item). CFF INDEX: count +/// u16; count>0: offSize u8 + (count+1) offset + data (offset tính từ 1). +fn read_index(data: &[u8], pos: usize) -> Option<(usize, Vec<&[u8]>)> { + let count = u16::from_be_bytes(data.get(pos..pos + 2)?.try_into().ok()?) as usize; + if count == 0 { + return Some((pos + 2, Vec::new())); + } + let off_size = *data.get(pos + 2)? as usize; + if !(1..=4).contains(&off_size) { + return None; + } + let offs_start = pos + 3; + let read_off = |i: usize| -> Option { + let s = offs_start + i * off_size; + let b = data.get(s..s + off_size)?; + Some(b.iter().fold(0usize, |a, &x| (a << 8) | x as usize)) + }; + let data_start = offs_start + (count + 1) * off_size - 1; + let mut items = Vec::with_capacity(count); + for i in 0..count { + let a = data_start + read_off(i)?; + let b = data_start + read_off(i + 1)?; + if a > b || b > data.len() { + return None; + } + items.push(&data[a..b]); + } + Some((data_start + read_off(count)?, items)) +} + +/// DICT CFF → map operator → operands. Escape (12 xx) mã hoá 0x0c00|xx. +fn parse_dict(d: &[u8]) -> HashMap> { + let mut out = HashMap::new(); + let mut operands: Vec = Vec::new(); + let mut i = 0; + while i < d.len() { + let b0 = d[i]; + match b0 { + 32..=246 => { + operands.push(f64::from(b0) - 139.0); + i += 1; + } + 247..=250 => { + let b1 = f64::from(d.get(i + 1).copied().unwrap_or(0)); + operands.push((f64::from(b0) - 247.0) * 256.0 + b1 + 108.0); + i += 2; + } + 251..=254 => { + let b1 = f64::from(d.get(i + 1).copied().unwrap_or(0)); + operands.push(-(f64::from(b0) - 251.0) * 256.0 - b1 - 108.0); + i += 2; + } + 28 => { + let v = d + .get(i + 1..i + 3) + .map(|b| i16::from_be_bytes([b[0], b[1]])) + .unwrap_or(0); + operands.push(f64::from(v)); + i += 3; + } + 29 => { + let v = d + .get(i + 1..i + 5) + .map(|b| i32::from_be_bytes([b[0], b[1], b[2], b[3]])) + .unwrap_or(0); + operands.push(f64::from(v)); + i += 5; + } + 30 => { + // Real: nibble packed, kết thúc 0xf. + let mut s = String::new(); + let mut j = i + 1; + 'outer: while j < d.len() { + for nib in [d[j] >> 4, d[j] & 0x0f] { + match nib { + 0..=9 => s.push(char::from(b'0' + nib)), + 0xa => s.push('.'), + 0xb => s.push('E'), + 0xc => s.push_str("E-"), + 0xe => s.push('-'), + 0xf => { + j += 1; + break 'outer; + } + _ => {} + } + } + j += 1; + } + operands.push(s.parse::().unwrap_or(0.0)); + i = j; + } + 12 => { + let op = 0x0c00 | u16::from(d.get(i + 1).copied().unwrap_or(0)); + out.insert(op, std::mem::take(&mut operands)); + i += 2; + } + 0..=21 => { + out.insert(u16::from(b0), std::mem::take(&mut operands)); + i += 1; + } + _ => break, + } + } + out +} + +pub(crate) fn parse(data: &[u8]) -> Option { + let hdr_size = *data.get(2)? as usize; + if data.first() != Some(&1) || hdr_size < 4 { + return None; + } + let (p1, _names) = read_index(data, hdr_size)?; + let (p2, tops) = read_index(data, p1)?; + let _ = read_index(data, p2)?; // String INDEX — không cần nội dung + let top = parse_dict(tops.first()?); + + let charstrings_off = *top.get(&17)?.first()? as usize; + let (_, charstrings) = read_index(data, charstrings_off)?; + let num_glyphs = u16::try_from(charstrings.len()).ok()?; + if num_glyphs == 0 { + return None; + } + let is_cid = top.contains_key(&0x0c1e); // ROS + + // unitsPerEm từ FontMatrix (mặc định 0.001 → 1000). + let upm = top + .get(&0x0c07) + .and_then(|m| m.first().copied()) + .filter(|&a| a > 1e-9) + .map(|a| (1.0 / a) as f32) + .unwrap_or(1000.0); + + let mut code_to_gid = HashMap::new(); + if !is_cid { + // Encoding: 0/1 = Standard/Expert (định nghĩa theo TÊN glyph — cần + // charset+String INDEX, ngoài phạm vi v1); offset = tuỳ biến fmt 0/1. + let enc = top.get(&16).and_then(|v| v.first().copied()).unwrap_or(0.0) as usize; + if enc > 1 && enc < data.len() { + let fmt = data[enc] & 0x7f; + if fmt == 0 { + let n = *data.get(enc + 1)? as usize; + for k in 0..n { + let code = *data.get(enc + 2 + k)? as u32; + code_to_gid.insert(code, k as u16 + 1); + } + } else if fmt == 1 { + let n_ranges = *data.get(enc + 1)? as usize; + let mut gid: u16 = 1; + for r in 0..n_ranges { + let first = *data.get(enc + 2 + r * 2)? as u32; + let n_left = *data.get(enc + 3 + r * 2)? as u32; + for k in 0..=n_left { + if gid >= num_glyphs { + break; + } + code_to_gid.insert(first + k, gid); + gid += 1; + } + } + } + } + } + + let mut cid_to_gid = HashMap::new(); + if is_cid { + // charset (op 15): gid → CID; nghịch đảo lại. 0/1/2 = bảng dựng sẵn + // (ISOAdobe... — CID font thực tế luôn dùng offset tuỳ biến). + let cs = top.get(&15).and_then(|v| v.first().copied()).unwrap_or(0.0) as usize; + cid_to_gid.insert(0, 0); + if cs > 2 && cs < data.len() { + let fmt = data[cs]; + match fmt { + 0 => { + for gid in 1..num_glyphs { + let o = cs + 1 + (gid as usize - 1) * 2; + let cid = + u16::from_be_bytes(data.get(o..o + 2)?.try_into().ok()?) as u32; + cid_to_gid.insert(cid, gid); + } + } + 1 | 2 => { + let step = if fmt == 1 { 3 } else { 4 }; + let mut gid: u16 = 1; + let mut o = cs + 1; + while gid < num_glyphs { + let first = + u16::from_be_bytes(data.get(o..o + 2)?.try_into().ok()?) as u32; + let n_left = if fmt == 1 { + u32::from(*data.get(o + 2)?) + } else { + u32::from(u16::from_be_bytes(data.get(o + 2..o + 4)?.try_into().ok()?)) + }; + for k in 0..=n_left { + if gid >= num_glyphs { + break; + } + cid_to_gid.insert(first + k, gid); + gid += 1; + } + o += step; + } + } + _ => {} + } + } else if cs == 0 { + // charset 0 với CID = Identity (hiếm): CID = GID. + for gid in 0..num_glyphs { + cid_to_gid.insert(u32::from(gid), gid); + } + } + } + + Some(CffInfo { num_glyphs, upm, is_cid, code_to_gid, cid_to_gid }) +} + +/// CFF giả tối thiểu cho test: 3 glyph, encoding format 0 (mã 65 'A' → gid 1, +/// mã 66 'B' → gid 2), FontMatrix mặc định. Dùng chung với test fontfix. +#[cfg(test)] +pub(crate) fn fake_cff() -> Vec { + build_fake_cff() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_simple_cff_encoding_and_glyph_count() { + let cff = fake_cff(); + let info = parse(&cff).expect("parse CFF"); + assert_eq!(info.num_glyphs, 3); + assert!(!info.is_cid); + assert_eq!(info.upm, 1000.0); + assert_eq!(info.code_to_gid.get(&65), Some(&1), "'A' → gid 1"); + assert_eq!(info.code_to_gid.get(&66), Some(&2), "'B' → gid 2"); + } +} + +#[cfg(test)] +fn build_fake_cff() -> Vec { + let mut v: Vec = Vec::new(); + v.extend_from_slice(&[1, 0, 4, 1]); // header: major minor hdrSize offSize + // Name INDEX: 1 item "T" + v.extend_from_slice(&[0, 1, 1, 1, 2]); // count=1 offSize=1 offs [1,2] + v.push(b'T'); + // Top DICT INDEX: 1 item (điền sau khi biết offset các phần) + // DICT: charstrings offset (op 17) + encoding offset (op 16). + // Offset tuyệt đối — tính trước bằng cách dựng phần đuôi trước. + // Layout sau Top DICT INDEX: String INDEX (rỗng, 2B) + GSubr INDEX + // (rỗng, 2B) + encoding + charstrings. + // Top DICT nội dung: "29 17 29 16" = 12B. + let dict_len = 12usize; + let top_index_len = 2 + 1 + 2 + dict_len; // count offSize offs[2] data + let after_top = v.len() + top_index_len; + let enc_off = after_top + 2 + 2; // sau String INDEX + GSubr INDEX + let enc_len = 2 + 2; // format0, nCodes=2, codes[65,66] + let cs_off = enc_off + enc_len; + // Top DICT INDEX header + v.extend_from_slice(&[0, 1, 1, 1, (1 + dict_len) as u8]); + // DICT bytes + v.push(29); + v.extend_from_slice(&(cs_off as i32).to_be_bytes()); + v.push(17); + v.push(29); + v.extend_from_slice(&(enc_off as i32).to_be_bytes()); + v.push(16); + assert_eq!(v.len(), after_top); + v.extend_from_slice(&[0, 0]); // String INDEX rỗng + v.extend_from_slice(&[0, 0]); // GSubr INDEX rỗng + // Encoding format 0 + assert_eq!(v.len(), enc_off); + v.extend_from_slice(&[0, 2, 65, 66]); + // CharStrings INDEX: 3 glyph 1 byte mỗi cái (nội dung không quan trọng) + assert_eq!(v.len(), cs_off); + v.extend_from_slice(&[0, 3, 1, 1, 2, 3, 4, 0x0e, 0x0e, 0x0e]); + v +} diff --git a/crates/ff-engine/src/fontfix.rs b/crates/ff-engine/src/fontfix.rs index 6942096..e1f22b1 100644 --- a/crates/ff-engine/src/fontfix.rs +++ b/crates/ff-engine/src/fontfix.rs @@ -28,9 +28,125 @@ pub(crate) fn web_compatible_font( italic: bool, mapping: Option<&FontMapping>, ) -> Vec { + // CFF trần (Type1C/FontFile3 — không có vỏ sfnt, header major=1): bọc + // thành OTF để FontFace nạp được. Không bọc nổi thì trả nguyên trạng + // (UI rơi về font đóng gói). + if data.len() >= 4 && data[0] == 1 && data[1] == 0 { + return wrap_bare_cff(data, bold, italic, mapping).unwrap_or_else(|| data.to_vec()); + } try_fix(data, bold, italic, mapping).unwrap_or_else(|| data.to_vec()) } +/// Bọc CFF trần thành OTF (tag OTTO): bảng `CFF ` giữ nguyên bytes gốc, các +/// bảng sfnt bắt buộc tổng hợp từ CFF (số glyph, encoding/charset, upm) + +/// PDF (/Widths, /Ascent, /ToUnicode). hmtx đúng bề rộng gốc → chữ trong ô +/// sửa chiếm chỗ y như trên trang. +fn wrap_bare_cff( + data: &[u8], + bold: bool, + italic: bool, + mapping: Option<&FontMapping>, +) -> Option> { + let m = mapping?; + if m.code_to_uni.is_empty() { + return None; + } + let cff = crate::cffwrap::parse(data)?; + let upm_f = cff.upm.clamp(16.0, 16384.0); + let upm = upm_f.round() as u16; + let to_units = |w: f32| (w * upm_f / 1000.0).round().clamp(0.0, 65535.0) as u16; + let code_gid = |code: u32| -> Option { + let g = if cff.is_cid { + cff.cid_to_gid.get(&code).copied() + } else { + cff.code_to_gid.get(&code).copied() + }?; + (g != 0 && g < cff.num_glyphs).then_some(g) + }; + let default_w = to_units(if m.default_width > 0.0 { m.default_width } else { 500.0 }); + let mut advances = vec![default_w; cff.num_glyphs as usize]; + let mut pairs: Vec<(u32, u16)> = Vec::new(); + for (&code, &uni) in &m.code_to_uni { + let Some(gid) = code_gid(code) else { continue }; + if uni != 0 && uni <= 0xFFFF { + pairs.push((uni, gid)); + } + if let Some(&w) = m.widths.get(&code) { + advances[gid as usize] = to_units(w); + } + } + if pairs.is_empty() { + return None; // không map được glyph nào — bọc cũng vô dụng + } + pairs.sort_by_key(|p| p.0); + pairs.dedup_by_key(|p| p.0); + + let asc = if m.ascent > 1.0 { (m.ascent * upm_f / 1000.0) as i16 } else { (upm_f * 0.8) as i16 }; + let desc = + if m.descent < -1.0 { (m.descent * upm_f / 1000.0) as i16 } else { -((upm_f * 0.2) as i16) }; + let adv_max = advances.iter().copied().max().unwrap_or(default_w); + + let mut tables: Vec<([u8; 4], Vec)> = vec![ + (*b"CFF ", data.to_vec()), + (*b"cmap", build_cmap_format4(&pairs)), + (*b"head", build_head(upm, desc, adv_max as i16, asc)), + (*b"hhea", build_hhea(asc, desc, adv_max, cff.num_glyphs)), + (*b"hmtx", build_hmtx(&advances)), + (*b"maxp", build_maxp05(cff.num_glyphs)), + ]; + let os2 = build_os2(&tables, bold, italic)?; + tables.push((*b"OS/2", os2)); + tables.push((*b"name", build_name())); + tables.push((*b"post", build_post())); + tables.sort_by(|a, b| a.0.cmp(&b.0)); + Some(assemble(TAG_OTTO, tables)) +} + +/// head 54B cho vỏ OTF (magic 0x5F0F3CF5 bắt buộc; checkSumAdjustment do +/// assemble điền). +fn build_head(upm: u16, y_min: i16, x_max: i16, y_max: i16) -> Vec { + let mut v = vec![0u8; 54]; + v[0..4].copy_from_slice(&0x0001_0000u32.to_be_bytes()); // version + v[4..8].copy_from_slice(&0x0001_0000u32.to_be_bytes()); // fontRevision + v[12..16].copy_from_slice(&0x5F0F_3CF5u32.to_be_bytes()); // magicNumber + v[18..20].copy_from_slice(&upm.to_be_bytes()); + v[38..40].copy_from_slice(&y_min.to_be_bytes()); + v[40..42].copy_from_slice(&x_max.to_be_bytes()); + v[42..44].copy_from_slice(&y_max.to_be_bytes()); + v[46..48].copy_from_slice(&8u16.to_be_bytes()); // lowestRecPPEM + v[48..50].copy_from_slice(&2i16.to_be_bytes()); // fontDirectionHint + v +} + +fn build_hhea(asc: i16, desc: i16, adv_max: u16, num_h_metrics: u16) -> Vec { + let mut v = vec![0u8; 36]; + v[0..4].copy_from_slice(&0x0001_0000u32.to_be_bytes()); + v[4..6].copy_from_slice(&asc.to_be_bytes()); + v[6..8].copy_from_slice(&desc.to_be_bytes()); + v[10..12].copy_from_slice(&adv_max.to_be_bytes()); + v[18..20].copy_from_slice(&1i16.to_be_bytes()); // caretSlopeRise + v[34..36].copy_from_slice(&num_h_metrics.to_be_bytes()); + v +} + +/// hmtx đầy đủ: mỗi glyph advance(u16) + lsb(i16=0). +fn build_hmtx(advances: &[u16]) -> Vec { + let mut v = Vec::with_capacity(advances.len() * 4); + for &a in advances { + v.extend_from_slice(&a.to_be_bytes()); + v.extend_from_slice(&0i16.to_be_bytes()); + } + v +} + +/// maxp version 0.5 (6B) — chuẩn cho font CFF (không có glyf/loca). +fn build_maxp05(num_glyphs: u16) -> Vec { + let mut v = Vec::with_capacity(6); + v.extend_from_slice(&0x0000_5000u32.to_be_bytes()); + v.extend_from_slice(&num_glyphs.to_be_bytes()); + v +} + const TAG_TRUE: u32 = u32::from_be_bytes(*b"true"); const TAG_OTTO: u32 = u32::from_be_bytes(*b"OTTO"); @@ -624,7 +740,15 @@ mod tests { } fn mapping(uni: HashMap, is_cid: bool, cid_to_gid: Option>) -> FontMapping { - FontMapping { code_to_uni: uni, is_cid, cid_to_gid } + FontMapping { + code_to_uni: uni, + is_cid, + cid_to_gid, + widths: HashMap::new(), + default_width: 1000.0, + ascent: 0.0, + descent: 0.0, + } } #[test] @@ -708,4 +832,43 @@ mod tests { assert_eq!(lookup_f4(&cm2, 0x1EA3), 7, "'ả' → GID 7 theo CIDToGIDMap"); assert_eq!(lookup_f4(&cm2, 0x41), 8); } + + /// CFF trần (Type1C): bọc thành OTF — bảng CFF giữ nguyên bytes, cmap từ + /// Encoding × ToUnicode, hmtx đúng /Widths của PDF. + #[test] + fn bare_cff_is_wrapped_into_otf() { + let cff = crate::cffwrap::fake_cff(); + let mut uni = HashMap::new(); + uni.insert(65u32, 65u32); // 'A' giữ mã + uni.insert(66u32, 0x1EA3u32); // mã 66 = 'ả' qua ToUnicode + let mut m = mapping(uni, false, None); + m.widths.insert(65, 600.0); + m.widths.insert(66, 480.0); + m.ascent = 800.0; + m.descent = -200.0; + let out = web_compatible_font(&cff, false, false, Some(&m)); + assert_ne!(out, cff, "phải được bọc"); + assert_eq!(&out[0..4], b"OTTO", "vỏ OTF cho CFF"); + let dir = table_dir(&out); + let tags: Vec<&str> = dir.iter().map(|(t, _, _)| t.as_str()).collect(); + for need in ["CFF ", "OS/2", "cmap", "head", "hhea", "hmtx", "maxp", "name", "post"] { + assert!(tags.contains(&need), "thiếu {need}: {tags:?}"); + } + let (_, off, len) = dir[tags.iter().position(|t| *t == "cmap").unwrap()].clone(); + let cm = out[off..off + len].to_vec(); + assert_eq!(lookup_f4(&cm, 65), 1, "'A' → gid 1 theo Encoding"); + assert_eq!(lookup_f4(&cm, 0x1EA3), 2, "'ả' → gid 2"); + let (_, ho, _) = dir[tags.iter().position(|t| *t == "hmtx").unwrap()].clone(); + assert_eq!( + u16::from_be_bytes(out[ho + 4..ho + 6].try_into().unwrap()), + 600, + "advance gid 1 theo /Widths" + ); + assert_eq!(u16::from_be_bytes(out[ho + 8..ho + 10].try_into().unwrap()), 480); + let (_, co, cl) = dir[tags.iter().position(|t| *t == "CFF ").unwrap()].clone(); + assert_eq!(&out[co..co + cl], &cff[..], "bảng CFF giữ nguyên bytes gốc"); + assert_eq!(checksum(&out), 0xB1B0_AFBA); + // Không có mapping → không bọc nổi, trả nguyên trạng (UI tự fallback). + assert_eq!(web_compatible_font(&cff, false, false, None), cff); + } } diff --git a/crates/ff-engine/src/lib.rs b/crates/ff-engine/src/lib.rs index 3fe6f51..cb745c6 100644 --- a/crates/ff-engine/src/lib.rs +++ b/crates/ff-engine/src/lib.rs @@ -7,6 +7,7 @@ pub mod annot; pub mod convert; pub mod edit; +mod cffwrap; mod fontfix; mod formsurgery; mod tounicode; diff --git a/crates/ff-engine/src/tounicode.rs b/crates/ff-engine/src/tounicode.rs index 7e662aa..f6a95ca 100644 --- a/crates/ff-engine/src/tounicode.rs +++ b/crates/ff-engine/src/tounicode.rs @@ -92,6 +92,22 @@ fn scan_resources( .unwrap_or_default(); let is_cid = subtype == b"Type0"; let mut cid_to_gid = None; + let mut widths: HashMap = HashMap::new(); + let mut default_width = 1000.0f32; + let mut ascent = 0.0f32; + let mut descent = 0.0f32; + let mut read_descriptor = |d: &Dictionary, asc: &mut f32, desc: &mut f32| { + if let Ok(x) = d.get(b"FontDescriptor") { + if let Ok(x) = deref(doc, x).as_dict() { + if let Some(v) = x.get(b"Ascent").ok().and_then(|o| as_num(doc, o)) { + *asc = v; + } + if let Some(v) = x.get(b"Descent").ok().and_then(|o| as_num(doc, o)) { + *desc = v; + } + } + } + }; if is_cid { if let Ok(desc) = fd.get(b"DescendantFonts") { let desc = deref(doc, desc); @@ -112,13 +128,46 @@ fn scan_resources( ); } } + // /W: [c [w..] | c1 c2 w] ; /DW: bề rộng mặc định. + if let Ok(w) = dd.get(b"W") { + if let Object::Array(arr) = deref(doc, w) { + parse_cid_widths(doc, arr, &mut widths); + } + } + if let Some(v) = dd.get(b"DW").ok().and_then(|o| as_num(doc, o)) { + default_width = v; + } + read_descriptor(dd, &mut ascent, &mut descent); } } + } else { + // Font đơn giản: /FirstChar + /Widths. + if let (Some(fc), Ok(ws)) = ( + fd.get(b"FirstChar").ok().and_then(|o| as_num(doc, o)), + fd.get(b"Widths"), + ) { + if let Object::Array(arr) = deref(doc, ws) { + for (i, w) in arr.iter().enumerate() { + if let Some(w) = as_num(doc, w) { + widths.insert(fc as u32 + i as u32, w); + } + } + } + } + read_descriptor(fd, &mut ascent, &mut descent); } if code_to_uni.is_empty() { continue; // không có ToUnicode — thử font trùng tên khác } - return Some(FontMapping { code_to_uni, is_cid, cid_to_gid }); + return Some(FontMapping { + code_to_uni, + is_cid, + cid_to_gid, + widths, + default_width, + ascent, + descent, + }); } } } @@ -141,6 +190,44 @@ fn scan_resources( None } +/// Số PDF (Integer/Real, kể cả qua reference) → f32. +fn as_num(doc: &LoDoc, o: &Object) -> Option { + match deref(doc, o) { + Object::Integer(i) => Some(*i as f32), + Object::Real(r) => Some(*r), + _ => None, + } +} + +/// Mảng /W của font CID: dạng `c [w1 w2 …]` (mã c..c+n-1) hoặc `c1 c2 w`. +fn parse_cid_widths(doc: &LoDoc, arr: &[Object], out: &mut HashMap) { + let mut i = 0; + while i < arr.len() { + let Some(c1) = as_num(doc, &arr[i]) else { break }; + match arr.get(i + 1).map(|o| deref(doc, o)) { + Some(Object::Array(list)) => { + for (k, w) in list.iter().enumerate() { + if let Some(w) = as_num(doc, w) { + out.insert(c1 as u32 + k as u32, w); + } + } + i += 2; + } + Some(_) => { + let c2 = arr.get(i + 1).and_then(|o| as_num(doc, o)); + let w = arr.get(i + 2).and_then(|o| as_num(doc, o)); + let (Some(c2), Some(w)) = (c2, w) else { break }; + let hi = (c2 as u32).min(c1 as u32 + 0xFFFF); + for c in (c1 as u32)..=hi { + out.insert(c, w); + } + i += 3; + } + None => break, + } + } +} + enum Tok { Hex(Vec), Kw(String),