use std::collections::{HashMap, HashSet};
use pdf_writer::{Name, Pdf, Rect, Ref};
use ttf_parser::Face;
use super::FontMetrics;
use super::encoding::winansi_to_char;
pub(super) fn embed_truetype(
pdf: &mut Pdf,
font_ref: Ref,
descriptor_ref: Ref,
data_ref: Ref,
font_name: &str,
font_data: &[u8],
face_index: u32,
used_chars: &HashSet<char>,
alloc: &mut impl FnMut() -> Ref,
) -> Option<FontMetrics> {
let face = Face::parse(font_data, face_index).ok()?;
let units = face.units_per_em() as f32;
let ascent = face.ascender() as f32 / units * 1000.0;
let descent = face.descender() as f32 / units * 1000.0;
let cap_height = face
.capital_height()
.map(|h| h as f32 / units * 1000.0)
.unwrap_or(700.0);
let bb = face.global_bounding_box();
let bbox = Rect::new(
bb.x_min as f32 / units * 1000.0,
bb.y_min as f32 / units * 1000.0,
bb.x_max as f32 / units * 1000.0,
bb.y_max as f32 / units * 1000.0,
);
let widths_1000: Vec<f32> = (32u8..=255u8)
.map(|byte| {
face.glyph_index(winansi_to_char(byte))
.and_then(|gid| face.glyph_hor_advance(gid))
.map(|adv| adv as f32 / units * 1000.0)
.unwrap_or(0.0)
})
.collect();
let mut remapper = subsetter::GlyphRemapper::new();
let mut char_to_gid = HashMap::new();
let mut char_widths_1000 = HashMap::new();
let symbol_cmap_lookup = |ch: char| -> Option<ttf_parser::GlyphId> {
if let Some(cmap) = face.tables().cmap {
for subtable in cmap.subtables {
if let Some(gid) = subtable.glyph_index(ch as u32) {
return Some(gid);
}
}
}
None
};
for &ch in used_chars {
let gid = face
.glyph_index(ch)
.or_else(|| {
let cp = ch as u32;
if (0xF000..=0xF0FF).contains(&cp) {
let low_ch = char::from_u32(cp - 0xF000)?;
face.glyph_index(low_ch)
} else {
None
}
})
.or_else(|| symbol_cmap_lookup(ch));
if let Some(gid) = gid {
let new_gid = remapper.remap(gid.0);
char_to_gid.insert(ch, new_gid);
let w = face
.glyph_hor_advance(gid)
.map(|adv| adv as f32 / units * 1000.0)
.unwrap_or(0.0);
char_widths_1000.insert(ch, w);
}
}
let mut kern_pairs = HashMap::new();
let char_gids: Vec<(char, ttf_parser::GlyphId, u16)> = char_to_gid
.iter()
.filter_map(|(&ch, &new_gid)| face.glyph_index(ch).map(|orig| (ch, orig, new_gid)))
.collect();
if let Some(kern) = face.tables().kern {
for &(_, l_orig, l_new) in &char_gids {
for &(_, r_orig, r_new) in &char_gids {
let mut total: i16 = 0;
for subtable in kern.subtables {
if subtable.horizontal && !subtable.variable {
if let Some(val) = subtable.glyphs_kerning(l_orig, r_orig) {
total += val;
}
}
}
if total != 0 {
kern_pairs.insert((l_new, r_new), total as f32 / units * 1000.0);
}
}
}
}
if let Some(gpos) = face.tables().gpos {
use ttf_parser::gpos::PairAdjustment;
use ttf_parser::gpos::PositioningSubtable;
for lookup_idx in 0..gpos.lookups.len() {
let Some(lookup) = gpos.lookups.get(lookup_idx) else {
continue;
};
for st_idx in 0..lookup.subtables.len() {
let Some(PositioningSubtable::Pair(pair)) =
lookup.subtables.get::<PositioningSubtable>(st_idx)
else {
continue;
};
match pair {
PairAdjustment::Format1 { coverage, sets } => {
for &(_, l_orig, l_new) in &char_gids {
let Some(cov_idx) = coverage.get(l_orig) else {
continue;
};
let Some(pair_set) = sets.get(cov_idx) else {
continue;
};
for &(_, r_orig, r_new) in &char_gids {
if let Some((val1, _)) = pair_set.get(r_orig) {
if val1.x_advance != 0 {
kern_pairs
.entry((l_new, r_new))
.or_insert(val1.x_advance as f32 / units * 1000.0);
}
}
}
}
}
PairAdjustment::Format2 {
coverage,
classes,
matrix,
} => {
for &(_, l_orig, l_new) in &char_gids {
if coverage.get(l_orig).is_none() {
continue;
}
let c1 = classes.0.get(l_orig);
for &(_, r_orig, r_new) in &char_gids {
let c2 = classes.1.get(r_orig);
if let Some((val1, _)) = matrix.get((c1, c2)) {
if val1.x_advance != 0 {
kern_pairs
.entry((l_new, r_new))
.or_insert(val1.x_advance as f32 / units * 1000.0);
}
}
}
}
}
}
}
}
}
if !kern_pairs.is_empty() {
log::info!(
"Kerning for {font_name}: {} pairs from {} chars",
kern_pairs.len(),
char_gids.len(),
);
}
let subset_data = subsetter::subset(font_data, face_index, &remapper).unwrap_or_else(|e| {
log::warn!("Font subsetting failed for {font_name}: {e} — embedding full font");
font_data.to_vec()
});
let data_len = i32::try_from(subset_data.len()).ok()?;
pdf.stream(data_ref, &subset_data)
.pair(Name(b"Length1"), data_len);
let ps_name = font_name.replace(' ', "");
pdf.font_descriptor(descriptor_ref)
.name(Name(ps_name.as_bytes()))
.flags(pdf_writer::types::FontFlags::NON_SYMBOLIC)
.bbox(bbox)
.italic_angle(0.0)
.ascent(ascent)
.descent(descent)
.cap_height(cap_height)
.stem_v(80.0)
.font_file2(data_ref);
let cid_font_ref = alloc();
let system_info = pdf_writer::types::SystemInfo {
registry: pdf_writer::Str(b"Adobe"),
ordering: pdf_writer::Str(b"Identity"),
supplement: 0,
};
{
let mut cid = pdf.cid_font(cid_font_ref);
cid.subtype(pdf_writer::types::CidFontType::Type2);
cid.base_font(Name(ps_name.as_bytes()));
cid.system_info(system_info);
cid.font_descriptor(descriptor_ref);
cid.default_width(0.0);
cid.cid_to_gid_map_predefined(Name(b"Identity"));
let mut gid_widths: Vec<(u16, f32)> = char_to_gid
.iter()
.filter_map(|(&ch, &new_gid)| {
face.glyph_index(ch)
.and_then(|gid| face.glyph_hor_advance(gid))
.map(|adv| (new_gid, adv as f32 / units * 1000.0))
})
.collect();
gid_widths.sort_by_key(|&(gid, _)| gid);
if !gid_widths.is_empty() {
let mut w = cid.widths();
for &(gid, width) in &gid_widths {
w.consecutive(gid, [width]);
}
}
}
let tounicode_ref = alloc();
let cmap_name = format!("{}-UTF16", ps_name);
let mut cmap = pdf_writer::types::UnicodeCmap::new(
Name(cmap_name.as_bytes()),
pdf_writer::types::SystemInfo {
registry: pdf_writer::Str(b"Adobe"),
ordering: pdf_writer::Str(b"Identity"),
supplement: 0,
},
);
for (&ch, &new_gid) in &char_to_gid {
cmap.pair(new_gid, ch);
}
let cmap_data = cmap.finish();
pdf.stream(tounicode_ref, cmap_data.as_slice());
pdf.type0_font(font_ref)
.base_font(Name(ps_name.as_bytes()))
.encoding_predefined(Name(b"Identity-H"))
.descendant_font(cid_font_ref)
.to_unicode(tounicode_ref);
let line_gap = face.line_gap() as f32;
let line_h_ratio = (face.ascender() as f32 - face.descender() as f32 + line_gap) / units;
let ascender_ratio = face.ascender() as f32 / units;
Some(FontMetrics {
widths_1000,
line_h_ratio,
ascender_ratio,
char_to_gid,
char_widths_1000,
kern_pairs,
})
}