use std::collections::BTreeMap;
use lopdf::{Dictionary, Document, Object, ObjectId, Stream};
use ttf_parser::Face;
fn dejavu_mono_regular() -> Option<&'static [u8]> {
for data in typst_assets::fonts() {
let Ok(face) = Face::parse(data, 0) else { continue };
if face.tables().glyf.is_some()
&& face.is_monospaced()
&& !face.is_bold()
&& !face.is_italic()
&& face.glyph_index('\u{0410}').is_some() {
return Some(data);
}
}
None
}
pub struct EmbeddedFont {
data: &'static [u8],
face: Face<'static>,
units_per_em: f32,
used: BTreeMap<u16, char>,
}
impl EmbeddedFont {
pub fn load() -> Option<EmbeddedFont> {
let data = dejavu_mono_regular()?;
let face = Face::parse(data, 0).ok()?;
let units_per_em = face.units_per_em() as f32;
if units_per_em <= 0.0 {
return None;
}
Some(EmbeddedFont { data, face, units_per_em, used: BTreeMap::new() })
}
pub fn encode(&mut self, text: &str) -> String {
let mut s = String::with_capacity(text.len() * 4 + 2);
s.push('<');
for ch in text.chars() {
let gid = self.face.glyph_index(ch).map(|g| g.0).unwrap_or(0);
self.used.insert(gid, ch);
s.push_str(&format!("{gid:04X}"));
}
s.push('>');
s
}
pub fn width(&self, text: &str, size: f32) -> f32 {
text.chars()
.map(|ch| {
let adv = self
.face
.glyph_index(ch)
.and_then(|g| self.face.glyph_hor_advance(g))
.unwrap_or(0) as f32;
adv / self.units_per_em * size
})
.sum()
}
pub fn em_advance(&self) -> f32 {
self.width("0", 1.0)
}
fn scale_1000(&self, v: f32) -> i64 {
(v / self.units_per_em * 1000.0).round() as i64
}
pub fn finalize(self, doc: &mut Document) -> ObjectId {
let mut ff = Stream::new(Dictionary::new(), self.data.to_vec());
ff.dict.set("Length1", Object::Integer(self.data.len() as i64));
let _ = ff.compress();
let ff_id = doc.add_object(ff);
let bbox = self.face.global_bounding_box();
let cap = self.face.capital_height().unwrap_or_else(|| self.face.ascender());
let mut fd = Dictionary::new();
fd.set("Type", "FontDescriptor");
fd.set("FontName", Object::Name(b"DejaVuSansMono".to_vec()));
fd.set("Flags", Object::Integer(33));
fd.set(
"FontBBox",
vec![
Object::Integer(self.scale_1000(bbox.x_min as f32)),
Object::Integer(self.scale_1000(bbox.y_min as f32)),
Object::Integer(self.scale_1000(bbox.x_max as f32)),
Object::Integer(self.scale_1000(bbox.y_max as f32)),
],
);
fd.set("ItalicAngle", Object::Real(self.face.italic_angle()));
fd.set("Ascent", Object::Integer(self.scale_1000(self.face.ascender() as f32)));
fd.set("Descent", Object::Integer(self.scale_1000(self.face.descender() as f32)));
fd.set("CapHeight", Object::Integer(self.scale_1000(cap as f32)));
fd.set("StemV", Object::Integer(80)); fd.set("FontFile2", Object::Reference(ff_id));
let fd_id = doc.add_object(fd);
let dw = self
.face
.glyph_index(' ')
.and_then(|g| self.face.glyph_hor_advance(g))
.map(|a| self.scale_1000(a as f32))
.unwrap_or(600);
let mut cid_sys = Dictionary::new();
cid_sys.set("Registry", Object::string_literal("Adobe"));
cid_sys.set("Ordering", Object::string_literal("Identity"));
cid_sys.set("Supplement", Object::Integer(0));
let mut cid = Dictionary::new();
cid.set("Type", "Font");
cid.set("Subtype", "CIDFontType2");
cid.set("BaseFont", Object::Name(b"DejaVuSansMono".to_vec()));
cid.set("CIDSystemInfo", Object::Dictionary(cid_sys));
cid.set("FontDescriptor", Object::Reference(fd_id));
cid.set("CIDToGIDMap", Object::Name(b"Identity".to_vec()));
cid.set("DW", Object::Integer(dw));
let cid_id = doc.add_object(cid);
let mut tu = Stream::new(Dictionary::new(), self.build_tounicode().into_bytes());
let _ = tu.compress();
let tu_id = doc.add_object(tu);
let mut t0 = Dictionary::new();
t0.set("Type", "Font");
t0.set("Subtype", "Type0");
t0.set("BaseFont", Object::Name(b"DejaVuSansMono".to_vec()));
t0.set("Encoding", Object::Name(b"Identity-H".to_vec()));
t0.set("DescendantFonts", vec![Object::Reference(cid_id)]);
t0.set("ToUnicode", Object::Reference(tu_id));
doc.add_object(t0)
}
fn build_tounicode(&self) -> String {
let entries: Vec<(u16, char)> = self.used.iter().map(|(&g, &c)| (g, c)).collect();
let mut body = String::new();
for chunk in entries.chunks(100) {
body.push_str(&format!("{} beginbfchar\n", chunk.len()));
for (g, c) in chunk {
let mut buf = [0u16; 2];
let hex: String = c.encode_utf16(&mut buf).iter().map(|u| format!("{u:04X}")).collect();
body.push_str(&format!("<{g:04X}> <{hex}>\n"));
}
body.push_str("endbfchar\n");
}
format!(
"/CIDInit /ProcSet findresource begin\n\
12 dict begin\n\
begincmap\n\
/CIDSystemInfo <</Registry (Adobe) /Ordering (UCS) /Supplement 0>> def\n\
/CMapName /Adobe-Identity-UCS def\n\
/CMapType 2 def\n\
1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n\
{body}\
endcmap\n\
CMapName currentdict /CMap defineresource pop\n\
end\nend\n"
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn loads_the_bundled_unicode_font() {
let f = EmbeddedFont::load().expect("DejaVu Sans Mono should be bundled");
let mut f = f;
let hex = f.encode("Война café");
assert!(hex.starts_with('<') && hex.ends_with('>'));
assert!(!hex.contains("0000"), "every char should map to a real glyph: {hex}");
assert!(f.width("Война", 12.0) > 0.0);
}
#[test]
fn finalize_builds_a_type0_font_object() {
let mut doc = Document::with_version("1.5");
let mut f = EmbeddedFont::load().unwrap();
let _ = f.encode("Мир");
let id = f.finalize(&mut doc);
let font = doc.get_object(id).unwrap().as_dict().unwrap();
assert_eq!(font.get(b"Subtype").unwrap().as_name().unwrap(), b"Type0");
assert_eq!(font.get(b"Encoding").unwrap().as_name().unwrap(), b"Identity-H");
assert!(font.get(b"ToUnicode").is_ok());
assert!(font.get(b"DescendantFonts").is_ok());
}
}