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)
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:
@@ -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<u32, u16>,
|
||||
/// Font CID: CID → GID (nghịch đảo charset).
|
||||
pub cid_to_gid: HashMap<u32, u16>,
|
||||
}
|
||||
|
||||
/// Đọ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<usize> {
|
||||
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<u16, Vec<f64>> {
|
||||
let mut out = HashMap::new();
|
||||
let mut operands: Vec<f64> = 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::<f64>().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<CffInfo> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
let mut v: Vec<u8> = 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 <i32 cs_off> 17 29 <i32 enc_off> 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
|
||||
}
|
||||
@@ -28,9 +28,125 @@ pub(crate) fn web_compatible_font(
|
||||
italic: bool,
|
||||
mapping: Option<&FontMapping>,
|
||||
) -> Vec<u8> {
|
||||
// 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<Vec<u8>> {
|
||||
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<u16> {
|
||||
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<u8>)> = 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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<u32, u32>, is_cid: bool, cid_to_gid: Option<Vec<u16>>) -> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
pub mod annot;
|
||||
pub mod convert;
|
||||
pub mod edit;
|
||||
mod cffwrap;
|
||||
mod fontfix;
|
||||
mod formsurgery;
|
||||
mod tounicode;
|
||||
|
||||
@@ -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<u32, f32> = 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<f32> {
|
||||
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<u32, f32>) {
|
||||
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<u8>),
|
||||
Kw(String),
|
||||
|
||||
Reference in New Issue
Block a user