use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use sha2::{Digest, Sha256};
use skrifa::{instance::Size, FontRef, MetadataProvider};
use crate::{ErrorCode, FileMakerError, Result};
#[derive(Clone, Debug)]
pub struct FontAsset {
pub name: String,
pub bytes: Arc<[u8]>,
pub face_index: u32,
pub digest: [u8; 32],
}
impl FontAsset {
pub fn new(name: impl Into<String>, bytes: Vec<u8>, face_index: u32) -> Result<Self> {
let name = name.into();
if name.is_empty() || name.len() > 128 {
return Err(font_error("font name is empty or too long"));
}
FontRef::from_index(&bytes, face_index)
.map_err(|_| font_error("font bytes or face index are invalid"))?;
let digest = Sha256::digest(&bytes).into();
Ok(Self {
name,
bytes: bytes.into(),
face_index,
digest,
})
}
#[must_use]
pub fn covers(&self, text: &str) -> bool {
let Ok(face) = FontRef::from_index(&self.bytes, self.face_index) else {
return false;
};
let charmap = face.charmap();
text.chars()
.all(|character| character.is_control() || charmap.map(character).is_some())
}
pub fn units_per_em(&self) -> Result<u16> {
let face = FontRef::from_index(&self.bytes, self.face_index)
.map_err(|_| font_error("registered font became invalid"))?;
let units_per_em = face
.metrics(Size::unscaled(), skrifa::instance::LocationRef::default())
.units_per_em;
if units_per_em == 0 {
return Err(font_error("registered font has no units-per-em"));
}
Ok(units_per_em)
}
}
pub trait FontResolver: Send + Sync {
fn resolve_font(&self, name: &str, max_bytes: usize) -> Result<FontAsset>;
}
#[derive(Clone, Debug, Default)]
pub struct FontManager {
fonts: BTreeMap<String, FontAsset>,
fallback: Vec<String>,
}
impl FontManager {
pub fn register_from(
&mut self,
resolver: &dyn FontResolver,
name: &str,
max_bytes: usize,
) -> Result<()> {
self.register(resolver.resolve_font(name, max_bytes)?)
}
pub fn register(&mut self, font: FontAsset) -> Result<()> {
if self.fonts.contains_key(&font.name) {
return Err(font_error(format!(
"font `{}` is already registered",
font.name
)));
}
self.fonts.insert(font.name.clone(), font);
Ok(())
}
pub fn set_fallback(&mut self, fallback: Vec<String>) -> Result<()> {
if fallback.len() > 64 || fallback.iter().any(|name| !self.fonts.contains_key(name)) {
return Err(font_error(
"fallback contains too many fonts or an unknown name",
));
}
self.fallback = fallback;
Ok(())
}
pub fn get(&self, name: &str) -> Result<&FontAsset> {
self.fonts
.get(name)
.ok_or_else(|| font_error(format!("font `{name}` is not registered")))
}
pub fn select_for_grapheme<'a>(
&'a self,
primary: &str,
grapheme: &str,
) -> Result<&'a FontAsset> {
let primary = self.get(primary)?;
if primary.covers(grapheme) {
return Ok(primary);
}
for name in &self.fallback {
let font = self.get(name)?;
if font.covers(grapheme) {
return Ok(font);
}
}
let scalar = grapheme
.chars()
.find(|value| !value.is_control())
.map_or(0, u32::from);
Err(font_error(format!(
"no explicit font contains glyph U+{scalar:04X}"
)))
}
pub fn digests(&self) -> impl Iterator<Item = (&str, &[u8; 32])> {
self.fonts
.iter()
.map(|(name, font)| (name.as_str(), &font.digest))
}
pub fn fallback_names(&self) -> impl Iterator<Item = &str> {
self.fallback.iter().map(String::as_str)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FontSubset {
pub font: String,
pub glyph_ids: BTreeSet<u16>,
}
impl FontSubset {
#[must_use]
pub fn new(font: impl Into<String>) -> Self {
Self {
font: font.into(),
glyph_ids: BTreeSet::new(),
}
}
pub fn record(&mut self, glyph_id: u16) {
self.glyph_ids.insert(glyph_id);
}
}
fn font_error(message: impl Into<String>) -> FileMakerError {
FileMakerError::new(ErrorCode::FontMissing, message)
}