use std::collections::BTreeMap;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum FontStyle {
Normal,
Italic,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FontSource {
Bundled,
Project,
Local,
}
#[derive(Clone)]
pub struct FontData {
pub id: String,
pub bytes: Arc<[u8]>,
pub index: u32,
pub source: FontSource,
}
impl std::fmt::Debug for FontData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FontData")
.field("id", &self.id)
.field("bytes_len", &self.bytes.len())
.field("index", &self.index)
.field("source", &self.source)
.finish()
}
}
pub trait FontProvider {
#[must_use]
fn resolve(&self, families: &[String], weight: u16, style: FontStyle) -> Option<FontData>;
#[must_use]
fn by_id(&self, id: &str) -> Option<FontData>;
#[must_use]
fn all_faces(&self) -> Vec<FontData>;
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct FaceKey {
family_lower: String,
weight: u16,
style: FontStyle,
}
pub struct BytesFontProvider {
by_key: BTreeMap<FaceKey, FontData>,
by_id: BTreeMap<String, FontData>,
}
impl std::fmt::Debug for BytesFontProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let ids: Vec<&str> = self.by_id.keys().map(String::as_str).collect();
f.debug_struct("BytesFontProvider")
.field("registered_faces", &ids)
.finish()
}
}
impl BytesFontProvider {
#[must_use]
pub fn new() -> Self {
Self {
by_key: BTreeMap::new(),
by_id: BTreeMap::new(),
}
}
pub fn register(
&mut self,
family: &str,
weight: u16,
style: FontStyle,
bytes: Arc<[u8]>,
index: u32,
source: FontSource,
) -> String {
let family_lower = family.to_lowercase();
let family_kebab = family_lower.replace(' ', "-");
let style_str = match style {
FontStyle::Normal => "normal",
FontStyle::Italic => "italic",
};
let base_id = format!("{family_kebab}-{weight}-{style_str}");
let key = FaceKey {
family_lower,
weight,
style,
};
let id = match self.by_key.get(&key) {
Some(existing) => existing.id.clone(),
None => {
let mut candidate = base_id.clone();
let mut n = 2u32;
while self.by_id.contains_key(&candidate) {
candidate = format!("{base_id}-{n}");
n += 1;
}
candidate
}
};
let data = FontData {
id: id.clone(),
bytes,
index,
source,
};
self.by_key.insert(key, data.clone());
self.by_id.insert(id.clone(), data);
id
}
#[must_use]
pub fn available_families(&self) -> Vec<String> {
let mut families: Vec<String> =
self.by_key.keys().map(|k| k.family_lower.clone()).collect();
families.dedup();
families
}
}
impl Default for BytesFontProvider {
fn default() -> Self {
Self::new()
}
}
impl FontProvider for BytesFontProvider {
fn resolve(&self, families: &[String], weight: u16, style: FontStyle) -> Option<FontData> {
for family in families {
let family_lower = family.to_lowercase();
let exact_key = FaceKey {
family_lower: family_lower.clone(),
weight,
style,
};
if let Some(data) = self.by_key.get(&exact_key) {
return Some(data.clone());
}
let fallback = self
.by_key
.range(
FaceKey {
family_lower: family_lower.clone(),
weight: 0,
style: FontStyle::Normal,
}..,
)
.find(|(k, _)| k.family_lower == family_lower)
.map(|(_, v)| v.clone());
if fallback.is_some() {
return fallback;
}
}
None
}
fn by_id(&self, id: &str) -> Option<FontData> {
self.by_id.get(id).cloned()
}
fn all_faces(&self) -> Vec<FontData> {
self.by_id.values().cloned().collect()
}
}
#[must_use]
pub fn default_provider() -> BytesFontProvider {
let sans: Arc<[u8]> = Arc::from(super::embedded::NOTO_SANS_REGULAR);
let sans_bold: Arc<[u8]> = Arc::from(super::embedded::NOTO_SANS_BOLD);
let sans_italic: Arc<[u8]> = Arc::from(super::embedded::NOTO_SANS_ITALIC);
let sans_bold_italic: Arc<[u8]> = Arc::from(super::embedded::NOTO_SANS_BOLD_ITALIC);
let serif: Arc<[u8]> = Arc::from(super::embedded::NOTO_SERIF_REGULAR);
let serif_bold: Arc<[u8]> = Arc::from(super::embedded::NOTO_SERIF_BOLD);
let serif_italic: Arc<[u8]> = Arc::from(super::embedded::NOTO_SERIF_ITALIC);
let serif_bold_italic: Arc<[u8]> = Arc::from(super::embedded::NOTO_SERIF_BOLD_ITALIC);
let mono: Arc<[u8]> = Arc::from(super::embedded::NOTO_SANS_MONO_REGULAR);
let mono_bold: Arc<[u8]> = Arc::from(super::embedded::NOTO_SANS_MONO_BOLD);
let mut provider = BytesFontProvider::new();
let b = FontSource::Bundled;
provider.register("Noto Sans", 400, FontStyle::Normal, sans, 0, b);
provider.register("Noto Sans", 700, FontStyle::Normal, sans_bold, 0, b);
provider.register("Noto Sans", 400, FontStyle::Italic, sans_italic, 0, b);
provider.register("Noto Sans", 700, FontStyle::Italic, sans_bold_italic, 0, b);
provider.register("Noto Serif", 400, FontStyle::Normal, serif, 0, b);
provider.register("Noto Serif", 700, FontStyle::Normal, serif_bold, 0, b);
provider.register("Noto Serif", 400, FontStyle::Italic, serif_italic, 0, b);
provider.register(
"Noto Serif",
700,
FontStyle::Italic,
serif_bold_italic,
0,
b,
);
provider.register("Noto Sans Mono", 400, FontStyle::Normal, mono, 0, b);
provider.register("Noto Sans Mono", 700, FontStyle::Normal, mono_bold, 0, b);
provider
}
#[cfg(test)]
mod tests {
use super::*;
fn is_valid_tt_header(bytes: &[u8]) -> bool {
bytes.len() > 1000 && bytes.starts_with(&[0x00, 0x01, 0x00, 0x00])
}
#[test]
fn default_provider_resolves_noto_sans() {
let p = default_provider();
let result = p.resolve(&["Noto Sans".to_string()], 400, FontStyle::Normal);
assert!(result.is_some(), "expected Some for Noto Sans 400 Normal");
let data = result.unwrap();
assert!(
is_valid_tt_header(&data.bytes),
"expected TrueType header and len > 1000, got len={}",
data.bytes.len()
);
}
#[test]
fn default_provider_resolves_noto_serif_matrix() {
let p = default_provider();
for (weight, style) in [
(400, FontStyle::Normal),
(700, FontStyle::Normal),
(400, FontStyle::Italic),
(700, FontStyle::Italic),
] {
let result = p.resolve(&["Noto Serif".to_string()], weight, style);
assert!(
result.is_some(),
"expected Some for Noto Serif {weight} {style:?}"
);
let data = result.unwrap();
assert_eq!(data.source, FontSource::Bundled, "serif must be bundled");
assert!(
is_valid_tt_header(&data.bytes),
"expected TrueType header for Noto Serif {weight} {style:?}"
);
}
}
#[test]
fn default_provider_resolves_noto_sans_mono() {
let p = default_provider();
let result = p.resolve(&["Noto Sans Mono".to_string()], 400, FontStyle::Normal);
assert!(
result.is_some(),
"expected Some for Noto Sans Mono 400 Normal"
);
let data = result.unwrap();
assert!(
is_valid_tt_header(&data.bytes),
"expected TrueType header and len > 1000, got len={}",
data.bytes.len()
);
assert!(
data.id.contains("noto-sans-mono"),
"id should contain noto-sans-mono, got {}",
data.id
);
}
#[test]
fn default_provider_distinguishes_sans_and_mono() {
let p = default_provider();
let sans = p
.resolve(&["Noto Sans".to_string()], 400, FontStyle::Normal)
.expect("sans resolves");
let mono = p
.resolve(&["Noto Sans Mono".to_string()], 400, FontStyle::Normal)
.expect("mono resolves");
assert_ne!(sans.id, mono.id, "sans and mono must have distinct ids");
assert_ne!(
sans.bytes.len(),
mono.bytes.len(),
"sans and mono must be different font files"
);
}
#[test]
fn case_insensitive_family_lookup() {
let p = default_provider();
let lower = p.resolve(&["noto sans".to_string()], 400, FontStyle::Normal);
let mixed = p.resolve(&["Noto Sans".to_string()], 400, FontStyle::Normal);
assert!(lower.is_some(), "lowercase family should resolve");
assert!(mixed.is_some(), "mixed-case family should resolve");
assert_eq!(lower.unwrap().id, mixed.unwrap().id);
}
#[test]
fn weight_fallback_resolves_unregistered_weight() {
let p = default_provider();
let result = p.resolve(&["Noto Sans".to_string()], 900, FontStyle::Normal);
assert!(
result.is_some(),
"weight 900 should fall back to a registered face"
);
let data = result.unwrap();
assert!(data.id.contains("noto-sans"), "id should contain noto-sans");
}
#[test]
fn bold_italic_resolves_distinct_combined_face() {
let p = default_provider();
let bold = p
.resolve(&["Noto Sans".to_string()], 700, FontStyle::Normal)
.expect("bold resolves");
let italic = p
.resolve(&["Noto Sans".to_string()], 400, FontStyle::Italic)
.expect("italic resolves");
let bold_italic = p
.resolve(&["Noto Sans".to_string()], 700, FontStyle::Italic)
.expect("bold-italic resolves");
assert!(
bold_italic.id.contains("700") && bold_italic.id.contains("italic"),
"bold-italic id should encode both 700 and italic, got {}",
bold_italic.id
);
assert_ne!(bold_italic.id, bold.id, "must differ from bold-upright");
assert_ne!(bold_italic.id, italic.id, "must differ from regular-italic");
}
#[test]
fn italic_style_resolves_distinct_italic_face() {
let p = default_provider();
let normal = p
.resolve(&["Noto Sans".to_string()], 400, FontStyle::Normal)
.expect("normal resolves");
let italic = p
.resolve(&["Noto Sans".to_string()], 400, FontStyle::Italic)
.expect("italic resolves");
assert!(
italic.id.contains("italic"),
"italic id should encode the italic style, got {}",
italic.id
);
assert_ne!(
normal.id, italic.id,
"normal and italic must have distinct ids"
);
assert_ne!(
normal.bytes.len(),
italic.bytes.len(),
"normal and italic must be different font files"
);
}
#[test]
fn bold_weight_resolves_distinct_bold_face() {
let p = default_provider();
let regular = p
.resolve(&["Noto Sans".to_string()], 400, FontStyle::Normal)
.expect("regular resolves");
let bold = p
.resolve(&["Noto Sans".to_string()], 700, FontStyle::Normal)
.expect("bold resolves");
assert!(
bold.id.contains("noto-sans-700"),
"bold id should encode weight 700, got {}",
bold.id
);
assert_ne!(
regular.id, bold.id,
"regular and bold must have distinct ids"
);
assert_ne!(
regular.bytes.len(),
bold.bytes.len(),
"regular and bold must be different font files"
);
}
#[test]
fn mono_bold_weight_resolves_distinct_bold_face() {
let p = default_provider();
let mono_regular = p
.resolve(&["Noto Sans Mono".to_string()], 400, FontStyle::Normal)
.expect("mono regular resolves");
let mono_bold = p
.resolve(&["Noto Sans Mono".to_string()], 700, FontStyle::Normal)
.expect("mono bold resolves");
assert!(
mono_bold.id.contains("noto-sans-mono-700"),
"mono bold id should encode weight 700, got {}",
mono_bold.id
);
assert_ne!(
mono_regular.id, mono_bold.id,
"mono regular and mono bold must have distinct ids"
);
assert_ne!(
mono_regular.bytes.len(),
mono_bold.bytes.len(),
"mono regular and mono bold must be different font files"
);
}
#[test]
fn unknown_family_returns_none() {
let p = default_provider();
let result = p.resolve(&["Nonexistent".to_string()], 400, FontStyle::Normal);
assert!(result.is_none(), "unknown family must return None");
}
#[test]
fn by_id_roundtrip() {
let p = default_provider();
let resolved = p
.resolve(&["Noto Sans".to_string()], 400, FontStyle::Normal)
.expect("should resolve");
let by_id = p
.by_id(&resolved.id)
.expect("by_id should return same face");
assert_eq!(resolved.id, by_id.id);
assert_eq!(resolved.bytes.len(), by_id.bytes.len());
}
#[test]
fn by_id_unknown_returns_none() {
let p = default_provider();
assert!(p.by_id("no-such-font-0-normal").is_none());
}
#[test]
fn manual_register_and_resolve() {
let mut p = BytesFontProvider::new();
let dummy_bytes: Arc<[u8]> = Arc::from(vec![0u8; 64].as_slice());
let id = p.register(
"Test Family",
400,
FontStyle::Normal,
dummy_bytes.clone(),
0,
FontSource::Project,
);
assert_eq!(id, "test-family-400-normal");
let resolved = p.resolve(&["Test Family".to_string()], 400, FontStyle::Normal);
assert!(resolved.is_some());
assert_eq!(resolved.unwrap().id, "test-family-400-normal");
}
#[test]
fn stable_id_format() {
let mut p = BytesFontProvider::new();
let bytes: Arc<[u8]> = Arc::from(vec![0u8; 4].as_slice());
let id = p.register(
"My Font",
700,
FontStyle::Italic,
bytes,
0,
FontSource::Local,
);
assert_eq!(id, "my-font-700-italic");
}
#[test]
fn re_registering_same_face_reuses_id() {
let mut p = BytesFontProvider::new();
let bytes: Arc<[u8]> = Arc::from(vec![1u8; 8].as_slice());
let id1 = p.register(
"Inter",
400,
FontStyle::Normal,
bytes.clone(),
0,
FontSource::Project,
);
let id2 = p.register(
"Inter",
400,
FontStyle::Normal,
bytes,
0,
FontSource::Project,
);
assert_eq!(id1, id2, "same face re-registration keeps a stable id");
}
#[test]
fn kebab_colliding_families_get_distinct_ids() {
let mut p = BytesFontProvider::new();
let a: Arc<[u8]> = Arc::from(vec![0xAAu8; 4].as_slice());
let b: Arc<[u8]> = Arc::from(vec![0xBBu8; 4].as_slice());
let id_a = p.register("My Font", 400, FontStyle::Normal, a, 0, FontSource::Local);
let id_b = p.register("my-font", 400, FontStyle::Normal, b, 0, FontSource::Local);
assert_eq!(id_a, "my-font-400-normal");
assert_ne!(id_a, id_b, "colliding families must not share an id");
assert_eq!(p.by_id(&id_a).unwrap().bytes[0], 0xAA);
assert_eq!(p.by_id(&id_b).unwrap().bytes[0], 0xBB);
}
#[test]
fn resolve_carries_registered_source() {
let mut p = BytesFontProvider::new();
let bytes: Arc<[u8]> = Arc::from(vec![0u8; 8].as_slice());
p.register(
"Local Face",
400,
FontStyle::Normal,
bytes,
0,
FontSource::Local,
);
let local = p
.resolve(&["Local Face".to_string()], 400, FontStyle::Normal)
.expect("local face resolves");
assert_eq!(local.source, FontSource::Local);
let bundled = default_provider()
.resolve(&["Noto Sans".to_string()], 400, FontStyle::Normal)
.expect("bundled face resolves");
assert_eq!(bundled.source, FontSource::Bundled);
}
#[test]
fn default_provider_faces_are_all_bundled() {
let p = default_provider();
for face in p.all_faces() {
assert_eq!(
face.source,
FontSource::Bundled,
"default provider face {} must be Bundled",
face.id
);
}
}
}