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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 16:55:06 +07:00
co-authored by Claude Fable 5
parent fd90a124dc
commit 57b8b84f7d
4 changed files with 551 additions and 2 deletions
+298
View File
@@ -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
}
+164 -1
View File
@@ -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);
}
}
+1
View File
@@ -7,6 +7,7 @@
pub mod annot;
pub mod convert;
pub mod edit;
mod cffwrap;
mod fontfix;
mod formsurgery;
mod tounicode;
+88 -1
View File
@@ -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),