pub mod builtin;
pub mod fallback;
pub mod metrics;
pub mod subset;
pub use metrics::{unicode_to_winansi, StandardFontMetrics};
use std::collections::HashMap;
pub struct FontRegistry {
fonts: HashMap<FontKey, FontData>,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct FontKey {
pub family: String,
pub weight: u32,
pub italic: bool,
}
#[derive(Debug, Clone)]
pub enum FontData {
Standard(StandardFont),
Custom {
data: Vec<u8>,
used_glyphs: Vec<u16>,
metrics: Option<CustomFontMetrics>,
},
}
#[derive(Debug, Clone)]
pub struct CustomFontMetrics {
pub units_per_em: u16,
pub advance_widths: HashMap<char, u16>,
pub default_advance: u16,
pub ascender: i16,
pub descender: i16,
pub glyph_ids: HashMap<char, u16>,
}
impl CustomFontMetrics {
pub fn char_width(&self, ch: char, font_size: f64) -> f64 {
let w = self
.advance_widths
.get(&ch)
.copied()
.unwrap_or(self.default_advance);
(w as f64 / self.units_per_em as f64) * font_size
}
pub fn from_font_data(data: &[u8]) -> Option<Self> {
let face = ttf_parser::Face::parse(data, 0).ok()?;
let units_per_em = face.units_per_em();
let ascender = face.ascender();
let descender = face.descender();
let mut advance_widths = HashMap::new();
let mut glyph_ids = HashMap::new();
let mut default_advance = 0u16;
for code in 32u32..=0xFFFF {
if let Some(ch) = char::from_u32(code) {
if let Some(glyph_id) = face.glyph_index(ch) {
let advance = face.glyph_hor_advance(glyph_id).unwrap_or(0);
advance_widths.insert(ch, advance);
glyph_ids.insert(ch, glyph_id.0);
if ch == ' ' {
default_advance = advance;
}
}
}
}
if default_advance == 0 {
default_advance = units_per_em / 2;
}
Some(CustomFontMetrics {
units_per_em,
advance_widths,
default_advance,
ascender,
descender,
glyph_ids,
})
}
}
#[derive(Debug, Clone, Copy)]
pub enum StandardFont {
Helvetica,
HelveticaBold,
HelveticaOblique,
HelveticaBoldOblique,
TimesRoman,
TimesBold,
TimesItalic,
TimesBoldItalic,
Courier,
CourierBold,
CourierOblique,
CourierBoldOblique,
Symbol,
ZapfDingbats,
}
impl FontData {
pub fn has_char(&self, ch: char) -> bool {
match self {
FontData::Custom {
metrics: Some(m), ..
} => m.glyph_ids.contains_key(&ch),
FontData::Custom { metrics: None, .. } => false,
FontData::Standard(_) => {
unicode_to_winansi(ch).is_some() || (ch as u32) >= 32 && (ch as u32) <= 255
}
}
}
}
impl StandardFont {
pub fn pdf_name(&self) -> &'static str {
match self {
Self::Helvetica => "Helvetica",
Self::HelveticaBold => "Helvetica-Bold",
Self::HelveticaOblique => "Helvetica-Oblique",
Self::HelveticaBoldOblique => "Helvetica-BoldOblique",
Self::TimesRoman => "Times-Roman",
Self::TimesBold => "Times-Bold",
Self::TimesItalic => "Times-Italic",
Self::TimesBoldItalic => "Times-BoldItalic",
Self::Courier => "Courier",
Self::CourierBold => "Courier-Bold",
Self::CourierOblique => "Courier-Oblique",
Self::CourierBoldOblique => "Courier-BoldOblique",
Self::Symbol => "Symbol",
Self::ZapfDingbats => "ZapfDingbats",
}
}
}
impl Default for FontRegistry {
fn default() -> Self {
Self::new()
}
}
impl FontRegistry {
pub fn new() -> Self {
let mut fonts = HashMap::new();
let standard_mappings = vec![
(("Helvetica", 400, false), StandardFont::Helvetica),
(("Helvetica", 700, false), StandardFont::HelveticaBold),
(("Helvetica", 400, true), StandardFont::HelveticaOblique),
(("Helvetica", 700, true), StandardFont::HelveticaBoldOblique),
(("Times", 400, false), StandardFont::TimesRoman),
(("Times", 700, false), StandardFont::TimesBold),
(("Times", 400, true), StandardFont::TimesItalic),
(("Times", 700, true), StandardFont::TimesBoldItalic),
(("Courier", 400, false), StandardFont::Courier),
(("Courier", 700, false), StandardFont::CourierBold),
(("Courier", 400, true), StandardFont::CourierOblique),
(("Courier", 700, true), StandardFont::CourierBoldOblique),
];
for ((family, weight, italic), font) in standard_mappings {
fonts.insert(
FontKey {
family: family.to_string(),
weight,
italic,
},
FontData::Standard(font),
);
}
let mut registry = Self { fonts };
builtin::register_builtin_fonts(&mut registry);
registry
}
pub fn resolve(&self, families: &str, weight: u32, italic: bool) -> &FontData {
let snapped_weight = if weight >= 600 { 700 } else { 400 };
for family in families.split(',') {
let family = family.trim().trim_matches('"').trim_matches('\'');
if family.is_empty() {
continue;
}
let key = FontKey {
family: family.to_string(),
weight,
italic,
};
if let Some(font) = self.fonts.get(&key) {
return font;
}
let key = FontKey {
family: family.to_string(),
weight: snapped_weight,
italic,
};
if let Some(font) = self.fonts.get(&key) {
return font;
}
let opposite_weight = if snapped_weight == 700 { 400 } else { 700 };
let key = FontKey {
family: family.to_string(),
weight: opposite_weight,
italic,
};
if let Some(font) = self.fonts.get(&key) {
return font;
}
}
let key = FontKey {
family: "Helvetica".to_string(),
weight: snapped_weight,
italic,
};
self.fonts.get(&key).unwrap_or_else(|| {
self.fonts
.get(&FontKey {
family: "Helvetica".to_string(),
weight: 400,
italic: false,
})
.expect("Helvetica must be registered")
})
}
pub fn resolve_for_char(
&self,
families: &str,
ch: char,
weight: u32,
italic: bool,
) -> (&FontData, String) {
let snapped_weight = if weight >= 600 { 700 } else { 400 };
for family in families.split(',') {
let family = family.trim().trim_matches('"').trim_matches('\'');
if family.is_empty() {
continue;
}
let key = FontKey {
family: family.to_string(),
weight,
italic,
};
if let Some(font) = self.fonts.get(&key) {
if font.has_char(ch) {
return (font, family.to_string());
}
}
let key = FontKey {
family: family.to_string(),
weight: snapped_weight,
italic,
};
if let Some(font) = self.fonts.get(&key) {
if font.has_char(ch) {
return (font, family.to_string());
}
}
let opposite_weight = if snapped_weight == 700 { 400 } else { 700 };
let key = FontKey {
family: family.to_string(),
weight: opposite_weight,
italic,
};
if let Some(font) = self.fonts.get(&key) {
if font.has_char(ch) {
return (font, family.to_string());
}
}
}
let builtin_key = FontKey {
family: "Noto Sans".to_string(),
weight: snapped_weight,
italic: false,
};
if let Some(font) = self.fonts.get(&builtin_key) {
if font.has_char(ch) {
return (font, "Noto Sans".to_string());
}
}
let key = FontKey {
family: "Helvetica".to_string(),
weight: snapped_weight,
italic,
};
let font = self.fonts.get(&key).unwrap_or_else(|| {
self.fonts
.get(&FontKey {
family: "Helvetica".to_string(),
weight: 400,
italic: false,
})
.expect("Helvetica must be registered")
});
(font, "Helvetica".to_string())
}
pub fn register(&mut self, family: &str, weight: u32, italic: bool, data: Vec<u8>) {
let metrics = CustomFontMetrics::from_font_data(&data);
self.fonts.insert(
FontKey {
family: family.to_string(),
weight,
italic,
},
FontData::Custom {
data,
used_glyphs: Vec::new(),
metrics,
},
);
}
pub fn iter(&self) -> impl Iterator<Item = (&FontKey, &FontData)> {
self.fonts.iter()
}
}
pub struct FontContext {
registry: FontRegistry,
sentinel_digit_count: u32,
}
impl Default for FontContext {
fn default() -> Self {
Self::new()
}
}
impl FontContext {
pub fn new() -> Self {
Self {
registry: FontRegistry::new(),
sentinel_digit_count: 2,
}
}
pub fn sentinel_digit_count(&self) -> u32 {
self.sentinel_digit_count
}
pub fn set_sentinel_digit_count(&mut self, count: u32) {
self.sentinel_digit_count = count;
}
pub fn char_width(
&self,
ch: char,
family: &str,
weight: u32,
italic: bool,
font_size: f64,
) -> f64 {
if ch == crate::layout::PAGE_NUMBER_SENTINEL || ch == crate::layout::TOTAL_PAGES_SENTINEL {
return self.char_width('0', family, weight, italic, font_size)
* self.sentinel_digit_count as f64;
}
let font_data = if !family.contains(',') {
let primary = self.registry.resolve(family, weight, italic);
if ch.is_whitespace() || primary.has_char(ch) {
primary
} else {
let (data, _) = self.registry.resolve_for_char(family, ch, weight, italic);
data
}
} else {
let (data, _) = self.registry.resolve_for_char(family, ch, weight, italic);
data
};
match font_data {
FontData::Standard(std_font) => std_font.metrics().char_width(ch, font_size),
FontData::Custom {
metrics: Some(m), ..
} => m.char_width(ch, font_size),
FontData::Custom { metrics: None, .. } => {
StandardFont::Helvetica.metrics().char_width(ch, font_size)
}
}
}
pub fn measure_string(
&self,
text: &str,
family: &str,
weight: u32,
italic: bool,
font_size: f64,
letter_spacing: f64,
) -> f64 {
let mut width = 0.0;
for ch in text.chars() {
width += self.char_width(ch, family, weight, italic, font_size) + letter_spacing;
}
width
}
pub fn resolve(&self, family: &str, weight: u32, italic: bool) -> &FontData {
self.registry.resolve(family, weight, italic)
}
pub fn registry(&self) -> &FontRegistry {
&self.registry
}
pub fn registry_mut(&mut self) -> &mut FontRegistry {
&mut self.registry
}
pub fn font_data(&self, family: &str, weight: u32, italic: bool) -> Option<&[u8]> {
let font_data = self.registry.resolve(family, weight, italic);
match font_data {
FontData::Custom { data, .. } => Some(data),
FontData::Standard(_) => None,
}
}
pub fn units_per_em(&self, family: &str, weight: u32, italic: bool) -> u16 {
let font_data = self.registry.resolve(family, weight, italic);
match font_data {
FontData::Custom {
metrics: Some(m), ..
} => m.units_per_em,
FontData::Custom { metrics: None, .. } => 1000,
FontData::Standard(_) => 1000,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_font_context_helvetica() {
let ctx = FontContext::new();
let w = ctx.char_width(' ', "Helvetica", 400, false, 12.0);
assert!((w - 3.336).abs() < 0.001);
}
#[test]
fn test_font_context_bold_wider() {
let ctx = FontContext::new();
let regular = ctx.char_width('A', "Helvetica", 400, false, 12.0);
let bold = ctx.char_width('A', "Helvetica", 700, false, 12.0);
assert!(bold > regular, "Bold A should be wider than regular A");
}
#[test]
fn test_font_context_measure_string() {
let ctx = FontContext::new();
let w = ctx.measure_string("Hello", "Helvetica", 400, false, 12.0, 0.0);
assert!(w > 0.0);
}
#[test]
fn test_font_context_fallback() {
let ctx = FontContext::new();
let w1 = ctx.char_width('A', "Helvetica", 400, false, 12.0);
let w2 = ctx.char_width('A', "UnknownFont", 400, false, 12.0);
assert!((w1 - w2).abs() < 0.001);
}
#[test]
fn test_font_context_weight_resolution() {
let ctx = FontContext::new();
let w700 = ctx.char_width('A', "Helvetica", 700, false, 12.0);
let w800 = ctx.char_width('A', "Helvetica", 800, false, 12.0);
assert!((w700 - w800).abs() < 0.001);
}
#[test]
fn test_font_fallback_chain_first_match() {
let ctx = FontContext::new();
let w1 = ctx.char_width('A', "Times", 400, false, 12.0);
let w2 = ctx.char_width('A', "Times, Helvetica", 400, false, 12.0);
assert!((w1 - w2).abs() < 0.001, "Should use Times (first in chain)");
}
#[test]
fn test_font_fallback_chain_second_match() {
let ctx = FontContext::new();
let w1 = ctx.char_width('A', "Helvetica", 400, false, 12.0);
let w2 = ctx.char_width('A', "Missing, Helvetica", 400, false, 12.0);
assert!((w1 - w2).abs() < 0.001, "Should fall back to Helvetica");
}
#[test]
fn test_font_fallback_chain_all_missing() {
let ctx = FontContext::new();
let w = ctx.char_width('A', "Missing, AlsoMissing", 400, false, 12.0);
assert!(w > 0.0, "Should still produce a valid width from fallback");
}
#[test]
fn test_font_fallback_chain_quoted_families() {
let ctx = FontContext::new();
let w1 = ctx.char_width('A', "Times", 400, false, 12.0);
let w2 = ctx.char_width('A', "'Times', \"Helvetica\"", 400, false, 12.0);
assert!((w1 - w2).abs() < 0.001, "Should strip quotes and use Times");
}
#[test]
fn test_builtin_noto_sans_registered() {
let registry = FontRegistry::new();
let font = registry.resolve("Noto Sans", 400, false);
assert!(
matches!(font, FontData::Custom { .. }),
"Noto Sans should be registered as a custom font"
);
assert!(
font.has_char('\u{041F}'),
"Noto Sans should have Cyrillic П"
);
assert!(font.has_char('\u{03B1}'), "Noto Sans should have Greek α");
}
#[test]
fn test_builtin_noto_sans_fallback_for_cyrillic() {
let registry = FontRegistry::new();
let (font, family) = registry.resolve_for_char("Helvetica", '\u{041F}', 400, false);
assert_eq!(
family, "Noto Sans",
"Cyrillic should fall back to Noto Sans"
);
assert!(matches!(font, FontData::Custom { .. }));
}
#[test]
fn test_font_fallback_single_family_unchanged() {
let ctx = FontContext::new();
let w1 = ctx.char_width('A', "Courier", 400, false, 12.0);
let w2 = ctx.char_width('A', "Courier", 400, false, 12.0);
assert!(
(w1 - w2).abs() < 0.001,
"Single family should work as before"
);
}
}