use std::io::Read;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use cranpose_ui::text::{FontFamily, FontFile, FontStyle, FontWeight};
use crate::software_text_raster::{
default_software_text_font, SoftwareTextFont, SoftwareTextFontError, SoftwareTextFontSet,
};
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>,
}
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 first_error = None;
let mut loaded = 0usize;
for file in &files {
match self.register_read_face(
&mut reads,
family,
file.weight,
file.style,
Path::new(&file.path),
) {
Ok(()) => loaded += 1,
Err(error) => first_error = first_error.or(Some(error)),
}
}
match first_error {
Some(error) if loaded == 0 => Err(error),
_ => Ok(()),
}
}
pub fn register_face_path(
&mut self,
family: &FontFamily,
weight: FontWeight,
style: FontStyle,
path: impl AsRef<Path>,
) -> Result<(), FontLoadError> {
self.register_read_face(
&mut FontFileReads::default(),
family,
weight,
style,
path.as_ref(),
)
}
fn register_read_face(
&mut self,
reads: &mut FontFileReads,
family: &FontFamily,
weight: FontWeight,
style: FontStyle,
path: &Path,
) -> Result<(), FontLoadError> {
let bytes = reads.read(path)?;
let face = SoftwareTextFont::from_registered_bytes(family, weight, style, bytes.to_vec())
.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> {
let face = SoftwareTextFont::from_registered_bytes(family, weight, style, bytes)
.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 first_error = None;
let mut loaded = 0usize;
for weight in weights {
match self.register_read_system_face(
&mut reads,
directory,
family,
*weight,
FontStyle::Normal,
) {
Ok(()) => loaded += 1,
Err(error) => first_error = first_error.or(Some(error)),
}
}
match first_error {
Some(error) if loaded == 0 => Err(error),
_ => Ok(()),
}
}
pub fn register_system_face(
&mut self,
directory: impl AsRef<Path>,
family: &FontFamily,
weight: FontWeight,
style: FontStyle,
) -> Result<(), FontLoadError> {
self.register_read_system_face(
&mut FontFileReads::default(),
directory.as_ref(),
family,
weight,
style,
)
}
fn register_read_system_face(
&mut self,
reads: &mut FontFileReads,
directory: &Path,
family: &FontFamily,
weight: FontWeight,
style: FontStyle,
) -> Result<(), FontLoadError> {
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)
}
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() {
if 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_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)],
}
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",
],
weighted: &[
(300, "Roboto-Light.ttf"),
(500, "Roboto-Medium.ttf"),
(700, "Roboto-Bold.ttf"),
(900, "Roboto-Black.ttf"),
],
}),
FontFamily::Serif | FontFamily::Fantasy => Some(SystemFamilyFiles {
regular: &["NotoSerif-Regular.ttf", "DroidSerif-Regular.ttf"],
weighted: &[(700, "NotoSerif-Bold.ttf"), (700, "DroidSerif-Bold.ttf")],
}),
FontFamily::Monospace => Some(SystemFamilyFiles {
regular: &[
"DroidSansMono.ttf",
"RobotoMono-Regular.ttf",
"CutiveMono-Regular.ttf",
],
weighted: &[(700, "RobotoMono-Bold.ttf")],
}),
FontFamily::Cursive => Some(SystemFamilyFiles {
regular: &["DancingScript-Regular.ttf"],
weighted: &[(700, "DancingScript-Bold.ttf")],
}),
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)]
mod tests {
use super::*;
use cranpose_ui::text::{SpanStyle, TextStyle};
use std::io::Cursor;
const REGULAR: &[u8] = include_bytes!("../assets/NotoSansMerged.ttf");
const BOLD: &[u8] = include_bytes!("../assets/NotoSansBold.ttf");
fn style_for(family: &FontFamily, weight: FontWeight) -> TextStyle {
TextStyle {
span_style: SpanStyle {
font_family: Some(family.clone()),
font_weight: Some(weight),
..Default::default()
},
..Default::default()
}
}
struct ScratchDir(PathBuf);
impl ScratchDir {
fn new(name: &str) -> Self {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../../target/test-output/cranpose-font-source")
.join(name);
let _ = std::fs::remove_dir_all(&path);
std::fs::create_dir_all(&path).expect("scratch directory");
Self(path)
}
fn write(&self, name: &str, bytes: &[u8]) -> PathBuf {
let path = self.0.join(name);
std::fs::write(&path, bytes).expect("scratch font file");
path
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for ScratchDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[test]
fn register_family_picks_the_face_matching_the_requested_weight() {
let dir = ScratchDir::new("weights");
let regular = dir.write("Test-Regular.ttf", REGULAR);
let bold = dir.write("Test-Bold.ttf", BOLD);
let family = FontFamily::file_backed(vec![
FontFile::new(regular.to_string_lossy().into_owned()),
FontFile::new(bold.to_string_lossy().into_owned()).with_weight(FontWeight::BOLD),
])
.expect("file-backed family");
let mut registry = SoftwareTextFontRegistry::new();
registry.register_family(&family).expect("family loads");
let fonts = registry.into_font_set_or_default(&[]);
let resolved_regular = fonts
.resolve(&style_for(&family, FontWeight::NORMAL))
.expect("regular face");
let resolved_bold = fonts
.resolve(&style_for(&family, FontWeight::BOLD))
.expect("bold face");
assert_eq!(resolved_regular.weight(), FontWeight::NORMAL);
assert_eq!(resolved_bold.weight(), FontWeight::BOLD);
assert_ne!(
resolved_regular.content_hash(),
resolved_bold.content_hash(),
"distinct faces must key the glyph atlas distinctly"
);
}
#[test]
fn register_family_honours_a_declared_weight_over_the_face_header() {
let dir = ScratchDir::new("declared");
let path = dir.write("Test-Regular.ttf", REGULAR);
let family =
FontFamily::file_backed(vec![
FontFile::new(path.to_string_lossy().into_owned()).with_weight(FontWeight::MEDIUM)
])
.expect("file-backed family");
let mut registry = SoftwareTextFontRegistry::new();
registry.register_family(&family).expect("family loads");
let fonts = registry.into_font_set_or_default(&[]);
let resolved = fonts
.resolve(&style_for(&family, FontWeight::MEDIUM))
.expect("declared face");
assert_eq!(resolved.weight(), FontWeight::MEDIUM);
}
#[test]
fn register_family_reports_a_missing_file_without_panicking() {
let dir = ScratchDir::new("missing");
let family = FontFamily::file_backed(vec![FontFile::new(
dir.path().join("Absent.ttf").to_string_lossy().into_owned(),
)])
.expect("file-backed family");
let mut registry = SoftwareTextFontRegistry::new();
let error = registry
.register_family(&family)
.expect_err("a missing file cannot register");
assert!(matches!(error, FontLoadError::Read { .. }), "{error}");
assert!(registry.is_empty());
}
#[test]
fn register_family_reports_a_corrupt_file_without_panicking() {
let dir = ScratchDir::new("corrupt");
let path = dir.write("Corrupt.ttf", b"this is not a font");
let family =
FontFamily::file_backed(vec![FontFile::new(path.to_string_lossy().into_owned())])
.expect("file-backed family");
let mut registry = SoftwareTextFontRegistry::new();
let error = registry
.register_family(&family)
.expect_err("a corrupt file cannot register");
assert!(matches!(error, FontLoadError::Parse { .. }), "{error}");
assert!(registry.is_empty());
}
#[test]
#[cfg(feature = "embedded-default-font")]
fn a_family_that_failed_to_load_falls_back_to_the_default_face() {
let dir = ScratchDir::new("fallback");
let family = FontFamily::file_backed(vec![FontFile::new(
dir.path().join("Absent.ttf").to_string_lossy().into_owned(),
)])
.expect("file-backed family");
let mut registry = SoftwareTextFontRegistry::new();
let _ = registry.register_family(&family);
let fonts = registry.into_font_set_or_default(&[]);
let resolved = fonts
.resolve(&style_for(&family, FontWeight::NORMAL))
.expect("fallback face");
assert_eq!(
resolved.content_hash(),
fonts.default_font().expect("default face").content_hash()
);
}
#[test]
fn a_partly_loadable_family_keeps_the_faces_that_did_load() {
let dir = ScratchDir::new("partial");
let regular = dir.write("Test-Regular.ttf", REGULAR);
let family = FontFamily::file_backed(vec![
FontFile::new(regular.to_string_lossy().into_owned()),
FontFile::new(dir.path().join("Absent.ttf").to_string_lossy().into_owned())
.with_weight(FontWeight::BOLD),
])
.expect("file-backed family");
let mut registry = SoftwareTextFontRegistry::new();
registry
.register_family(&family)
.expect("one readable face is enough");
assert_eq!(registry.faces().len(), 1);
}
#[test]
fn register_face_reader_accepts_a_font_that_is_not_a_file() {
let family = FontFamily::named("Bundled");
let mut registry = SoftwareTextFontRegistry::new();
registry
.register_face_reader(
&family,
FontWeight::NORMAL,
FontStyle::Normal,
&mut Cursor::new(REGULAR.to_vec()),
)
.expect("streamed face loads");
let fonts = registry.into_font_set_or_default(&[]);
let resolved = fonts
.resolve(&style_for(&family, FontWeight::NORMAL))
.expect("streamed face");
assert_eq!(resolved.registered_family(), {
let mut expected = SoftwareTextFontRegistry::new();
expected
.register_face_bytes(
&family,
FontWeight::NORMAL,
FontStyle::Normal,
REGULAR.to_vec(),
)
.expect("face loads");
expected.faces()[0].registered_family()
});
}
#[test]
#[cfg(feature = "embedded-default-font")]
fn an_empty_registry_falls_back_to_the_embedded_default_face() {
let fonts = SoftwareTextFontRegistry::new().into_font_set_or_default(&[]);
assert!(
fonts.default_font().is_some(),
"the embedded default font must still serve apps that supply nothing"
);
assert!(fonts
.resolve(&TextStyle::default())
.is_some_and(|font| font.registered_family().is_none()));
}
#[test]
fn system_font_file_prefers_a_weight_specific_static_face() {
let dir = ScratchDir::new("system-static");
dir.write("Roboto-Regular.ttf", REGULAR);
let medium = dir.write("Roboto-Medium.ttf", BOLD);
assert_eq!(
system_font_file(dir.path(), &FontFamily::SansSerif, FontWeight::MEDIUM),
Some(medium)
);
}
#[test]
fn system_font_file_falls_back_to_the_regular_face_for_other_weights() {
let dir = ScratchDir::new("system-regular");
let regular = dir.write("Roboto-Regular.ttf", REGULAR);
assert_eq!(
system_font_file(dir.path(), &FontFamily::SansSerif, FontWeight::MEDIUM),
Some(regular)
);
}
#[test]
fn system_font_file_reports_nothing_when_the_directory_is_empty() {
let dir = ScratchDir::new("system-empty");
assert_eq!(
system_font_file(dir.path(), &FontFamily::SansSerif, FontWeight::NORMAL),
None
);
assert_eq!(
system_font_file(dir.path(), &FontFamily::named("Roboto"), FontWeight::NORMAL),
None
);
}
#[test]
fn register_system_family_binds_the_generic_alias_to_the_platform_face() {
let dir = ScratchDir::new("system-family");
dir.write("Roboto-Regular.ttf", REGULAR);
dir.write("Roboto-Bold.ttf", BOLD);
let mut registry = SoftwareTextFontRegistry::new();
registry
.register_system_family(
dir.path(),
&FontFamily::SansSerif,
DEFAULT_SYSTEM_FAMILY_WEIGHTS,
)
.expect("system family loads");
let fonts = registry.into_font_set_or_default(&[]);
assert!(fonts.has_registered_family(&FontFamily::SansSerif));
let bold = fonts
.resolve(&style_for(&FontFamily::SansSerif, FontWeight::BOLD))
.expect("bold system face");
assert_eq!(bold.weight(), FontWeight::BOLD);
assert_eq!(bold.registered_family(), {
let key = fonts
.resolve(&style_for(&FontFamily::SansSerif, FontWeight::NORMAL))
.expect("regular system face")
.registered_family();
key
});
}
#[test]
fn register_system_family_reports_an_absent_font_directory() {
let mut registry = SoftwareTextFontRegistry::new();
let error = registry
.register_system_family(
Path::new("/definitely/not/a/font/directory"),
&FontFamily::SansSerif,
DEFAULT_SYSTEM_FAMILY_WEIGHTS,
)
.expect_err("an absent directory cannot register");
assert!(
matches!(error, FontLoadError::NoSystemFontFile { .. }),
"{error}"
);
assert!(registry.is_empty());
}
#[test]
fn register_family_rejects_a_family_that_names_no_files() {
let mut registry = SoftwareTextFontRegistry::new();
let error = registry
.register_family(&FontFamily::named("Roboto"))
.expect_err("a named family has nothing to read");
assert!(matches!(error, FontLoadError::NotFileBacked), "{error}");
}
#[test]
fn loaded_typeface_families_register_their_single_file() {
let dir = ScratchDir::new("typeface");
let path = dir.write("Test-Regular.ttf", REGULAR);
let family = FontFamily::loaded_typeface_path(path.to_string_lossy().into_owned());
let mut registry = SoftwareTextFontRegistry::new();
registry.register_family(&family).expect("typeface loads");
let fonts = registry.into_font_set_or_default(&[]);
assert!(fonts.has_registered_family(&family));
assert!(fonts
.resolve(&style_for(&family, FontWeight::NORMAL))
.is_some_and(|font| font.registered_family().is_some()));
}
}