use std::sync::Arc;
use astrelis_assets::{Asset, AssetLoader, AssetResult, LoadContext};
#[derive(Debug, Clone)]
pub struct FontAsset {
data: Arc<[u8]>,
name: String,
}
impl FontAsset {
pub fn new(data: impl Into<Arc<[u8]>>, name: impl Into<String>) -> Self {
Self {
data: data.into(),
name: name.into(),
}
}
pub fn data(&self) -> &[u8] {
&self.data
}
pub fn data_arc(&self) -> Arc<[u8]> {
self.data.clone()
}
pub fn name(&self) -> &str {
&self.name
}
pub fn size(&self) -> usize {
self.data.len()
}
pub fn format(&self) -> FontFormat {
FontFormat::detect(&self.data)
}
pub fn load_into(&self, db: &mut crate::FontDatabase) {
db.load_font_data(self.data.to_vec());
}
}
impl Asset for FontAsset {
fn type_name() -> &'static str {
"FontAsset"
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FontFormat {
TrueType,
OpenType,
Woff,
Woff2,
TrueTypeCollection,
OpenTypeCollection,
Unknown,
}
impl FontFormat {
pub fn detect(data: &[u8]) -> Self {
if data.len() < 4 {
return FontFormat::Unknown;
}
match &data[0..4] {
[0x00, 0x01, 0x00, 0x00] | [b't', b'r', b'u', b'e'] => FontFormat::TrueType,
[b'O', b'T', b'T', b'O'] => FontFormat::OpenType,
[b'w', b'O', b'F', b'F'] => FontFormat::Woff,
[b'w', b'O', b'F', b'2'] => FontFormat::Woff2,
[b't', b't', b'c', b'f'] => FontFormat::TrueTypeCollection,
_ => FontFormat::Unknown,
}
}
pub fn extension(&self) -> &'static str {
match self {
FontFormat::TrueType => "ttf",
FontFormat::OpenType => "otf",
FontFormat::Woff => "woff",
FontFormat::Woff2 => "woff2",
FontFormat::TrueTypeCollection => "ttc",
FontFormat::OpenTypeCollection => "otc",
FontFormat::Unknown => "bin",
}
}
}
pub struct FontLoader;
impl AssetLoader for FontLoader {
type Asset = FontAsset;
fn extensions(&self) -> &[&str] {
&["ttf", "otf", "woff", "woff2", "ttc", "otc"]
}
fn load(&self, ctx: LoadContext<'_>) -> AssetResult<Self::Asset> {
let format = FontFormat::detect(ctx.bytes);
if format == FontFormat::Unknown && ctx.bytes.len() > 4 {
tracing::warn!(
"Font file '{}' has unrecognized format (magic: {:02x?}), loading anyway",
ctx.source.display_path(),
&ctx.bytes[..4.min(ctx.bytes.len())]
);
}
let name = ctx
.source
.path()
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
.map(String::from)
.unwrap_or_else(|| ctx.source.display_path());
Ok(FontAsset::new(ctx.bytes.to_vec(), name))
}
fn priority(&self) -> i32 {
0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_font_format_detection() {
let ttf_data = [0x00, 0x01, 0x00, 0x00, 0x00, 0x00];
assert_eq!(FontFormat::detect(&ttf_data), FontFormat::TrueType);
let ttf_true = b"true\x00\x00";
assert_eq!(FontFormat::detect(ttf_true), FontFormat::TrueType);
let otf_data = b"OTTO\x00\x00";
assert_eq!(FontFormat::detect(otf_data), FontFormat::OpenType);
let woff_data = b"wOFF\x00\x00";
assert_eq!(FontFormat::detect(woff_data), FontFormat::Woff);
let woff2_data = b"wOF2\x00\x00";
assert_eq!(FontFormat::detect(woff2_data), FontFormat::Woff2);
let ttc_data = b"ttcf\x00\x00";
assert_eq!(FontFormat::detect(ttc_data), FontFormat::TrueTypeCollection);
let unknown = b"????";
assert_eq!(FontFormat::detect(unknown), FontFormat::Unknown);
let short = [0x00, 0x01];
assert_eq!(FontFormat::detect(&short), FontFormat::Unknown);
}
#[test]
fn test_font_asset_creation() {
let data: Vec<u8> = vec![0x00, 0x01, 0x00, 0x00, 0x00, 0x10];
let asset = FontAsset::new(data.clone(), "test.ttf");
assert_eq!(asset.name(), "test.ttf");
assert_eq!(asset.data(), &data[..]);
assert_eq!(asset.size(), 6);
assert_eq!(asset.format(), FontFormat::TrueType);
}
#[test]
fn test_font_asset_clone() {
let data: Vec<u8> = vec![0x00, 0x01, 0x00, 0x00];
let asset1 = FontAsset::new(data, "font.ttf");
let asset2 = asset1.clone();
assert_eq!(asset1.name(), asset2.name());
assert_eq!(asset1.data(), asset2.data());
assert!(Arc::ptr_eq(&asset1.data, &asset2.data));
}
#[test]
fn test_font_loader_extensions() {
let loader = FontLoader;
let exts = loader.extensions();
assert!(exts.contains(&"ttf"));
assert!(exts.contains(&"otf"));
assert!(exts.contains(&"woff"));
assert!(exts.contains(&"woff2"));
assert!(exts.contains(&"ttc"));
assert!(exts.contains(&"otc"));
}
#[test]
fn test_font_loader_load() {
use astrelis_assets::AssetSource;
let loader = FontLoader;
let data = vec![0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00];
let source = AssetSource::disk("fonts/TestFont.ttf");
let ctx = LoadContext::new(&source, &data, Some("ttf"));
let result = loader.load(ctx);
assert!(result.is_ok());
let asset = result.unwrap();
assert_eq!(asset.name(), "TestFont.ttf");
assert_eq!(asset.data(), &data[..]);
assert_eq!(asset.format(), FontFormat::TrueType);
}
#[test]
fn test_font_format_extensions() {
assert_eq!(FontFormat::TrueType.extension(), "ttf");
assert_eq!(FontFormat::OpenType.extension(), "otf");
assert_eq!(FontFormat::Woff.extension(), "woff");
assert_eq!(FontFormat::Woff2.extension(), "woff2");
assert_eq!(FontFormat::TrueTypeCollection.extension(), "ttc");
assert_eq!(FontFormat::OpenTypeCollection.extension(), "otc");
assert_eq!(FontFormat::Unknown.extension(), "bin");
}
}