Files
FoFreeXit/crates/ff-engine/src/tounicode.rs
T

404 lines
15 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.
//! Đọc /ToUnicode của font trong PDF (lopdf): map MÃ trong content stream →
//! Unicode. Dùng cho fontfix: subset kiểu LibreOffice gán các mã thấp 1–31
//! cho ký tự ngoài WinAnsi (chữ Việt có dấu…) — chỉ /ToUnicode giải nghĩa
//! được các mã đó; ASCII giữ nguyên mã nên không cần.
use std::collections::HashMap;
use std::path::Path;
use lopdf::{Dictionary, Document as LoDoc, Object};
use crate::formsurgery::{deref, stream_bytes};
/// Thông tin giải mã của 1 font trong PDF — đủ để fontfix dựng cmap Unicode
/// và (với CFF trần) cả hmtx/OS-2 khi bọc thành OTF.
pub(crate) struct FontMapping {
/// Mã trong content stream → Unicode (từ /ToUnicode).
pub code_to_uni: HashMap<u32, u32>,
/// Font CID (Type0/Identity-H): mã = CID. `cid_to_gid`: None = Identity
/// (GID = CID — trường hợp phổ biến của Word/InDesign), Some = bảng tra
/// từ stream /CIDToGIDMap (2 byte big-endian mỗi CID).
pub is_cid: bool,
pub cid_to_gid: Option<Vec<u16>>,
/// Mã → bề rộng advance (đơn vị text space ×1000): /Widths (font đơn
/// giản) hoặc /W (font CID).
pub widths: HashMap<u32, f32>,
/// /DW của font CID (mặc định 1000) — bề rộng cho mã không có trong /W.
pub default_width: f32,
/// /Ascent /Descent từ FontDescriptor (0 = không rõ).
pub ascent: f32,
pub descent: f32,
}
/// Mapping của font có BaseFont khớp `font_name` (so phần sau prefix subset
/// "ABCDEF+") trên trang `page_index`; tìm cả trong resources của Form
/// XObject (sâu tối đa 3). Không thấy/không parse được → None (fontfix rơi về
/// ASCII identity + MacRoman).
pub(crate) fn font_mapping(
input: &Path,
page_index: u16,
font_name: &str,
) -> Option<FontMapping> {
let want = font_name.rsplit('+').next().unwrap_or(font_name).trim();
if want.is_empty() {
return None;
}
let doc = LoDoc::load(input).ok()?;
let pages = doc.get_pages();
let page_id = *pages.get(&(u32::from(page_index) + 1))?;
let page = doc.get_dictionary(page_id).ok()?;
let res = deref(&doc, page.get(b"Resources").ok()?).as_dict().ok()?;
scan_resources(&doc, res, want, 0)
}
fn scan_resources(
doc: &LoDoc,
res: &Dictionary,
want: &str,
depth: usize,
) -> Option<FontMapping> {
if depth > 3 {
return None;
}
if let Ok(fonts) = res.get(b"Font") {
if let Ok(fonts) = deref(doc, fonts).as_dict() {
for (_, fref) in fonts.iter() {
let Ok(fd) = deref(doc, fref).as_dict() else { continue };
let base = fd
.get(b"BaseFont")
.ok()
.and_then(|o| deref(doc, o).as_name().ok())
.map(|n| String::from_utf8_lossy(n).to_string())
.unwrap_or_default();
let suffix = base.rsplit('+').next().unwrap_or(&base);
if suffix != want {
continue;
}
let mut code_to_uni = HashMap::new();
if let Ok(tu) = fd.get(b"ToUnicode") {
if let Object::Stream(s) = deref(doc, tu) {
if let Some(m) = parse_cmap(&stream_bytes(s)) {
code_to_uni = m;
}
}
}
// Type0 (Identity-H): mã = CID; GID tra qua /CIDToGIDMap của
// descendant font (Name "Identity" hoặc stream 2B/CID).
let subtype = fd
.get(b"Subtype")
.ok()
.and_then(|o| deref(doc, o).as_name().ok())
.map(|n| n.to_vec())
.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);
let first = match desc {
Object::Array(a) => a.first().map(|o| deref(doc, o)),
other => Some(other),
};
if let Some(Ok(dd)) = first.map(|o| o.as_dict()) {
if let Ok(map) = dd.get(b"CIDToGIDMap") {
if let Object::Stream(s) = deref(doc, map) {
let raw = stream_bytes(s);
cid_to_gid = Some(
raw.chunks(2)
.map(|c| {
u16::from_be_bytes([c[0], *c.get(1).unwrap_or(&0)])
})
.collect(),
);
}
}
// /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,
widths,
default_width,
ascent,
descent,
});
}
}
}
// Font có thể nằm trong resources của Form XObject (file Canva…).
if let Ok(xo) = res.get(b"XObject") {
if let Ok(xo) = deref(doc, xo).as_dict() {
for (_, oref) in xo.iter() {
if let Object::Stream(s) = deref(doc, oref) {
if let Ok(r2) = s.dict.get(b"Resources") {
if let Ok(r2) = deref(doc, r2).as_dict() {
if let Some(m) = scan_resources(doc, r2, want, depth + 1) {
return Some(m);
}
}
}
}
}
}
}
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),
ArrOpen,
ArrClose,
}
fn tokenize(data: &[u8]) -> Vec<Tok> {
let mut out = Vec::new();
let mut i = 0;
while i < data.len() {
match data[i] {
b'<' => {
let mut j = i + 1;
let mut hex: Vec<u8> = Vec::new();
let mut hi: Option<u8> = None;
while j < data.len() && data[j] != b'>' {
let c = data[j];
let d = match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(c - b'a' + 10),
b'A'..=b'F' => Some(c - b'A' + 10),
_ => None,
};
if let Some(d) = d {
match hi {
None => hi = Some(d),
Some(h) => {
hex.push(h * 16 + d);
hi = None;
}
}
}
j += 1;
}
if let Some(h) = hi {
hex.push(h * 16); // số hex lẻ digit: pad 0 theo spec PDF
}
out.push(Tok::Hex(hex));
i = j + 1;
}
b'[' => {
out.push(Tok::ArrOpen);
i += 1;
}
b']' => {
out.push(Tok::ArrClose);
i += 1;
}
c if c.is_ascii_alphabetic() => {
let mut j = i;
while j < data.len() && data[j].is_ascii_alphanumeric() {
j += 1;
}
out.push(Tok::Kw(String::from_utf8_lossy(&data[i..j]).to_string()));
i = j;
}
_ => i += 1,
}
}
out
}
/// Mã nguồn (≤4 byte hex) → u32 big-endian.
fn hex_code(b: &[u8]) -> Option<u32> {
if b.is_empty() || b.len() > 4 {
return None;
}
Some(b.iter().fold(0u32, |v, &x| (v << 8) | u32::from(x)))
}
/// Giá trị UTF-16BE → đúng 1 scalar Unicode (surrogate pair ok); mapping ra
/// nhiều ký tự (ligature) → None — cmap chỉ chứa 1 codepoint/glyph.
fn utf16be_single(b: &[u8]) -> Option<u32> {
if b.len() < 2 || b.len() % 2 != 0 {
return None;
}
let units: Vec<u16> = b.chunks(2).map(|c| u16::from_be_bytes([c[0], c[1]])).collect();
let mut it = char::decode_utf16(units.iter().copied());
let first = it.next()?.ok()?;
if it.next().is_some() {
return None;
}
Some(first as u32)
}
/// Parse bfchar/bfrange của CMap ToUnicode — đủ cho file thực tế
/// (LibreOffice/Word/Canva). Lỗi cục bộ thì bỏ entry đó, không fail cả map.
fn parse_cmap(data: &[u8]) -> Option<HashMap<u32, u32>> {
let toks = tokenize(data);
let mut map = HashMap::new();
let mut i = 0;
while i < toks.len() {
match &toks[i] {
Tok::Kw(k) if k == "beginbfchar" => {
i += 1;
while let (Some(Tok::Hex(src)), Some(Tok::Hex(dst))) = (toks.get(i), toks.get(i + 1)) {
if let (Some(c), Some(u)) = (hex_code(src), utf16be_single(dst)) {
map.insert(c, u);
}
i += 2;
}
}
Tok::Kw(k) if k == "beginbfrange" => {
i += 1;
loop {
let (Some(Tok::Hex(lo)), Some(Tok::Hex(hi))) = (toks.get(i), toks.get(i + 1)) else {
break;
};
let (Some(lo), Some(hi)) = (hex_code(lo), hex_code(hi)) else { break };
match toks.get(i + 2) {
Some(Tok::Hex(d)) => {
if let Some(u0) = utf16be_single(d) {
for k in 0..=hi.saturating_sub(lo).min(0xFFFF) {
map.insert(lo + k, u0 + k);
}
}
i += 3;
}
Some(Tok::ArrOpen) => {
i += 3;
let mut c = lo;
while let Some(Tok::Hex(d)) = toks.get(i) {
if let Some(u) = utf16be_single(d) {
map.insert(c, u);
}
c += 1;
i += 1;
}
if matches!(toks.get(i), Some(Tok::ArrClose)) {
i += 1;
}
}
_ => break,
}
}
}
_ => i += 1,
}
}
Some(map)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_bfchar_and_bfrange() {
let cmap = br#"
/CIDInit /ProcSet findresource begin
begincmap
1 begincodespacerange <00> <FF> endcodespacerange
2 beginbfchar
<01> <1EA3>
<0E> <D83D DE00>
endbfchar
2 beginbfrange
<20> <7E> <0020>
<02> <04> [<1EBF> <1EC7> <0110>]
endbfrange
endcmap
"#;
let m = parse_cmap(cmap).unwrap();
assert_eq!(m.get(&0x01), Some(&0x1EA3)); // ả
assert_eq!(m.get(&0x0E), Some(&0x1F600)); // surrogate pair → emoji
assert_eq!(m.get(&0x20), Some(&0x20)); // range dạng 1
assert_eq!(m.get(&0x41), Some(&0x41));
assert_eq!(m.get(&0x7E), Some(&0x7E));
assert_eq!(m.get(&0x03), Some(&0x1EC7)); // range dạng mảng
assert_eq!(m.get(&0x04), Some(&0x0110));
}
}