1use std::sync::Arc;
26
27use astrelis_assets::{Asset, AssetLoader, AssetResult, LoadContext};
28
29#[derive(Debug, Clone)]
47pub struct FontAsset {
48 data: Arc<[u8]>,
50 name: String,
52}
53
54impl FontAsset {
55 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 pub fn data(&self) -> &[u8] {
65 &self.data
66 }
67
68 pub fn data_arc(&self) -> Arc<[u8]> {
70 self.data.clone()
71 }
72
73 pub fn name(&self) -> &str {
75 &self.name
76 }
77
78 pub fn size(&self) -> usize {
80 self.data.len()
81 }
82
83 pub fn format(&self) -> FontFormat {
85 FontFormat::detect(&self.data)
86 }
87
88 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum FontFormat {
105 TrueType,
107 OpenType,
109 Woff,
111 Woff2,
113 TrueTypeCollection,
115 OpenTypeCollection,
117 Unknown,
119}
120
121impl FontFormat {
122 pub fn detect(data: &[u8]) -> Self {
124 if data.len() < 4 {
125 return FontFormat::Unknown;
126 }
127
128 match &data[0..4] {
129 [0x00, 0x01, 0x00, 0x00] | [b't', b'r', b'u', b'e'] => FontFormat::TrueType,
131 [b'O', b'T', b'T', b'O'] => FontFormat::OpenType,
133 [b'w', b'O', b'F', b'F'] => FontFormat::Woff,
135 [b'w', b'O', b'F', b'2'] => FontFormat::Woff2,
137 [b't', b't', b'c', b'f'] => FontFormat::TrueTypeCollection,
139 _ => FontFormat::Unknown,
140 }
141 }
142
143 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
157pub 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 let format = FontFormat::detect(ctx.bytes);
185 if format == FontFormat::Unknown && ctx.bytes.len() > 4 {
186 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 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 0
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn test_font_format_detection() {
218 let ttf_data = [0x00, 0x01, 0x00, 0x00, 0x00, 0x00];
220 assert_eq!(FontFormat::detect(&ttf_data), FontFormat::TrueType);
221
222 let ttf_true = b"true\x00\x00";
224 assert_eq!(FontFormat::detect(ttf_true), FontFormat::TrueType);
225
226 let otf_data = b"OTTO\x00\x00";
228 assert_eq!(FontFormat::detect(otf_data), FontFormat::OpenType);
229
230 let woff_data = b"wOFF\x00\x00";
232 assert_eq!(FontFormat::detect(woff_data), FontFormat::Woff);
233
234 let woff2_data = b"wOF2\x00\x00";
236 assert_eq!(FontFormat::detect(woff2_data), FontFormat::Woff2);
237
238 let ttc_data = b"ttcf\x00\x00";
240 assert_eq!(FontFormat::detect(ttc_data), FontFormat::TrueTypeCollection);
241
242 let unknown = b"????";
244 assert_eq!(FontFormat::detect(unknown), FontFormat::Unknown);
245
246 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 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}