use std::{
io::Read,
path::{Path, PathBuf},
sync::Arc,
};
use cranpose_ui::text::{FontFamily, FontFile, FontStyle, FontWeight};
use crate::software_text_raster::{
FontFamilyKey, SoftwareTextFont, SoftwareTextFontError, SoftwareTextFontSet,
default_software_text_font,
};
pub const ANDROID_SYSTEM_FONT_DIR: &str = "/system/fonts";
pub const DEFAULT_SYSTEM_FAMILY_WEIGHTS: &[FontWeight] =
&[FontWeight::NORMAL, FontWeight::MEDIUM, FontWeight::BOLD];
#[derive(Debug, thiserror::Error)]
pub enum FontLoadError {
#[error("font family declares no faces")]
EmptyFamily,
#[error("font family is not backed by files, so it has nothing to load")]
NotFileBacked,
#[error("no system font file for this family under {directory}")]
NoSystemFontFile { directory: PathBuf },
#[error("failed to read font file {path}: {source}")]
Read {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("failed to parse font file {path}: {source}")]
Parse {
path: PathBuf,
#[source]
source: SoftwareTextFontError,
},
#[error("failed to parse font bytes: {source}")]
ParseBytes {
#[source]
source: SoftwareTextFontError,
},
}
#[derive(Clone, Default)]
pub struct SoftwareTextFontRegistry {
faces: Vec<SoftwareTextFont>,
system_faces: Vec<(FontFamilyKey, FontWeight, FontStyle)>,
}
#[derive(Default)]
struct TolerantLoad {
first_error: Option<FontLoadError>,
loaded: usize,
}
impl TolerantLoad {
fn record(&mut self, result: Result<(), FontLoadError>) {
match result {
Ok(()) => self.loaded += 1,
Err(error) => self.first_error = self.first_error.take().or(Some(error)),
}
}
fn finish(self) -> Result<(), FontLoadError> {
match self.first_error {
Some(error) if self.loaded == 0 => Err(error),
_ => Ok(()),
}
}
}
impl SoftwareTextFontRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register_family(&mut self, family: &FontFamily) -> Result<(), FontLoadError> {
let files = font_files_for(family)?;
if files.is_empty() {
return Err(FontLoadError::EmptyFamily);
}
let mut reads = FontFileReads::default();
let mut load = TolerantLoad::default();
for file in &files {
load.record(self.register_read_face(
&mut reads,
family,
file.weight,
file.style,
Path::new(&file.path),
&[],
));
}
load.finish()
}
fn register_read_face(
&mut self,
reads: &mut FontFileReads,
family: &FontFamily,
weight: FontWeight,
style: FontStyle,
path: &Path,
variations: &[([u8; 4], f32)],
) -> Result<(), FontLoadError> {
let bytes = reads.read(path)?;
let face = SoftwareTextFont::from_registered_bytes_with_variations(
family,
weight,
style,
bytes.to_vec(),
variations,
)
.map_err(|source| FontLoadError::Parse {
path: path.to_path_buf(),
source,
})?;
self.faces.push(face);
Ok(())
}
pub fn register_face_reader(
&mut self,
family: &FontFamily,
weight: FontWeight,
style: FontStyle,
reader: &mut impl Read,
) -> Result<(), FontLoadError> {
let mut bytes = Vec::new();
reader
.read_to_end(&mut bytes)
.map_err(|source| FontLoadError::Read {
path: PathBuf::new(),
source,
})?;
self.register_face_bytes(family, weight, style, bytes)
}
pub fn register_face_bytes(
&mut self,
family: &FontFamily,
weight: FontWeight,
style: FontStyle,
bytes: impl Into<Vec<u8>>,
) -> Result<(), FontLoadError> {
self.register_face_bytes_with_variations(family, weight, style, bytes, &[])
}
pub fn register_face_bytes_with_variations(
&mut self,
family: &FontFamily,
weight: FontWeight,
style: FontStyle,
bytes: impl Into<Vec<u8>>,
variations: &[([u8; 4], f32)],
) -> Result<(), FontLoadError> {
let face = SoftwareTextFont::from_registered_bytes_with_variations(
family, weight, style, bytes, variations,
)
.map_err(|source| FontLoadError::ParseBytes { source })?;
self.faces.push(face);
Ok(())
}
pub fn register_fallback_bytes(
&mut self,
bytes: impl Into<Vec<u8>>,
) -> Result<(), FontLoadError> {
let face = SoftwareTextFont::from_bytes(bytes)
.map_err(|source| FontLoadError::ParseBytes { source })?;
self.faces.push(face);
Ok(())
}
pub fn register_system_family(
&mut self,
directory: impl AsRef<Path>,
family: &FontFamily,
weights: &[FontWeight],
) -> Result<(), FontLoadError> {
let directory = directory.as_ref();
let mut reads = FontFileReads::default();
let mut load = TolerantLoad::default();
for weight in weights {
load.record(self.register_read_system_face(
&mut reads,
directory,
family,
*weight,
FontStyle::Normal,
&[],
));
}
load.finish()
}
pub fn register_system_face(
&mut self,
directory: impl AsRef<Path>,
family: &FontFamily,
weight: FontWeight,
style: FontStyle,
) -> Result<(), FontLoadError> {
self.register_system_face_with_variations(directory, family, weight, style, &[])
}
pub fn register_system_face_with_variations(
&mut self,
directory: impl AsRef<Path>,
family: &FontFamily,
weight: FontWeight,
style: FontStyle,
variations: &[([u8; 4], f32)],
) -> Result<(), FontLoadError> {
self.register_read_system_face(
&mut FontFileReads::default(),
directory.as_ref(),
family,
weight,
style,
variations,
)
}
fn register_read_system_face(
&mut self,
reads: &mut FontFileReads,
directory: &Path,
family: &FontFamily,
weight: FontWeight,
style: FontStyle,
variations: &[([u8; 4], f32)],
) -> Result<(), FontLoadError> {
let weight = system_declared_weight(family, weight);
if self.has_system_face(family, weight, style) {
return Ok(());
}
let path = system_font_file(directory, family, weight).ok_or_else(|| {
FontLoadError::NoSystemFontFile {
directory: directory.to_path_buf(),
}
})?;
self.register_read_face(reads, family, weight, style, &path, variations)?;
self.system_faces
.push((FontFamilyKey::of(family), weight, style));
Ok(())
}
fn has_system_face(&self, family: &FontFamily, weight: FontWeight, style: FontStyle) -> bool {
self.system_faces
.contains(&(FontFamilyKey::of(family), weight, style))
}
pub fn faces(&self) -> &[SoftwareTextFont] {
&self.faces
}
pub fn is_empty(&self) -> bool {
self.faces.is_empty()
}
pub fn into_font_set_or_default(mut self, fonts: &[&[u8]]) -> SoftwareTextFontSet {
for bytes in fonts {
let _ = self.register_fallback_bytes((*bytes).to_vec());
}
if self.faces.is_empty()
&& let Some(default_font) = default_software_text_font()
{
self.faces.push(default_font);
}
SoftwareTextFontSet::from_faces(self.faces)
}
}
#[derive(Default)]
struct FontFileReads {
entries: Vec<(PathBuf, Arc<[u8]>)>,
}
impl FontFileReads {
fn read(&mut self, path: &Path) -> Result<Arc<[u8]>, FontLoadError> {
if let Some((_, bytes)) = self.entries.iter().find(|(read, _)| read == path) {
return Ok(Arc::clone(bytes));
}
let bytes: Arc<[u8]> = std::fs::read(path)
.map_err(|source| FontLoadError::Read {
path: path.to_path_buf(),
source,
})?
.into();
self.entries.push((path.to_path_buf(), Arc::clone(&bytes)));
Ok(bytes)
}
}
pub fn system_declared_weight(family: &FontFamily, weight: FontWeight) -> FontWeight {
let Some(files) = system_family_files(family) else {
return weight;
};
closest_declared_weight(files.declared, weight).unwrap_or(weight)
}
fn closest_declared_weight(declared: &[u16], requested: FontWeight) -> Option<FontWeight> {
let mut best: Option<(u16, u16)> = None;
for candidate in declared {
let score = weight_match_score(*candidate, requested.value());
if best.is_none_or(|(_, best_score)| score < best_score) {
best = Some((*candidate, score));
}
}
best.map(|(candidate, _)| FontWeight(candidate))
}
fn weight_match_score(declared: u16, requested: u16) -> u16 {
(declared / 100).abs_diff(requested / 100)
}
pub fn system_font_file(
directory: &Path,
family: &FontFamily,
weight: FontWeight,
) -> Option<PathBuf> {
let files = system_family_files(family)?;
files
.weighted
.iter()
.filter(|(candidate_weight, _)| *candidate_weight == weight.value())
.map(|(_, name)| directory.join(name))
.chain(files.regular.iter().map(|name| directory.join(name)))
.find(|path| path.is_file())
}
struct SystemFamilyFiles {
regular: &'static [&'static str],
weighted: &'static [(u16, &'static str)],
declared: &'static [u16],
}
const DECLARED_HUNDREDS: &[u16] = &[100, 200, 300, 400, 500, 600, 700, 800, 900];
fn system_family_files(family: &FontFamily) -> Option<SystemFamilyFiles> {
match family {
FontFamily::Default | FontFamily::SansSerif => Some(SystemFamilyFiles {
regular: &[
"Roboto-Regular.ttf",
"RobotoStatic-Regular.ttf",
"NotoSans-Regular.ttf",
"DroidSans.ttf",
"Core/SFUI.ttf",
"SFNS.ttf",
],
weighted: &[
(300, "Roboto-Light.ttf"),
(500, "Roboto-Medium.ttf"),
(700, "Roboto-Bold.ttf"),
(900, "Roboto-Black.ttf"),
],
declared: DECLARED_HUNDREDS,
}),
FontFamily::Serif | FontFamily::Fantasy => Some(SystemFamilyFiles {
regular: &["NotoSerif-Regular.ttf", "DroidSerif-Regular.ttf"],
weighted: &[(700, "NotoSerif-Bold.ttf"), (700, "DroidSerif-Bold.ttf")],
declared: &[400, 700],
}),
FontFamily::Monospace => Some(SystemFamilyFiles {
regular: &[
"DroidSansMono.ttf",
"RobotoMono-Regular.ttf",
"CutiveMono-Regular.ttf",
],
weighted: &[(700, "RobotoMono-Bold.ttf")],
declared: &[400, 700],
}),
FontFamily::Cursive => Some(SystemFamilyFiles {
regular: &["DancingScript-Regular.ttf"],
weighted: &[(700, "DancingScript-Bold.ttf")],
declared: &[400, 700],
}),
FontFamily::Named(_) | FontFamily::FileBacked(_) | FontFamily::LoadedTypeface(_) => None,
}
}
fn font_files_for(family: &FontFamily) -> Result<Vec<FontFile>, FontLoadError> {
match family {
FontFamily::FileBacked(file_backed) => Ok(file_backed.fonts.clone()),
FontFamily::LoadedTypeface(typeface) => Ok(vec![FontFile::new(typeface.path.clone())]),
_ => Err(FontLoadError::NotFileBacked),
}
}
#[cfg(test)]
#[path = "tests/font_source_tests.rs"]
mod tests;