use std::collections::BTreeMap;
use pdf_writer::types::{CidFontType, FontFlags, SystemInfo};
use pdf_writer::{Filter, Finish, Name, Pdf, Rect, Ref, Str};
use zenith_core::FontProvider;
use zenith_scene::{Scene, SceneCommand};
pub(super) const REFS_PER_FONT: i32 = 5;
type Usage = BTreeMap<String, BTreeMap<u16, String>>;
pub(super) struct EmbeddedFont {
bytes: Vec<u8>,
is_cff: bool,
gid_to_cid: BTreeMap<u16, u16>,
cid_to_unicode: BTreeMap<u16, String>,
cid_widths: BTreeMap<u16, u16>,
units_per_em: u16,
ascent: i16,
descent: i16,
cap_height: i16,
italic_angle: f32,
}
pub(super) struct FontPlan {
pub(super) fonts: Vec<EmbeddedFont>,
index: BTreeMap<String, usize>,
}
impl FontPlan {
pub(super) fn cid_of(&self, font_id: &str, glyph_id: u16) -> Option<(usize, u16)> {
let idx = *self.index.get(font_id)?;
let cid = *self.fonts.get(idx)?.gid_to_cid.get(&glyph_id)?;
Some((idx, cid))
}
pub(super) fn resource_index(&self, font_id: &str) -> Option<usize> {
self.index.get(font_id).copied()
}
}
pub(super) fn collect_usage(scenes: &[Scene]) -> Usage {
let mut usage: Usage = BTreeMap::new();
for scene in scenes {
for cmd in &scene.commands {
let SceneCommand::DrawGlyphRun {
font_id,
selectable,
glyphs,
..
} = cmd
else {
continue;
};
if !*selectable {
continue;
}
let entry = usage.entry(font_id.clone()).or_default();
for g in glyphs {
let slot = entry.entry(g.glyph_id).or_default();
if slot.is_empty() && !g.text.is_empty() {
*slot = g.text.clone();
}
}
}
}
usage
}
pub(super) fn build_plan(usage: &Usage, fonts: &dyn FontProvider, subset: bool) -> FontPlan {
let mut plan = FontPlan {
fonts: Vec::new(),
index: BTreeMap::new(),
};
for (font_id, glyphs) in usage {
let Some(font_data) = fonts.by_id(font_id) else {
continue;
};
let Some(embedded) = build_font(&font_data.bytes, font_data.index, glyphs, subset) else {
continue;
};
let idx = plan.fonts.len();
plan.fonts.push(embedded);
plan.index.insert(font_id.clone(), idx);
}
plan
}
fn build_font(
bytes: &[u8],
index: u32,
glyphs: &BTreeMap<u16, String>,
subset: bool,
) -> Option<EmbeddedFont> {
let face = ttf_parser::Face::parse(bytes, index).ok()?;
let units_per_em = face.units_per_em();
if units_per_em == 0 {
return None;
}
let is_cff = face.tables().cff.is_some();
let (program, gid_to_cid) = if subset {
let mut remapper = subsetter::GlyphRemapper::new();
for &gid in glyphs.keys() {
remapper.remap(gid);
}
let subset_bytes = subsetter::subset(bytes, index, &remapper).ok()?;
let mut map = BTreeMap::new();
for &gid in glyphs.keys() {
if let Some(cid) = remapper.get(gid) {
map.insert(gid, cid);
}
}
(subset_bytes, map)
} else {
let map: BTreeMap<u16, u16> = glyphs.keys().map(|&gid| (gid, gid)).collect();
(bytes.to_vec(), map)
};
let mut cid_to_unicode = BTreeMap::new();
let mut cid_widths = BTreeMap::new();
for (&gid, text) in glyphs {
let Some(&cid) = gid_to_cid.get(&gid) else {
continue;
};
if !text.is_empty() {
cid_to_unicode.insert(cid, text.clone());
}
let advance = face
.glyph_hor_advance(ttf_parser::GlyphId(gid))
.unwrap_or(0);
cid_widths.insert(cid, advance);
}
Some(EmbeddedFont {
bytes: program,
is_cff,
gid_to_cid,
cid_to_unicode,
cid_widths,
units_per_em,
ascent: face.ascender(),
descent: face.descender(),
cap_height: face.capital_height().unwrap_or_else(|| face.ascender()),
italic_angle: face.italic_angle(),
})
}
pub(super) struct FontRefs {
type0: Ref,
cid: Ref,
descriptor: Ref,
program: Ref,
to_unicode: Ref,
}
impl FontRefs {
pub(super) fn type0_ref(&self) -> Ref {
self.type0
}
}
pub(super) fn font_refs_at(base: i32, idx: usize) -> FontRefs {
let b = base + (idx as i32) * REFS_PER_FONT;
FontRefs {
type0: Ref::new(b),
cid: Ref::new(b + 1),
descriptor: Ref::new(b + 2),
program: Ref::new(b + 3),
to_unicode: Ref::new(b + 4),
}
}
pub(super) fn write_font(pdf: &mut Pdf, font: &EmbeddedFont, refs: &FontRefs) {
let upem = f32::from(font.units_per_em);
let to_thousand = |v: i16| f32::from(v) * 1000.0 / upem;
let mut t0 = pdf.type0_font(refs.type0);
t0.base_font(Name(b"ZenithFont"));
t0.encoding_predefined(Name(b"Identity-H"));
t0.descendant_font(refs.cid);
t0.to_unicode(refs.to_unicode);
t0.finish();
let mut cid = pdf.cid_font(refs.cid);
cid.subtype(CidFontType::Type2);
cid.base_font(Name(b"ZenithFont"));
cid.system_info(SystemInfo {
registry: Str(b"Adobe"),
ordering: Str(b"Identity"),
supplement: 0,
});
cid.font_descriptor(refs.descriptor);
cid.default_width(1000.0);
cid.cid_to_gid_map_predefined(Name(b"Identity"));
{
let mut w = cid.widths();
for (&c, &advance) in &font.cid_widths {
let scaled = f32::from(advance) * 1000.0 / upem;
w.consecutive(c, [scaled]);
}
w.finish();
}
cid.finish();
let flags = if font.italic_angle != 0.0 {
FontFlags::NON_SYMBOLIC | FontFlags::ITALIC
} else {
FontFlags::NON_SYMBOLIC
};
let mut desc = pdf.font_descriptor(refs.descriptor);
desc.name(Name(b"ZenithFont"));
desc.flags(flags);
desc.bbox(Rect::new(
0.0,
to_thousand(font.descent),
1000.0,
to_thousand(font.ascent),
));
desc.italic_angle(font.italic_angle);
desc.ascent(to_thousand(font.ascent));
desc.descent(to_thousand(font.descent));
desc.cap_height(to_thousand(font.cap_height));
desc.stem_v(80.0);
if font.is_cff {
desc.font_file3(refs.program);
} else {
desc.font_file2(refs.program);
}
desc.finish();
let compressed = miniz_oxide::deflate::compress_to_vec_zlib(&font.bytes, 6);
let mut stream = pdf.stream(refs.program, &compressed);
stream.filter(Filter::FlateDecode);
if font.is_cff {
stream.pair(Name(b"Subtype"), Name(b"OpenType"));
} else {
stream.pair(Name(b"Length1"), font.bytes.len() as i32);
}
stream.finish();
let cmap = build_tounicode_cmap(font);
pdf.stream(refs.to_unicode, cmap.as_bytes()).finish();
}
fn build_tounicode_cmap(font: &EmbeddedFont) -> String {
let mut entries: Vec<(u16, String)> = Vec::new();
for (&cid, text) in &font.cid_to_unicode {
let hex: String = text
.chars()
.flat_map(|c| {
let mut buf = [0u16; 2];
c.encode_utf16(&mut buf)
.iter()
.map(|u| format!("{u:04X}"))
.collect::<Vec<_>>()
})
.collect();
entries.push((cid, hex));
}
entries.sort_by_key(|(cid, _)| *cid);
let mut cmap = String::new();
cmap.push_str("/CIDInit /ProcSet findresource begin\n");
cmap.push_str("12 dict begin\nbegincmap\n");
cmap.push_str("/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n");
cmap.push_str("/CMapName /Adobe-Identity-UCS def\n");
cmap.push_str("/CMapType 2 def\n");
cmap.push_str("1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n");
for chunk in entries.chunks(100) {
cmap.push_str(&format!("{} beginbfchar\n", chunk.len()));
for (cid, hex) in chunk {
cmap.push_str(&format!("<{cid:04X}> <{hex}>\n"));
}
cmap.push_str("endbfchar\n");
}
cmap.push_str("endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend\n");
cmap
}