use std::collections::BTreeMap;
use std::path::PathBuf;
use printpdf::{BuiltinFont, FontId, PdfDocument, PdfFontHandle};
use ttf_parser::Face;
use super::ir::{RunFlags, VariantUsage};
use crate::fonts::{FontConfig, FontSource, find_system_font};
#[derive(Debug, Clone, Copy)]
pub enum FontVariant {
HelveticaRegular,
HelveticaBold,
HelveticaItalic,
HelveticaBoldItalic,
CourierRegular,
CourierBold,
CourierItalic,
CourierBoldItalic,
}
impl FontVariant {
pub fn for_flags(flags: RunFlags) -> Self {
match (flags.monospace, flags.bold, flags.italic) {
(true, true, true) => FontVariant::CourierBoldItalic,
(true, true, false) => FontVariant::CourierBold,
(true, false, true) => FontVariant::CourierItalic,
(true, false, false) => FontVariant::CourierRegular,
(false, true, true) => FontVariant::HelveticaBoldItalic,
(false, true, false) => FontVariant::HelveticaBold,
(false, false, true) => FontVariant::HelveticaItalic,
(false, false, false) => FontVariant::HelveticaRegular,
}
}
pub fn builtin(self) -> BuiltinFont {
match self {
FontVariant::HelveticaRegular => BuiltinFont::Helvetica,
FontVariant::HelveticaBold => BuiltinFont::HelveticaBold,
FontVariant::HelveticaItalic => BuiltinFont::HelveticaOblique,
FontVariant::HelveticaBoldItalic => BuiltinFont::HelveticaBoldOblique,
FontVariant::CourierRegular => BuiltinFont::Courier,
FontVariant::CourierBold => BuiltinFont::CourierBold,
FontVariant::CourierItalic => BuiltinFont::CourierOblique,
FontVariant::CourierBoldItalic => BuiltinFont::CourierBoldOblique,
}
}
}
pub struct VariantMetrics {
pub units_per_em: u16,
pub unscaled_widths: Box<[u16; 256]>,
pub fallback_width: u16,
}
impl VariantMetrics {
fn load(variant: FontVariant) -> Self {
let subset = variant.builtin().get_subset_font();
let face = Face::parse(&subset.bytes, 0).expect("built-in subset font must parse");
let units_per_em = face.units_per_em();
let mut widths = Box::new([0u16; 256]);
for (i, slot) in widths.iter_mut().enumerate() {
let ch = char::from_u32(i as u32).unwrap_or('\0');
*slot = face
.glyph_index(ch)
.and_then(|gid| face.glyph_hor_advance(gid))
.unwrap_or(0);
}
backfill_afm_widths(variant, units_per_em, &mut widths);
let (sum, count) = widths.iter().fold((0u64, 0u64), |(s, c), w| {
if *w > 0 {
(s + *w as u64, c + 1)
} else {
(s, c)
}
});
let fallback_width = if count > 0 {
(sum / count) as u16
} else {
500
};
VariantMetrics {
units_per_em,
unscaled_widths: widths,
fallback_width,
}
}
#[allow(dead_code)]
pub fn units_per_em(&self) -> u16 {
self.units_per_em
}
pub fn measure(&self, text: &str, font_size_pt: f32) -> f32 {
let mut unscaled: u64 = 0;
for c in text.chars() {
let w = if (c as u32) < 256 {
self.unscaled_widths[c as usize]
} else {
0
};
let w = if w == 0 { self.fallback_width } else { w };
unscaled += w as u64;
}
unscaled as f32 * font_size_pt / self.units_per_em as f32
}
}
pub struct FontMetricsCache {
helvetica_regular: VariantMetrics,
helvetica_bold: VariantMetrics,
helvetica_italic: VariantMetrics,
helvetica_bold_italic: VariantMetrics,
courier_regular: VariantMetrics,
courier_bold: VariantMetrics,
courier_italic: VariantMetrics,
courier_bold_italic: VariantMetrics,
}
impl FontMetricsCache {
pub fn new() -> Self {
Self {
helvetica_regular: VariantMetrics::load(FontVariant::HelveticaRegular),
helvetica_bold: VariantMetrics::load(FontVariant::HelveticaBold),
helvetica_italic: VariantMetrics::load(FontVariant::HelveticaItalic),
helvetica_bold_italic: VariantMetrics::load(FontVariant::HelveticaBoldItalic),
courier_regular: VariantMetrics::load(FontVariant::CourierRegular),
courier_bold: VariantMetrics::load(FontVariant::CourierBold),
courier_italic: VariantMetrics::load(FontVariant::CourierItalic),
courier_bold_italic: VariantMetrics::load(FontVariant::CourierBoldItalic),
}
}
pub fn for_variant(&self, v: FontVariant) -> &VariantMetrics {
match v {
FontVariant::HelveticaRegular => &self.helvetica_regular,
FontVariant::HelveticaBold => &self.helvetica_bold,
FontVariant::HelveticaItalic => &self.helvetica_italic,
FontVariant::HelveticaBoldItalic => &self.helvetica_bold_italic,
FontVariant::CourierRegular => &self.courier_regular,
FontVariant::CourierBold => &self.courier_bold,
FontVariant::CourierItalic => &self.courier_italic,
FontVariant::CourierBoldItalic => &self.courier_bold_italic,
}
}
}
pub struct ExternalFont {
pub font_id: FontId,
units_per_em: u16,
advance_by_codepoint: BTreeMap<u32, u16>,
fallback_advance: u16,
}
impl ExternalFont {
pub fn measure(&self, text: &str, font_size_pt: f32) -> f32 {
let mut unscaled: u64 = 0;
for c in text.chars() {
let w = self
.advance_by_codepoint
.get(&(c as u32))
.copied()
.unwrap_or(self.fallback_advance);
unscaled += w as u64;
}
unscaled as f32 * font_size_pt / self.units_per_em as f32
}
}
pub struct FontSet {
pub builtin: FontMetricsCache,
pub external_body: ExternalFamily,
pub external_code: ExternalFamily,
}
#[derive(Default)]
pub struct ExternalFamily {
pub regular: Option<ExternalFont>,
pub bold: Option<ExternalFont>,
pub italic: Option<ExternalFont>,
pub bold_italic: Option<ExternalFont>,
}
impl ExternalFamily {
pub fn pick(&self, flags: RunFlags) -> Option<&ExternalFont> {
match (flags.bold, flags.italic) {
(true, true) => self
.bold_italic
.as_ref()
.or(self.bold.as_ref())
.or(self.italic.as_ref())
.or(self.regular.as_ref()),
(true, false) => self.bold.as_ref().or(self.regular.as_ref()),
(false, true) => self.italic.as_ref().or(self.regular.as_ref()),
(false, false) => self.regular.as_ref(),
}
}
pub fn is_loaded(&self) -> bool {
self.regular.is_some()
|| self.bold.is_some()
|| self.italic.is_some()
|| self.bold_italic.is_some()
}
}
pub enum FontResolution<'a> {
Builtin {
handle: PdfFontHandle,
metrics: &'a VariantMetrics,
},
External {
handle: PdfFontHandle,
font: &'a ExternalFont,
},
}
impl FontSet {
pub fn load(
font_config: Option<&FontConfig>,
used_codepoints: &[char],
usage: VariantUsage,
doc: &mut PdfDocument,
) -> Self {
let builtin = FontMetricsCache::new();
let body_variants = BodyVariantNeed {
bold: usage.body_bold || usage.body_bold_italic,
italic: usage.body_italic || usage.body_bold_italic,
bold_italic: usage.body_bold_italic,
};
let code_variants = BodyVariantNeed {
bold: usage.mono_bold || usage.mono_bold_italic,
italic: usage.mono_italic || usage.mono_bold_italic,
bold_italic: usage.mono_bold_italic,
};
let external_body = font_config
.and_then(|c| load_external_family(default_source(c), used_codepoints, body_variants, doc))
.unwrap_or_default();
let user_code_src = font_config.and_then(code_source);
let code_src = match user_code_src {
Some(src) => Some(src),
None if external_body.is_loaded() => default_monospace_source(),
None => None,
};
let external_code = load_external_family(code_src, used_codepoints, code_variants, doc)
.unwrap_or_default();
Self {
builtin,
external_body,
external_code,
}
}
pub fn resolve(&self, flags: RunFlags) -> FontResolution<'_> {
if flags.monospace {
if let Some(ext) = self.external_code.pick(flags) {
return FontResolution::External {
handle: PdfFontHandle::External(ext.font_id.clone()),
font: ext,
};
}
} else if let Some(ext) = self.external_body.pick(flags) {
return FontResolution::External {
handle: PdfFontHandle::External(ext.font_id.clone()),
font: ext,
};
}
let variant = FontVariant::for_flags(flags);
FontResolution::Builtin {
handle: PdfFontHandle::Builtin(variant.builtin()),
metrics: self.builtin.for_variant(variant),
}
}
pub fn measure(&self, flags: RunFlags, text: &str, size_pt: f32) -> f32 {
match self.resolve(flags) {
FontResolution::Builtin { metrics, .. } => metrics.measure(text, size_pt),
FontResolution::External { font, .. } => font.measure(text, size_pt),
}
}
pub fn handle(&self, flags: RunFlags) -> PdfFontHandle {
match self.resolve(flags) {
FontResolution::Builtin { handle, .. } | FontResolution::External { handle, .. } => {
handle
}
}
}
pub fn needs_transliteration(&self, flags: RunFlags) -> bool {
matches!(self.resolve(flags), FontResolution::Builtin { .. })
}
}
fn default_source(c: &FontConfig) -> Option<FontSource> {
if let Some(src) = c.default_font_source.clone() {
return Some(src);
}
c.default_font.as_deref().map(name_to_external_source)
}
fn code_source(c: &FontConfig) -> Option<FontSource> {
if let Some(src) = c.code_font_source.clone() {
return Some(src);
}
c.code_font.as_deref().map(name_to_external_source)
}
fn default_monospace_source() -> Option<FontSource> {
#[cfg(target_os = "macos")]
const CANDIDATES: &[&str] = &["Menlo", "Monaco", "Courier New"];
#[cfg(target_os = "windows")]
const CANDIDATES: &[&str] = &["Consolas", "Cascadia Code", "Courier New", "Lucida Console"];
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
const CANDIDATES: &[&str] = &[
"DejaVu Sans Mono",
"Liberation Mono",
"Noto Sans Mono",
"Ubuntu Mono",
];
for name in CANDIDATES {
if find_system_font(name).is_some() {
return Some(FontSource::System((*name).to_string()));
}
}
None
}
fn name_to_external_source(name: &str) -> FontSource {
if name.contains('/')
|| name.contains('\\')
|| name.ends_with(".ttf")
|| name.ends_with(".otf")
{
return FontSource::File(name.into());
}
FontSource::System(name.to_string())
}
fn resolve_regular(source: FontSource) -> Option<(Option<PathBuf>, Vec<u8>)> {
match source {
FontSource::Builtin(_) => None,
FontSource::Bytes(b) => Some((None, b.to_vec())),
FontSource::File(path) => {
let bytes = read_font_file(&path)?;
Some((Some(path), bytes))
}
FontSource::System(name) => {
let path = find_system_font(&name).or_else(|| {
log::warn!("could not locate system font {:?}", name);
None
})?;
let bytes = read_font_file(&path)?;
Some((Some(path), bytes))
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct BodyVariantNeed {
pub bold: bool,
pub italic: bool,
pub bold_italic: bool,
}
fn load_external_family(
source: Option<FontSource>,
used_codepoints: &[char],
need: BodyVariantNeed,
doc: &mut PdfDocument,
) -> Option<ExternalFamily> {
let source = source?;
let (anchor_path, regular_bytes) = resolve_regular(source)?;
let regular = parse_and_register(regular_bytes, "regular", used_codepoints, doc)?;
let mut family = ExternalFamily {
regular: Some(regular),
..ExternalFamily::default()
};
if let Some(path) = anchor_path {
let candidates: &[(VariantKind, &[&str], bool)] = &[
(VariantKind::Bold, &["Bold"], need.bold),
(VariantKind::Italic, &["Italic", "Oblique"], need.italic),
(
VariantKind::BoldItalic,
&["Bold Italic", "BoldItalic", "Bold-Italic", "BoldOblique"],
need.bold_italic,
),
];
for (kind, names, wanted) in candidates {
if !wanted {
continue;
}
if let Some(variant_path) = find_variant_path(&path, names) {
if let Some(bytes) = read_font_file(&variant_path) {
if let Some(parsed) =
parse_and_register(bytes, kind.label(), used_codepoints, doc)
{
match kind {
VariantKind::Bold => family.bold = Some(parsed),
VariantKind::Italic => family.italic = Some(parsed),
VariantKind::BoldItalic => family.bold_italic = Some(parsed),
}
}
}
}
}
}
if family.is_loaded() { Some(family) } else { None }
}
#[derive(Clone, Copy)]
enum VariantKind {
Bold,
Italic,
BoldItalic,
}
impl VariantKind {
fn label(self) -> &'static str {
match self {
VariantKind::Bold => "bold",
VariantKind::Italic => "italic",
VariantKind::BoldItalic => "bold-italic",
}
}
}
fn find_variant_path(anchor: &std::path::Path, variant_names: &[&str]) -> Option<PathBuf> {
let parent = anchor.parent()?;
let stem = anchor.file_stem()?.to_string_lossy().to_string();
for variant in variant_names {
for sep in [" ", "-", ""] {
for ext in ["ttf", "otf"] {
let candidate = parent.join(format!("{}{}{}.{}", stem, sep, variant, ext));
if candidate.exists() {
return Some(candidate);
}
}
}
}
None
}
const RENDERER_INJECTED_CHARS: &[char] = &[
'\u{2022}', '[', ']', 'x', ' ', '.',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'(', ')', ':', '-',
];
fn parse_and_register(
bytes: Vec<u8>,
label: &str,
used_codepoints: &[char],
doc: &mut PdfDocument,
) -> Option<ExternalFont> {
let face = match Face::parse(&bytes, 0) {
Ok(f) => f,
Err(e) => {
log::warn!("could not parse {} font face: {}", label, e);
return None;
}
};
let units_per_em = face.units_per_em();
let mut codepoints: Vec<char> = used_codepoints.to_vec();
codepoints.extend_from_slice(RENDERER_INJECTED_CHARS);
codepoints.sort();
codepoints.dedup();
let used_codepoints = &codepoints[..];
let mut codepoint_to_orig_gid: BTreeMap<u32, u16> = BTreeMap::new();
let mut orig_gid_advance: BTreeMap<u16, u16> = BTreeMap::new();
for &ch in used_codepoints {
let cp = ch as u32;
if let Some(gid) = face.glyph_index(ch) {
if let Some(w) = face.glyph_hor_advance(gid) {
codepoint_to_orig_gid.insert(cp, gid.0);
orig_gid_advance.insert(gid.0, w);
}
}
}
let orig_gids: Vec<u16> = orig_gid_advance.keys().copied().collect();
let remapper = subsetter::GlyphRemapper::new_from_glyphs_sorted(&orig_gids);
let (subset_bytes, gid_remap): (Vec<u8>, Box<dyn Fn(u16) -> u16>) =
match subsetter::subset(&bytes, 0, &remapper) {
Ok(b) => (
b,
Box::new(move |old| remapper.get(old).unwrap_or(0)),
),
Err(e) => {
log::warn!(
"could not subset {} font: {:?}; embedding full font instead",
label,
e
);
(bytes.clone(), Box::new(|old| old))
}
};
let mut advance_by_codepoint: BTreeMap<u32, u16> = BTreeMap::new();
let mut codepoint_to_glyph: BTreeMap<u32, u16> = BTreeMap::new();
let mut glyph_widths: BTreeMap<u16, u16> = BTreeMap::new();
let mut sum: u64 = 0;
let mut count: u64 = 0;
for (cp, orig_gid) in &codepoint_to_orig_gid {
let new_gid = gid_remap(*orig_gid);
let w = orig_gid_advance.get(orig_gid).copied().unwrap_or(0);
codepoint_to_glyph.insert(*cp, new_gid);
glyph_widths.insert(new_gid, w);
advance_by_codepoint.insert(*cp, w);
if w > 0 {
sum += w as u64;
count += 1;
}
}
let fallback_advance = if count > 0 {
(sum / count) as u16
} else {
units_per_em / 2
};
let ascent = normalize_to_1000_em(face.ascender(), units_per_em);
let descent = normalize_to_1000_em(face.descender(), units_per_em);
let parsed = printpdf::ParsedFont::with_glyph_data(
subset_bytes,
0,
None,
codepoint_to_glyph,
glyph_widths,
units_per_em,
printpdf::FontMetrics { ascent, descent },
);
let font_id = doc.add_font(&parsed);
Some(ExternalFont {
font_id,
units_per_em,
advance_by_codepoint,
fallback_advance,
})
}
fn normalize_to_1000_em(value: i16, units_per_em: u16) -> i16 {
let upem = i32::from(units_per_em).max(1);
let scaled = i32::from(value) * 1000 / upem;
scaled.clamp(i32::from(i16::MIN), i32::from(i16::MAX)) as i16
}
fn read_font_file(path: &std::path::Path) -> Option<Vec<u8>> {
std::fs::read(path)
.map_err(|e| log::warn!("could not read font {:?}: {}", path, e))
.ok()
}
fn backfill_afm_widths(variant: FontVariant, units_per_em: u16, widths: &mut [u16; 256]) {
let space_per_1000 = match variant {
FontVariant::HelveticaRegular
| FontVariant::HelveticaBold
| FontVariant::HelveticaItalic
| FontVariant::HelveticaBoldItalic => 278u32,
FontVariant::CourierRegular
| FontVariant::CourierBold
| FontVariant::CourierItalic
| FontVariant::CourierBoldItalic => 600u32,
};
if widths[b' ' as usize] == 0 {
let scaled = space_per_1000 * units_per_em as u32 / 1000;
widths[b' ' as usize] = scaled.min(u16::MAX as u32) as u16;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn measures_helvetica_width_monotonic_in_text_length() {
let cache = FontMetricsCache::new();
let m = cache.for_variant(FontVariant::HelveticaRegular);
let a = m.measure("a", 12.0);
let aaa = m.measure("aaa", 12.0);
assert!(aaa > a * 2.5);
assert!(a > 1.0 && a < 12.0, "unexpected width: {}", a);
}
#[test]
fn courier_is_monospace() {
let cache = FontMetricsCache::new();
let m = cache.for_variant(FontVariant::CourierRegular);
let i = m.measure("i", 12.0);
let w = m.measure("w", 12.0);
assert!((i - w).abs() < 0.5, "courier should be monospace");
}
#[test]
fn normalize_to_1000_em_is_font_agnostic() {
assert_eq!(normalize_to_1000_em(1878, 2048), 916);
assert_eq!(normalize_to_1000_em(-449, 2048), -219);
assert_eq!(normalize_to_1000_em(800, 1000), 800);
assert_eq!(normalize_to_1000_em(-200, 1000), -200);
assert_eq!(normalize_to_1000_em(819, 1024), 799);
assert_eq!(normalize_to_1000_em(3000, 4096), 732);
assert_eq!(normalize_to_1000_em(1000, 0), i16::MAX);
assert_eq!(normalize_to_1000_em(i16::MAX, 1), i16::MAX);
assert_eq!(normalize_to_1000_em(i16::MIN, 1), i16::MIN);
}
#[test]
fn default_monospace_source_resolves_on_supported_oses() {
let src = default_monospace_source();
if cfg!(any(target_os = "macos", target_os = "windows")) {
assert!(
src.is_some(),
"expected a system monospace fallback on macOS/Windows"
);
}
}
#[test]
fn for_flags_routes_correctly() {
assert!(matches!(
FontVariant::for_flags(RunFlags::default()),
FontVariant::HelveticaRegular
));
assert!(matches!(
FontVariant::for_flags(RunFlags::default().with_bold()),
FontVariant::HelveticaBold
));
assert!(matches!(
FontVariant::for_flags(RunFlags::default().with_monospace().with_bold()),
FontVariant::CourierBold
));
}
}