use crate::error::{ManipError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Outlines {
TrueType,
Cff,
}
use lopdf::{dictionary, Dictionary, Document, Object, ObjectId, Stream};
use std::collections::BTreeMap;
use std::sync::Arc;
pub(crate) const UNICODE_FONT_RESOURCE: &str = "F__Uni";
#[derive(Clone)]
pub struct UnicodeFont {
data: Arc<Vec<u8>>,
units_per_em: u16,
outlines: Outlines,
fingerprint: [u8; 8],
postscript_name: String,
variations: Vec<([u8; 4], f32)>,
}
const WGHT: [u8; 4] = *b"wght";
const REGULAR_WEIGHT: f32 = 400.0;
impl std::fmt::Debug for UnicodeFont {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UnicodeFont")
.field("name", &self.postscript_name)
.field("bytes", &self.data.len())
.field("fingerprint", &hex8(&self.fingerprint))
.finish()
}
}
impl PartialEq for UnicodeFont {
fn eq(&self, other: &Self) -> bool {
self.fingerprint == other.fingerprint
}
}
impl Eq for UnicodeFont {}
#[cfg(feature = "serde")]
impl serde::Serialize for UnicodeFont {
fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut st = s.serialize_struct("UnicodeFont", 2)?;
st.serialize_field("name", &self.postscript_name)?;
st.serialize_field("fingerprint", &hex8(&self.fingerprint))?;
st.end()
}
}
fn hex8(bytes: &[u8; 8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
impl UnicodeFont {
pub fn from_bytes(data: Vec<u8>) -> Result<Self> {
let face = ttf_parser::Face::parse(&data, 0)
.map_err(|e| ManipError::Other(format!("not a usable font program: {e}")))?;
let outlines = if face.tables().glyf.is_some() {
Outlines::TrueType
} else if face.tables().cff.is_some() {
Outlines::Cff
} else {
return Err(ManipError::Other(
"font carries neither TrueType (glyf) nor CFF outlines, so there is \
nothing to embed"
.to_string(),
));
};
let units_per_em = face.units_per_em();
if units_per_em == 0 {
return Err(ManipError::Other(
"font declares unitsPerEm = 0, cannot scale widths".to_string(),
));
}
let postscript_name = face
.names()
.into_iter()
.find(|n| n.name_id == ttf_parser::name_id::POST_SCRIPT_NAME)
.and_then(|n| n.to_string())
.unwrap_or_else(|| "EmbeddedFont".to_string());
let fingerprint = {
use sha2::{Digest, Sha256};
let digest = Sha256::digest(&data);
let mut fp = [0u8; 8];
fp.copy_from_slice(&digest[..8]);
fp
};
let variations = face
.variation_axes()
.into_iter()
.find(|a| a.tag.to_bytes() == WGHT)
.filter(|a| (a.def_value - REGULAR_WEIGHT).abs() > f32::EPSILON)
.map(|a| vec![(WGHT, REGULAR_WEIGHT.clamp(a.min_value, a.max_value))])
.unwrap_or_default();
Ok(Self {
data: Arc::new(data),
units_per_em,
outlines,
fingerprint,
postscript_name,
variations,
})
}
#[must_use]
pub fn with_variation(mut self, axis: &[u8; 4], value: f32) -> Self {
self.variations.retain(|(t, _)| t != axis);
self.variations.push((*axis, value));
self
}
#[must_use]
pub fn is_variable(&self) -> bool {
ttf_parser::Face::parse(&self.data, 0)
.map(|f| !f.variation_axes().is_empty())
.unwrap_or(false)
}
#[must_use]
pub fn missing_chars(&self, text: &str) -> Vec<char> {
let Ok(face) = ttf_parser::Face::parse(&self.data, 0) else {
return text.chars().collect();
};
let mut missing = Vec::new();
for ch in text.chars() {
if face.glyph_index(ch).is_none() && !missing.contains(&ch) {
missing.push(ch);
}
}
missing
}
#[must_use]
pub fn covers(&self, text: &str) -> bool {
self.missing_chars(text).is_empty()
}
#[must_use]
pub fn name(&self) -> &str {
&self.postscript_name
}
}
pub struct UnicodeEncoder {
font: UnicodeFont,
remapper: subsetter::GlyphRemapper,
to_unicode: BTreeMap<u16, String>,
advances: BTreeMap<u16, u16>,
}
impl UnicodeEncoder {
#[must_use]
pub fn new(font: UnicodeFont) -> Self {
Self {
font,
remapper: subsetter::GlyphRemapper::new(),
to_unicode: BTreeMap::new(),
advances: BTreeMap::new(),
}
}
pub fn encode(&mut self, text: &str) -> Result<Vec<u8>> {
let face = ttf_parser::Face::parse(&self.font.data, 0)
.map_err(|e| ManipError::Other(format!("font became unparseable: {e}")))?;
let missing: Vec<char> = text
.chars()
.filter(|c| face.glyph_index(*c).is_none())
.collect();
if !missing.is_empty() {
let shown: String = missing
.iter()
.take(8)
.map(|c| format!("'{c}' (U+{:04X})", *c as u32))
.collect::<Vec<_>>()
.join(", ");
return Err(ManipError::Other(format!(
"font '{}' has no glyph for {shown}{}",
self.font.postscript_name,
if missing.len() > 8 {
format!(" and {} more", missing.len() - 8)
} else {
String::new()
}
)));
}
let mut bytes = Vec::with_capacity(text.len() * 2);
for ch in text.chars() {
let Some(old_gid) = face.glyph_index(ch) else {
continue;
};
let new_gid = self.remapper.remap(old_gid.0);
self.to_unicode
.entry(new_gid)
.or_insert_with(|| ch.to_string());
if let Some(adv) = face.glyph_hor_advance(old_gid) {
self.advances.insert(new_gid, adv);
}
bytes.extend_from_slice(&new_gid.to_be_bytes());
}
Ok(bytes)
}
#[must_use]
pub fn width_em(&self, text: &str) -> f64 {
let Ok(face) = ttf_parser::Face::parse(&self.font.data, 0) else {
return 0.0;
};
let upem = f64::from(self.font.units_per_em);
text.chars()
.filter_map(|c| face.glyph_index(c))
.filter_map(|g| face.glyph_hor_advance(g))
.map(|a| f64::from(a) / upem)
.sum()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.to_unicode.is_empty()
}
pub fn embed(self, doc: &mut Document, page_num: u32) -> Result<String> {
if self.is_empty() {
return Err(ManipError::Other(
"nothing encoded; no font to embed".to_string(),
));
}
let subset = if self.font.variations.is_empty() {
subsetter::subset(&self.font.data, 0, &self.remapper)
} else {
let coords: Vec<(subsetter::Tag, f32)> = self
.font
.variations
.iter()
.map(|(tag, value)| (subsetter::Tag::new(tag), *value))
.collect();
subsetter::subset_with_variations(&self.font.data, 0, &coords, &self.remapper)
}
.map_err(|e| ManipError::Other(format!("font subsetting failed: {e}")))?;
let face = ttf_parser::Face::parse(&self.font.data, 0)
.map_err(|e| ManipError::Other(format!("font became unparseable: {e}")))?;
let upem = f64::from(self.font.units_per_em);
let to_pdf = |v: f64| (v * 1000.0 / upem).round() as i64;
let descriptor_id = self.write_descriptor(doc, &subset, &face, to_pdf)?;
let cid_font_id = self.write_cid_font(doc, descriptor_id, to_pdf);
let to_unicode_id = self.write_to_unicode(doc)?;
let base_font = self.subset_base_font_name();
let type0 = dictionary! {
"Type" => Object::Name(b"Font".to_vec()),
"Subtype" => Object::Name(b"Type0".to_vec()),
"BaseFont" => Object::Name(base_font.into_bytes()),
"Encoding" => Object::Name(b"Identity-H".to_vec()),
"DescendantFonts" => Object::Array(vec![Object::Reference(cid_font_id)]),
"ToUnicode" => Object::Reference(to_unicode_id),
};
let type0_id = doc.add_object(Object::Dictionary(type0));
register_page_font(doc, page_num, UNICODE_FONT_RESOURCE, type0_id).ok_or_else(|| {
ManipError::Other(format!(
"could not register the Unicode font in the resources of page {page_num}"
))
})?;
Ok(UNICODE_FONT_RESOURCE.to_string())
}
fn subset_base_font_name(&self) -> String {
let tag: String = self.font.fingerprint[..6]
.iter()
.map(|b| char::from(b'A' + (b % 26)))
.collect();
let clean: String = self
.font
.postscript_name
.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '-')
.collect();
format!("{tag}+{clean}")
}
fn write_descriptor(
&self,
doc: &mut Document,
subset: &[u8],
face: &ttf_parser::Face,
to_pdf: impl Fn(f64) -> i64,
) -> Result<ObjectId> {
let stream_dict = match self.font.outlines {
Outlines::TrueType => {
dictionary! { "Length1" => Object::Integer(subset.len() as i64) }
}
Outlines::Cff => {
dictionary! { "Subtype" => Object::Name(b"OpenType".to_vec()) }
}
};
let mut stream = Stream::new(stream_dict, subset.to_vec());
let _ = stream.compress();
let file_id = doc.add_object(Object::Stream(stream));
let bbox = face.global_bounding_box();
let mut descriptor = dictionary! {
"Type" => Object::Name(b"FontDescriptor".to_vec()),
"FontName" => Object::Name(self.subset_base_font_name().into_bytes()),
"Flags" => Object::Integer(4),
"FontBBox" => Object::Array(vec![
Object::Integer(to_pdf(f64::from(bbox.x_min))),
Object::Integer(to_pdf(f64::from(bbox.y_min))),
Object::Integer(to_pdf(f64::from(bbox.x_max))),
Object::Integer(to_pdf(f64::from(bbox.y_max))),
]),
"ItalicAngle" => Object::Real(face.italic_angle()),
"Ascent" => Object::Integer(to_pdf(f64::from(face.ascender()))),
"Descent" => Object::Integer(to_pdf(f64::from(face.descender()))),
"CapHeight" => Object::Integer(
face.capital_height()
.map_or_else(|| to_pdf(f64::from(face.ascender())), |h| to_pdf(f64::from(h)))
),
"StemV" => Object::Integer(80),
};
match self.font.outlines {
Outlines::TrueType => {
descriptor.set("FontFile2", Object::Reference(file_id));
}
Outlines::Cff => {
descriptor.set("FontFile3", Object::Reference(file_id));
}
}
Ok(doc.add_object(Object::Dictionary(descriptor)))
}
fn write_cid_font(
&self,
doc: &mut Document,
descriptor_id: ObjectId,
to_pdf: impl Fn(f64) -> i64,
) -> ObjectId {
let subtype: &[u8] = match self.font.outlines {
Outlines::TrueType => b"CIDFontType2",
Outlines::Cff => b"CIDFontType0",
};
let mut cid_font = dictionary! {
"Type" => Object::Name(b"Font".to_vec()),
"Subtype" => Object::Name(subtype.to_vec()),
"BaseFont" => Object::Name(self.subset_base_font_name().into_bytes()),
"CIDSystemInfo" => dictionary! {
"Registry" => Object::string_literal("Adobe"),
"Ordering" => Object::string_literal("Identity"),
"Supplement" => Object::Integer(0),
},
"FontDescriptor" => Object::Reference(descriptor_id),
"DW" => Object::Integer(1000),
"W" => Object::Array(self.build_w_array(to_pdf)),
};
if self.font.outlines == Outlines::TrueType {
cid_font.set("CIDToGIDMap", Object::Name(b"Identity".to_vec()));
}
doc.add_object(Object::Dictionary(cid_font))
}
fn build_w_array(&self, to_pdf: impl Fn(f64) -> i64) -> Vec<Object> {
let mut out = Vec::new();
let mut run_start: Option<u16> = None;
let mut run: Vec<Object> = Vec::new();
let mut prev: Option<u16> = None;
for (&gid, &adv) in &self.advances {
let contiguous = prev.is_some_and(|p| gid == p + 1);
if !contiguous {
if let Some(start) = run_start.take() {
out.push(Object::Integer(i64::from(start)));
out.push(Object::Array(std::mem::take(&mut run)));
}
run_start = Some(gid);
}
run.push(Object::Integer(to_pdf(f64::from(adv))));
prev = Some(gid);
}
if let Some(start) = run_start {
out.push(Object::Integer(i64::from(start)));
out.push(Object::Array(run));
}
out
}
fn write_to_unicode(&self, doc: &mut Document) -> Result<ObjectId> {
let mut cmap = String::from(
"/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>\n endcodespacerange\n",
);
let entries: Vec<(u16, &String)> = self.to_unicode.iter().map(|(g, s)| (*g, s)).collect();
for chunk in entries.chunks(100) {
cmap.push_str(&format!("{} beginbfchar\n", chunk.len()));
for (gid, text) in chunk {
let utf16: String = text
.encode_utf16()
.map(|u| format!("{u:04X}"))
.collect::<Vec<_>>()
.join("");
cmap.push_str(&format!("<{gid:04X}> <{utf16}>\n"));
}
cmap.push_str("endbfchar\n");
}
cmap.push_str("endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend\n");
let mut stream = Stream::new(Dictionary::new(), cmap.into_bytes());
let _ = stream.compress();
Ok(doc.add_object(Object::Stream(stream)))
}
}
fn register_page_font(
doc: &mut Document,
page_num: u32,
name: &str,
font_id: ObjectId,
) -> Option<()> {
let pages = doc.get_pages();
let &page_id = pages.get(&page_num)?;
let resources_entry = doc
.get_object(page_id)
.ok()
.and_then(|o| o.as_dict().ok())
.and_then(|d| d.get(b"Resources").ok())
.cloned();
match resources_entry {
Some(Object::Reference(res_id)) => {
let font_entry = doc
.get_object(res_id)
.ok()
.and_then(|o| o.as_dict().ok())
.and_then(|d| d.get(b"Font").ok())
.cloned();
match font_entry {
Some(Object::Reference(font_dict_id)) => {
let dict = doc.get_object_mut(font_dict_id).ok()?.as_dict_mut().ok()?;
dict.set(name, Object::Reference(font_id));
}
Some(Object::Dictionary(mut d)) => {
d.set(name, Object::Reference(font_id));
let res = doc.get_object_mut(res_id).ok()?.as_dict_mut().ok()?;
res.set("Font", Object::Dictionary(d));
}
_ => {
let mut d = Dictionary::new();
d.set(name, Object::Reference(font_id));
let res = doc.get_object_mut(res_id).ok()?.as_dict_mut().ok()?;
res.set("Font", Object::Dictionary(d));
}
}
}
Some(Object::Dictionary(mut res)) => match res.get(b"Font").ok().cloned() {
Some(Object::Reference(font_dict_id)) => {
let dict = doc.get_object_mut(font_dict_id).ok()?.as_dict_mut().ok()?;
dict.set(name, Object::Reference(font_id));
}
Some(Object::Dictionary(mut d)) => {
d.set(name, Object::Reference(font_id));
res.set("Font", Object::Dictionary(d));
let page = doc.get_object_mut(page_id).ok()?.as_dict_mut().ok()?;
page.set("Resources", Object::Dictionary(res));
}
_ => {
let mut d = Dictionary::new();
d.set(name, Object::Reference(font_id));
res.set("Font", Object::Dictionary(d));
let page = doc.get_object_mut(page_id).ok()?.as_dict_mut().ok()?;
page.set("Resources", Object::Dictionary(res));
}
},
_ => {
let mut fonts = Dictionary::new();
fonts.set(name, Object::Reference(font_id));
let mut res = Dictionary::new();
res.set("Font", Object::Dictionary(fonts));
let page = doc.get_object_mut(page_id).ok()?.as_dict_mut().ok()?;
page.set("Resources", Object::Dictionary(res));
}
}
Some(())
}
#[cfg(test)]
mod tests {
use super::*;
fn host_font() -> Option<Vec<u8>> {
for path in [
"/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
"/System/Library/Fonts/Supplemental/Arial.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
] {
if let Ok(data) = std::fs::read(path) {
return Some(data);
}
}
None
}
#[test]
fn rejects_a_non_font() {
let err = UnicodeFont::from_bytes(b"this is not a font".to_vec()).unwrap_err();
assert!(err.to_string().contains("not a usable font program"));
}
#[test]
fn reports_the_characters_a_font_cannot_write() {
let Some(data) = host_font() else { return };
let font = UnicodeFont::from_bytes(data).unwrap();
assert!(font.covers("Hello"));
let missing = font.missing_chars("A\u{E000}B");
assert_eq!(missing, vec!['\u{E000}']);
}
#[test]
fn encodes_two_bytes_per_glyph_and_registers_them() {
let Some(data) = host_font() else { return };
let font = UnicodeFont::from_bytes(data).unwrap();
let mut enc = UnicodeEncoder::new(font);
let bytes = enc.encode("AB").unwrap();
assert_eq!(bytes.len(), 4, "Identity-H is 2 bytes per glyph");
assert!(!enc.is_empty());
let again = enc.encode("A").unwrap();
assert_eq!(&bytes[0..2], &again[..]);
}
#[test]
fn refuses_characters_the_font_lacks_instead_of_writing_blanks() {
let Some(data) = host_font() else { return };
let font = UnicodeFont::from_bytes(data).unwrap();
let mut enc = UnicodeEncoder::new(font);
let err = enc.encode("A\u{E000}").unwrap_err().to_string();
assert!(
err.contains("U+E000"),
"error should name the character: {err}"
);
}
#[test]
fn a_repeated_character_maps_back_to_one_character() {
let Some(data) = host_font() else { return };
let font = UnicodeFont::from_bytes(data).unwrap();
let mut enc = UnicodeEncoder::new(font);
enc.encode("aaa").unwrap();
enc.encode("a").unwrap();
let mapped = enc
.to_unicode
.values()
.find(|s| s.starts_with('a'))
.expect("the glyph for 'a' should be registered");
assert_eq!(
mapped, "a",
"one glyph must map back to exactly one character, got {mapped:?}"
);
}
#[test]
fn a_variable_font_is_aimed_at_regular_not_its_own_default() {
let Some(data) = host_font() else { return };
let font = UnicodeFont::from_bytes(data).unwrap();
if !font.is_variable() {
assert!(font.variations.is_empty());
return;
}
for (axis, value) in &font.variations {
if axis == &WGHT {
assert!(
(*value - REGULAR_WEIGHT).abs() < f32::EPSILON,
"a variable font should be pinned to Regular, got {value}"
);
}
}
}
#[test]
fn an_explicit_variation_overrides_the_automatic_one() {
let Some(data) = host_font() else { return };
let font = UnicodeFont::from_bytes(data)
.unwrap()
.with_variation(b"wght", 700.0);
let weight = font
.variations
.iter()
.find(|(a, _)| a == &WGHT)
.map(|(_, v)| *v);
assert_eq!(weight, Some(700.0), "caller's weight must win");
}
#[test]
fn w_array_coalesces_consecutive_glyphs() {
let Some(data) = host_font() else { return };
let font = UnicodeFont::from_bytes(data).unwrap();
let mut enc = UnicodeEncoder::new(font);
enc.encode("Hello world").unwrap();
let w = enc.build_w_array(|v| v.round() as i64);
assert_eq!(w.len() % 2, 0);
assert!(matches!(w.first(), Some(Object::Integer(_))));
assert!(matches!(w.get(1), Some(Object::Array(_))));
}
#[test]
fn subset_tag_is_six_uppercase_letters_and_stable() {
let Some(data) = host_font() else { return };
let font = UnicodeFont::from_bytes(data).unwrap();
let a = UnicodeEncoder::new(font.clone()).subset_base_font_name();
let b = UnicodeEncoder::new(font).subset_base_font_name();
assert_eq!(a, b, "same font must give the same tag across runs");
let tag = a.split('+').next().unwrap();
assert_eq!(tag.len(), 6);
assert!(tag.chars().all(|c| c.is_ascii_uppercase()));
}
}