use std::collections::{BTreeSet, HashMap};
use pdf_writer::types::{SystemInfo, UnicodeCmap};
use pdf_writer::{Name, Str};
use super::render_context::PdfOp;
use super::ttf::TtfMetrics;
use super::PageRecord;
pub(super) const ADOBE_IDENTITY_UCS: SystemInfo<'static> = SystemInfo { registry: Str(b"Adobe"), ordering: Str(b"UCS"), supplement: 0 };
pub(super) struct FontData {
pub subset_bytes: Vec<u8>,
pub widths: Vec<(u16, f32)>,
pub to_unicode: Vec<u8>,
pub orig_to_new: HashMap<u16, u16>,
pub subset_tag: Option<String>,
}
pub(super) fn build_font_data(font_index: u32, ttf_bytes: &[u8], metrics: &TtfMetrics, pages: &[PageRecord]) -> FontData {
let mut used_gids: BTreeSet<u16> = BTreeSet::new();
let mut gid_to_char: HashMap<u16, char> = HashMap::new();
for page in pages {
for run in &page.runs {
if run.font_id.0 != font_index {
continue;
}
for &(gid, ch) in &run.glyphs {
used_gids.insert(gid);
if gid != 0 {
gid_to_char.entry(gid).or_insert(ch);
}
}
}
for op in &page.content_ops {
if let PdfOp::Text { font, glyphs, .. } = op {
if font.0 != font_index {
continue;
}
for &(gid, ch) in glyphs {
used_gids.insert(gid);
if gid != 0 {
gid_to_char.entry(gid).or_insert(ch);
}
}
}
}
}
let mut remapper = subsetter::GlyphRemapper::new();
for &gid in &used_gids {
remapper.remap(gid);
}
match subsetter::subset(ttf_bytes, 0, &remapper) {
Ok(subset_bytes) => {
let mut widths = Vec::new();
let mut orig_to_new = HashMap::new();
let mut cmap = UnicodeCmap::new(Name(b"Adobe-Identity-UCS"), ADOBE_IDENTITY_UCS);
for (new_cid, old_gid) in remapper.remapped_gids().enumerate() {
let new_cid = new_cid as u16;
orig_to_new.insert(old_gid, new_cid);
widths.push((new_cid, metrics.advance_1000_for_gid(old_gid) as f32));
if let Some(&ch) = gid_to_char.get(&old_gid) {
cmap.pair(new_cid, ch);
}
}
FontData {
subset_bytes,
widths,
to_unicode: cmap.finish().into_vec(),
orig_to_new,
subset_tag: Some(subset_tag(font_index)),
}
}
Err(_) => {
let mut widths = Vec::new();
let mut orig_to_new = HashMap::new();
let mut cmap = UnicodeCmap::new(Name(b"Adobe-Identity-UCS"), ADOBE_IDENTITY_UCS);
for &gid in &used_gids {
orig_to_new.insert(gid, gid);
widths.push((gid, metrics.advance_1000_for_gid(gid) as f32));
if let Some(&ch) = gid_to_char.get(&gid) {
cmap.pair(gid, ch);
}
}
FontData { subset_bytes: ttf_bytes.to_vec(), widths, to_unicode: cmap.finish().into_vec(), orig_to_new, subset_tag: None }
}
}
}
fn subset_tag(mut index: u32) -> String {
let mut letters = [b'A'; 6];
for slot in letters.iter_mut().rev() {
*slot = b'A' + (index % 26) as u8;
index /= 26;
}
String::from_utf8(letters.to_vec()).unwrap_or_else(|_| "AAAAAA".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pdf::{FontId, PageTextRun};
const ROBOTO_REGULAR: &[u8] = include_bytes!("../../../uzor-fonts/fonts/Roboto-Regular.ttf");
fn page_with_glyphs(font_index: u32, glyphs: Vec<(u16, char)>) -> PageRecord {
PageRecord {
width_pt: 100.0,
height_pt: 100.0,
raster_rgb: None,
runs: vec![PageTextRun { font_id: FontId(font_index), size_pt: 12.0, x_pt: 0.0, y_pt: 0.0, rgb: 0, glyphs, real_advances_pt: None, struct_tag: None }],
links: Vec::new(),
content_ops: Vec::new(),
}
}
#[test]
fn subsetting_a_small_glyph_set_shrinks_the_embedded_font() {
let metrics = TtfMetrics::parse(ROBOTO_REGULAR);
let gid_a = metrics.gid_for_char('A').expect("Roboto must have a glyph for 'A'");
let pages = vec![page_with_glyphs(0, vec![(gid_a, 'A')])];
let data = build_font_data(0, ROBOTO_REGULAR, &metrics, &pages);
assert!(data.subset_tag.is_some(), "subsetting a well-formed TrueType font must succeed");
assert!(
data.subset_bytes.len() < ROBOTO_REGULAR.len(),
"a 1-glyph subset ({} bytes) must be far smaller than the full font ({} bytes)",
data.subset_bytes.len(),
ROBOTO_REGULAR.len()
);
}
#[test]
fn tounicode_and_cid_remap_agree_on_the_same_glyph() {
let metrics = TtfMetrics::parse(ROBOTO_REGULAR);
let gid_a = metrics.gid_for_char('A').expect("Roboto must have a glyph for 'A'");
let pages = vec![page_with_glyphs(0, vec![(gid_a, 'A')])];
let data = build_font_data(0, ROBOTO_REGULAR, &metrics, &pages);
let new_cid = *data.orig_to_new.get(&gid_a).expect("the used glyph must be present in the remap table");
let cmap_text = String::from_utf8_lossy(&data.to_unicode);
let cid_hex = format!("{new_cid:04x}");
assert!(
cmap_text.to_lowercase().contains(&format!("<{cid_hex}> <0041>")),
"ToUnicode must map the SAME cid {new_cid} used for the CID font to U+0041, got: {cmap_text}"
);
}
#[test]
fn font_index_filters_out_other_fonts_own_runs() {
let metrics = TtfMetrics::parse(ROBOTO_REGULAR);
let gid_a = metrics.gid_for_char('A').expect("Roboto must have a glyph for 'A'");
let gid_b = metrics.gid_for_char('B').expect("Roboto must have a glyph for 'B'");
let pages = vec![page_with_glyphs(0, vec![(gid_a, 'A')]), page_with_glyphs(1, vec![(gid_b, 'B')])];
let data = build_font_data(0, ROBOTO_REGULAR, &metrics, &pages);
assert!(data.orig_to_new.contains_key(&gid_a), "font 0's own glyph must be remapped");
assert!(!data.orig_to_new.contains_key(&gid_b), "font 1's own glyph must never leak into font 0's remap table");
}
}