1#[allow(dead_code)]
8#[derive(Debug, Clone)]
9pub struct FontConfig {
10 pub size_px: u32,
11 pub bold: bool,
12 pub italic: bool,
13 pub name: String,
14}
15
16#[allow(dead_code)]
17#[derive(Debug, Clone)]
18pub struct GlyphMetrics {
19 pub advance: f32,
20 pub bearing_x: f32,
21 pub bearing_y: f32,
22 pub width: u32,
23 pub height: u32,
24}
25
26#[allow(dead_code)]
27#[derive(Debug, Clone)]
28pub struct TextLayout {
29 pub glyphs: Vec<GlyphMetrics>,
30 pub total_width: f32,
31 pub line_height: f32,
32}
33
34#[allow(dead_code)]
35#[derive(Debug, Clone)]
36pub struct FontRendererStub {
37 pub config: FontConfig,
38 pub glyph_cache_size: usize,
39}
40
41#[derive(Debug, thiserror::Error)]
43pub enum FontError {
44 #[error("Failed to parse font: {0}")]
45 ParseError(String),
46 #[error("Glyph not found for character: {0}")]
47 GlyphNotFound(char),
48}
49
50pub struct FontRenderer {
53 inner: Option<fontdue::Font>,
54}
55
56impl FontRenderer {
57 pub fn new_stub() -> Self {
60 FontRenderer { inner: None }
61 }
62
63 pub fn from_font_bytes(bytes: &[u8]) -> Result<Self, FontError> {
65 let font = fontdue::Font::from_bytes(bytes, fontdue::FontSettings::default())
66 .map_err(|e| FontError::ParseError(e.to_string()))?;
67 Ok(FontRenderer { inner: Some(font) })
68 }
69
70 pub fn measure_glyph(&self, c: char, px_size: f32) -> GlyphMetrics {
73 if let Some(ref font) = self.inner {
74 let m = font.metrics(c, px_size);
75 GlyphMetrics {
76 advance: m.advance_width,
77 bearing_x: m.xmin as f32,
78 bearing_y: m.ymin as f32,
79 width: m.width as u32,
80 height: m.height as u32,
81 }
82 } else {
83 let advance = if c.is_alphabetic() {
85 px_size * 0.6
86 } else {
87 px_size * 0.5
88 };
89 GlyphMetrics {
90 advance,
91 bearing_x: 0.0,
92 bearing_y: px_size * 0.8,
93 width: (advance as u32).max(1),
94 height: px_size as u32,
95 }
96 }
97 }
98
99 pub fn rasterize(&self, c: char, px_size: f32) -> Option<(usize, usize, Vec<u8>)> {
104 let font = self.inner.as_ref()?;
105 let (metrics, bitmap) = font.rasterize(c, px_size);
106 Some((metrics.width, metrics.height, bitmap))
107 }
108}
109
110#[allow(dead_code)]
111pub fn default_font_config() -> FontConfig {
112 FontConfig {
113 size_px: 16,
114 bold: false,
115 italic: false,
116 name: String::from("sans-serif"),
117 }
118}
119
120#[allow(dead_code)]
121pub fn new_font_renderer_stub(cfg: FontConfig) -> FontRendererStub {
122 FontRendererStub {
123 config: cfg,
124 glyph_cache_size: 256,
125 }
126}
127
128#[allow(dead_code)]
129pub fn measure_glyph(renderer: &FontRendererStub, ch: char) -> GlyphMetrics {
130 let size = renderer.config.size_px as f32;
131 let bold_scale = if renderer.config.bold { 1.1 } else { 1.0 };
132 let char_scale = if ch.is_ascii_alphabetic() { 0.6 } else { 0.5 };
134 let advance = size * char_scale * bold_scale;
135 GlyphMetrics {
136 advance,
137 bearing_x: 0.0,
138 bearing_y: size * 0.8,
139 width: (advance as u32).max(1),
140 height: size as u32,
141 }
142}
143
144#[allow(dead_code)]
145pub fn layout_text(renderer: &FontRendererStub, text: &str) -> TextLayout {
146 let mut glyphs = Vec::with_capacity(text.len());
147 let mut total_width = 0.0f32;
148 for ch in text.chars() {
149 let g = measure_glyph(renderer, ch);
150 total_width += g.advance;
151 glyphs.push(g);
152 }
153 let line_height = renderer.config.size_px as f32 * 1.2;
154 TextLayout {
155 glyphs,
156 total_width,
157 line_height,
158 }
159}
160
161#[allow(dead_code)]
162pub fn glyph_count(layout: &TextLayout) -> usize {
163 layout.glyphs.len()
164}
165
166#[allow(dead_code)]
167pub fn font_config_to_json(cfg: &FontConfig) -> String {
168 format!(
169 "{{\"size_px\":{},\"bold\":{},\"italic\":{},\"name\":\"{}\"}}",
170 cfg.size_px, cfg.bold, cfg.italic, cfg.name
171 )
172}
173
174#[allow(dead_code)]
176pub fn text_bounding_box(layout: &TextLayout) -> [f32; 4] {
177 [0.0, 0.0, layout.total_width, layout.line_height]
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn test_default_font_config() {
186 let cfg = default_font_config();
187 assert_eq!(cfg.size_px, 16);
188 assert!(!cfg.bold);
189 assert!(!cfg.italic);
190 assert!(!cfg.name.is_empty());
191 }
192
193 #[test]
194 fn test_new_font_renderer_stub() {
195 let cfg = default_font_config();
196 let renderer = new_font_renderer_stub(cfg);
197 assert_eq!(renderer.glyph_cache_size, 256);
198 assert_eq!(renderer.config.size_px, 16);
199 }
200
201 #[test]
202 fn test_measure_glyph_nonzero() {
203 let renderer = new_font_renderer_stub(default_font_config());
204 let g = measure_glyph(&renderer, 'A');
205 assert!(g.advance > 0.0);
206 assert!(g.height > 0);
207 }
208
209 #[test]
210 fn test_layout_text_empty() {
211 let renderer = new_font_renderer_stub(default_font_config());
212 let layout = layout_text(&renderer, "");
213 assert_eq!(glyph_count(&layout), 0);
214 assert!((layout.total_width - 0.0).abs() < 1e-6);
215 }
216
217 #[test]
218 fn test_layout_text_nonempty() {
219 let renderer = new_font_renderer_stub(default_font_config());
220 let layout = layout_text(&renderer, "Hi");
221 assert_eq!(glyph_count(&layout), 2);
222 assert!(layout.total_width > 0.0);
223 }
224
225 #[test]
226 fn test_glyph_count() {
227 let renderer = new_font_renderer_stub(default_font_config());
228 let layout = layout_text(&renderer, "abc");
229 assert_eq!(glyph_count(&layout), 3);
230 }
231
232 #[test]
233 fn test_font_config_to_json() {
234 let cfg = default_font_config();
235 let json = font_config_to_json(&cfg);
236 assert!(json.contains("size_px"));
237 assert!(json.contains("sans-serif"));
238 }
239
240 #[test]
241 fn test_text_bounding_box() {
242 let renderer = new_font_renderer_stub(default_font_config());
243 let layout = layout_text(&renderer, "Test");
244 let bb = text_bounding_box(&layout);
245 assert!((bb[0] - 0.0).abs() < 1e-6);
246 assert!((bb[1] - 0.0).abs() < 1e-6);
247 assert!(bb[2] > 0.0);
248 assert!(bb[3] > 0.0);
249 }
250
251 #[test]
252 fn test_bold_advance_larger() {
253 let cfg_normal = default_font_config();
254 let mut cfg_bold = default_font_config();
255 cfg_bold.bold = true;
256 let r_normal = new_font_renderer_stub(cfg_normal);
257 let r_bold = new_font_renderer_stub(cfg_bold);
258 let g_normal = measure_glyph(&r_normal, 'A');
259 let g_bold = measure_glyph(&r_bold, 'A');
260 assert!(g_bold.advance > g_normal.advance);
261 }
262
263 #[test]
266 fn test_stub_fallback_no_font() {
267 let renderer = FontRenderer::new_stub();
268 let m = renderer.measure_glyph('A', 16.0);
269 assert!(m.advance > 0.0);
270 }
271
272 #[test]
273 fn test_layout_accumulates() {
274 let renderer = FontRenderer::new_stub();
275 let glyphs: Vec<f32> = "hello"
276 .chars()
277 .map(|c| renderer.measure_glyph(c, 16.0).advance)
278 .collect();
279 let total: f32 = glyphs.iter().sum();
280 assert!(total > 0.0);
281 assert_eq!(glyphs.len(), 5);
282 }
283
284 #[test]
285 fn test_real_font_metrics() {
286 let font_path = match std::env::var("OXIHUMAN_FONT_DATA") {
287 Ok(p) => p,
288 Err(_) => return, };
290 let bytes = std::fs::read(&font_path).expect("read font file");
291 let renderer = FontRenderer::from_font_bytes(&bytes).expect("parse font");
292 let m = renderer.measure_glyph('A', 16.0);
293 assert!(
294 m.advance > 0.0,
295 "advance should be positive for 'A' at 16px"
296 );
297 }
298
299 #[test]
300 fn test_rasterize_returns_none_without_font() {
301 let renderer = FontRenderer::new_stub();
302 assert!(renderer.rasterize('A', 16.0).is_none());
303 }
304}