Skip to main content

astrelis_text/
asset.rs

1//! Asset integration for font loading.
2//!
3//! This module provides integration with the `astrelis-assets` system,
4//! allowing fonts to be loaded through the standard asset pipeline.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use astrelis_assets::{AssetServer, Handle};
10//! use astrelis_text::{FontAsset, FontLoader};
11//!
12//! let mut server = AssetServer::new();
13//! server.register_loader(FontLoader);
14//!
15//! // Load a font file
16//! let font: Handle<FontAsset> = server.load_sync("fonts/MyFont.ttf").unwrap();
17//!
18//! // Use the font data
19//! if let Some(font_asset) = server.get(&font) {
20//!     // font_asset.data() returns the raw font bytes
21//!     // font_asset.name() returns the font file name
22//! }
23//! ```
24
25use std::sync::Arc;
26
27use astrelis_assets::{Asset, AssetLoader, AssetResult, LoadContext};
28
29/// A font asset containing raw font data.
30///
31/// This asset holds the raw bytes of a font file (.ttf, .otf, .woff, .woff2)
32/// which can be used to load the font into a `FontDatabase` or `FontSystem`.
33///
34/// # Usage
35///
36/// ```ignore
37/// use astrelis_text::{FontAsset, FontDatabase};
38///
39/// // After loading the font asset
40/// let font_asset: &FontAsset = server.get(&handle).unwrap();
41///
42/// // Load into a font database
43/// let mut db = FontDatabase::empty();
44/// db.load_font_data(font_asset.data().to_vec());
45/// ```
46#[derive(Debug, Clone)]
47pub struct FontAsset {
48    /// The raw font data bytes.
49    data: Arc<[u8]>,
50    /// The name/identifier of the font (usually the filename).
51    name: String,
52}
53
54impl FontAsset {
55    /// Create a new font asset from raw data.
56    pub fn new(data: impl Into<Arc<[u8]>>, name: impl Into<String>) -> Self {
57        Self {
58            data: data.into(),
59            name: name.into(),
60        }
61    }
62
63    /// Get the raw font data.
64    pub fn data(&self) -> &[u8] {
65        &self.data
66    }
67
68    /// Get the font data as an Arc for efficient sharing.
69    pub fn data_arc(&self) -> Arc<[u8]> {
70        self.data.clone()
71    }
72
73    /// Get the name/identifier of the font.
74    pub fn name(&self) -> &str {
75        &self.name
76    }
77
78    /// Get the size of the font data in bytes.
79    pub fn size(&self) -> usize {
80        self.data.len()
81    }
82
83    /// Detect the font format from the data.
84    pub fn format(&self) -> FontFormat {
85        FontFormat::detect(&self.data)
86    }
87
88    /// Load this font into a FontDatabase.
89    ///
90    /// This is a convenience method that adds the font data to the database.
91    pub fn load_into(&self, db: &mut crate::FontDatabase) {
92        db.load_font_data(self.data.to_vec());
93    }
94}
95
96impl Asset for FontAsset {
97    fn type_name() -> &'static str {
98        "FontAsset"
99    }
100}
101
102/// Detected font file format.
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum FontFormat {
105    /// TrueType font (.ttf)
106    TrueType,
107    /// OpenType font (.otf)
108    OpenType,
109    /// Web Open Font Format (.woff)
110    Woff,
111    /// Web Open Font Format 2 (.woff2)
112    Woff2,
113    /// TrueType Collection (.ttc)
114    TrueTypeCollection,
115    /// OpenType Collection (.otc)
116    OpenTypeCollection,
117    /// Unknown format
118    Unknown,
119}
120
121impl FontFormat {
122    /// Detect font format from the magic bytes.
123    pub fn detect(data: &[u8]) -> Self {
124        if data.len() < 4 {
125            return FontFormat::Unknown;
126        }
127
128        match &data[0..4] {
129            // TrueType: 0x00010000 or 'true'
130            [0x00, 0x01, 0x00, 0x00] | [b't', b'r', b'u', b'e'] => FontFormat::TrueType,
131            // OpenType: 'OTTO'
132            [b'O', b'T', b'T', b'O'] => FontFormat::OpenType,
133            // WOFF: 'wOFF'
134            [b'w', b'O', b'F', b'F'] => FontFormat::Woff,
135            // WOFF2: 'wOF2'
136            [b'w', b'O', b'F', b'2'] => FontFormat::Woff2,
137            // TrueType Collection: 'ttcf'
138            [b't', b't', b'c', b'f'] => FontFormat::TrueTypeCollection,
139            _ => FontFormat::Unknown,
140        }
141    }
142
143    /// Get the typical file extension for this format.
144    pub fn extension(&self) -> &'static str {
145        match self {
146            FontFormat::TrueType => "ttf",
147            FontFormat::OpenType => "otf",
148            FontFormat::Woff => "woff",
149            FontFormat::Woff2 => "woff2",
150            FontFormat::TrueTypeCollection => "ttc",
151            FontFormat::OpenTypeCollection => "otc",
152            FontFormat::Unknown => "bin",
153        }
154    }
155}
156
157/// Asset loader for font files.
158///
159/// Supports loading `.ttf`, `.otf`, `.woff`, `.woff2`, `.ttc`, and `.otc` files.
160///
161/// # Example
162///
163/// ```ignore
164/// use astrelis_assets::AssetServer;
165/// use astrelis_text::FontLoader;
166///
167/// let mut server = AssetServer::new();
168/// server.register_loader(FontLoader);
169///
170/// // Now you can load font files
171/// let handle = server.load_sync::<FontAsset>("fonts/Roboto-Regular.ttf");
172/// ```
173pub struct FontLoader;
174
175impl AssetLoader for FontLoader {
176    type Asset = FontAsset;
177
178    fn extensions(&self) -> &[&str] {
179        &["ttf", "otf", "woff", "woff2", "ttc", "otc"]
180    }
181
182    fn load(&self, ctx: LoadContext<'_>) -> AssetResult<Self::Asset> {
183        // Validate that the data looks like a font
184        let format = FontFormat::detect(ctx.bytes);
185        if format == FontFormat::Unknown && ctx.bytes.len() > 4 {
186            // Only warn for non-empty files that don't have recognized magic bytes
187            tracing::warn!(
188                "Font file '{}' has unrecognized format (magic: {:02x?}), loading anyway",
189                ctx.source.display_path(),
190                &ctx.bytes[..4.min(ctx.bytes.len())]
191            );
192        }
193
194        // Extract name from source path
195        let name = ctx
196            .source
197            .path()
198            .and_then(|p| p.file_name())
199            .and_then(|n| n.to_str())
200            .map(String::from)
201            .unwrap_or_else(|| ctx.source.display_path());
202
203        Ok(FontAsset::new(ctx.bytes.to_vec(), name))
204    }
205
206    fn priority(&self) -> i32 {
207        // Default priority
208        0
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn test_font_format_detection() {
218        // TrueType
219        let ttf_data = [0x00, 0x01, 0x00, 0x00, 0x00, 0x00];
220        assert_eq!(FontFormat::detect(&ttf_data), FontFormat::TrueType);
221
222        // TrueType (alternate magic)
223        let ttf_true = b"true\x00\x00";
224        assert_eq!(FontFormat::detect(ttf_true), FontFormat::TrueType);
225
226        // OpenType
227        let otf_data = b"OTTO\x00\x00";
228        assert_eq!(FontFormat::detect(otf_data), FontFormat::OpenType);
229
230        // WOFF
231        let woff_data = b"wOFF\x00\x00";
232        assert_eq!(FontFormat::detect(woff_data), FontFormat::Woff);
233
234        // WOFF2
235        let woff2_data = b"wOF2\x00\x00";
236        assert_eq!(FontFormat::detect(woff2_data), FontFormat::Woff2);
237
238        // TTC
239        let ttc_data = b"ttcf\x00\x00";
240        assert_eq!(FontFormat::detect(ttc_data), FontFormat::TrueTypeCollection);
241
242        // Unknown
243        let unknown = b"????";
244        assert_eq!(FontFormat::detect(unknown), FontFormat::Unknown);
245
246        // Too short
247        let short = [0x00, 0x01];
248        assert_eq!(FontFormat::detect(&short), FontFormat::Unknown);
249    }
250
251    #[test]
252    fn test_font_asset_creation() {
253        let data: Vec<u8> = vec![0x00, 0x01, 0x00, 0x00, 0x00, 0x10];
254        let asset = FontAsset::new(data.clone(), "test.ttf");
255
256        assert_eq!(asset.name(), "test.ttf");
257        assert_eq!(asset.data(), &data[..]);
258        assert_eq!(asset.size(), 6);
259        assert_eq!(asset.format(), FontFormat::TrueType);
260    }
261
262    #[test]
263    fn test_font_asset_clone() {
264        let data: Vec<u8> = vec![0x00, 0x01, 0x00, 0x00];
265        let asset1 = FontAsset::new(data, "font.ttf");
266        let asset2 = asset1.clone();
267
268        assert_eq!(asset1.name(), asset2.name());
269        assert_eq!(asset1.data(), asset2.data());
270        // Data should be shared via Arc
271        assert!(Arc::ptr_eq(&asset1.data, &asset2.data));
272    }
273
274    #[test]
275    fn test_font_loader_extensions() {
276        let loader = FontLoader;
277        let exts = loader.extensions();
278
279        assert!(exts.contains(&"ttf"));
280        assert!(exts.contains(&"otf"));
281        assert!(exts.contains(&"woff"));
282        assert!(exts.contains(&"woff2"));
283        assert!(exts.contains(&"ttc"));
284        assert!(exts.contains(&"otc"));
285    }
286
287    #[test]
288    fn test_font_loader_load() {
289        use astrelis_assets::AssetSource;
290
291        let loader = FontLoader;
292        let data = vec![0x00, 0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00];
293        let source = AssetSource::disk("fonts/TestFont.ttf");
294        let ctx = LoadContext::new(&source, &data, Some("ttf"));
295
296        let result = loader.load(ctx);
297        assert!(result.is_ok());
298
299        let asset = result.unwrap();
300        assert_eq!(asset.name(), "TestFont.ttf");
301        assert_eq!(asset.data(), &data[..]);
302        assert_eq!(asset.format(), FontFormat::TrueType);
303    }
304
305    #[test]
306    fn test_font_format_extensions() {
307        assert_eq!(FontFormat::TrueType.extension(), "ttf");
308        assert_eq!(FontFormat::OpenType.extension(), "otf");
309        assert_eq!(FontFormat::Woff.extension(), "woff");
310        assert_eq!(FontFormat::Woff2.extension(), "woff2");
311        assert_eq!(FontFormat::TrueTypeCollection.extension(), "ttc");
312        assert_eq!(FontFormat::OpenTypeCollection.extension(), "otc");
313        assert_eq!(FontFormat::Unknown.extension(), "bin");
314    }
315}