1use std::collections::HashMap;
5
6use anyhow::{Context as _, Result, bail};
7
8use crate::graphics::Graphics;
9use crate::texture::TextureId;
10
11#[derive(Copy, Clone, Debug, PartialEq, Eq)]
13pub struct Glyph {
14 pub src_pos: [u32; 2],
16 pub src_size: [u32; 2],
18 pub offset: [i32; 2],
20 pub advance: i32,
22}
23
24#[derive(Clone, Debug)]
26pub struct GridLayout {
27 pub cell: [u32; 2],
29 pub chars: String,
32 pub proportional: bool,
36 pub spacing: u32,
38}
39
40impl GridLayout {
41 pub fn ascii(width: u32, height: u32) -> Self {
43 Self {
44 cell: [width, height],
45 chars: (' '..='~').collect(),
46 proportional: false,
47 spacing: 1,
48 }
49 }
50}
51
52#[derive(Clone, Debug)]
60pub struct BitmapFont {
61 texture: TextureId,
62 texture_size: [u32; 2],
63 glyphs: HashMap<char, Glyph>,
64 kerning: HashMap<(char, char), i32>,
65 line_height: u32,
66 fallback: Option<char>,
67}
68
69impl BitmapFont {
70 pub fn new(
72 texture: TextureId,
73 texture_size: [u32; 2],
74 line_height: u32,
75 glyphs: HashMap<char, Glyph>,
76 ) -> Self {
77 let fallback = glyphs.contains_key(&'?').then_some('?');
78 Self {
79 texture,
80 texture_size,
81 glyphs,
82 kerning: HashMap::new(),
83 line_height,
84 fallback,
85 }
86 }
87
88 pub fn from_grid(gfx: &mut Graphics, png: &[u8], layout: &GridLayout) -> Result<Self> {
91 let img = image::load_from_memory(png)
92 .context("decode glyph sheet")?
93 .to_rgba8();
94 let (w, h) = img.dimensions();
95 let texture = gfx.create_texture_rgba(w, h, img.as_raw(), Some("bitmap_font"));
96 Ok(Self::from_grid_rgba(texture, [w, h], img.as_raw(), layout))
97 }
98
99 pub fn from_grid_rgba(
105 texture: TextureId,
106 size: [u32; 2],
107 rgba: &[u8],
108 layout: &GridLayout,
109 ) -> Self {
110 let needed = (size[0] as usize)
111 .checked_mul(size[1] as usize)
112 .and_then(|n| n.checked_mul(4))
113 .unwrap_or_else(|| {
114 panic!(
115 "from_grid_rgba: a {}x{} sheet is too large",
116 size[0], size[1]
117 )
118 });
119 assert!(
120 rgba.len() >= needed,
121 "from_grid_rgba: {} bytes of RGBA for a {}x{} sheet, need {needed}",
122 rgba.len(),
123 size[0],
124 size[1]
125 );
126 let [cw, ch] = [layout.cell[0].max(1), layout.cell[1].max(1)];
127 let cols = size[0] / cw;
128 let rows = size[1] / ch;
129 let alpha = |x: u32, y: u32| rgba[(y as usize * size[0] as usize + x as usize) * 4 + 3];
131 let mut glyphs = HashMap::new();
132 for (i, c) in layout.chars.chars().enumerate() {
133 let i = i as u32;
134 if i >= cols * rows {
135 break;
136 }
137 let x0 = (i % cols) * cw;
138 let y0 = (i / cols) * ch;
139 let glyph = if layout.proportional {
140 let used: Vec<u32> = (0..cw)
141 .filter(|&dx| (0..ch).any(|dy| alpha(x0 + dx, y0 + dy) > 0))
142 .collect();
143 match (used.first(), used.last()) {
144 (Some(&l), Some(&r)) => Glyph {
145 src_pos: [x0 + l, y0],
146 src_size: [r - l + 1, ch],
147 offset: [0, 0],
148 advance: (r - l + 1 + layout.spacing) as i32,
149 },
150 _ => Glyph {
151 src_pos: [x0, y0],
152 src_size: [0, 0],
153 offset: [0, 0],
154 advance: (cw / 2).max(1) as i32,
155 },
156 }
157 } else {
158 Glyph {
159 src_pos: [x0, y0],
160 src_size: [cw, ch],
161 offset: [0, 0],
162 advance: cw as i32,
163 }
164 };
165 glyphs.insert(c, glyph);
166 }
167 Self::new(texture, size, ch, glyphs)
168 }
169
170 pub fn from_bmfont(gfx: &mut Graphics, fnt: &str, page_png: &[u8]) -> Result<Self> {
173 let img = image::load_from_memory(page_png)
174 .context("decode BMFont page")?
175 .to_rgba8();
176 let (w, h) = img.dimensions();
177 let texture = gfx.create_texture_rgba(w, h, img.as_raw(), Some("bmfont"));
178 Self::parse_bmfont(texture, [w, h], fnt)
179 }
180
181 pub fn bmfont_page_file(fnt: &str) -> Option<String> {
183 fnt.lines()
184 .filter_map(parse_line)
185 .find(|(tag, kv)| *tag == "page" && kv.get("id").is_none_or(|id| id == "0"))
186 .and_then(|(_, kv)| kv.get("file").cloned())
187 }
188
189 pub fn parse_bmfont(texture: TextureId, texture_size: [u32; 2], fnt: &str) -> Result<Self> {
191 let mut line_height = None;
192 let mut glyphs = HashMap::new();
193 let mut kerning = HashMap::new();
194 for (tag, kv) in fnt.lines().filter_map(parse_line) {
195 let num = |k: &str| -> Result<i32> {
196 kv.get(k)
197 .with_context(|| format!("BMFont {tag}: missing {k}"))?
198 .parse()
199 .with_context(|| format!("BMFont {tag}: bad {k}"))
200 };
201 match tag {
202 "common" => {
203 line_height = Some(num("lineHeight")?.max(1) as u32);
204 if kv.get("pages").is_some_and(|p| p != "1") {
205 bail!("BMFont with more than one page is not supported");
206 }
207 }
208 "char" => {
209 let Some(c) = char::from_u32(num("id")? as u32) else {
210 continue;
211 };
212 glyphs.insert(
213 c,
214 Glyph {
215 src_pos: [num("x")?.max(0) as u32, num("y")?.max(0) as u32],
216 src_size: [num("width")?.max(0) as u32, num("height")?.max(0) as u32],
217 offset: [num("xoffset")?, num("yoffset")?],
218 advance: num("xadvance")?,
219 },
220 );
221 }
222 "kerning" => {
223 let (Some(a), Some(b)) = (
224 char::from_u32(num("first")? as u32),
225 char::from_u32(num("second")? as u32),
226 ) else {
227 continue;
228 };
229 kerning.insert((a, b), num("amount")?);
230 }
231 _ => {}
232 }
233 }
234 let line_height = line_height.context("BMFont: missing `common` line")?;
235 let mut font = Self::new(texture, texture_size, line_height, glyphs);
236 font.kerning = kerning;
237 Ok(font)
238 }
239
240 pub fn builtin(gfx: &mut Graphics) -> Self {
243 let (size, rgba) = builtin_sheet();
244 let texture = gfx.create_texture_rgba(size[0], size[1], &rgba, Some("builtin_font"));
245 Self::from_grid_rgba(texture, size, &rgba, &GridLayout::ascii(6, 8))
246 }
247
248 pub fn set_fallback(&mut self, c: Option<char>) {
251 self.fallback = c;
252 }
253
254 pub fn texture(&self) -> TextureId {
255 self.texture
256 }
257
258 pub fn texture_size(&self) -> [u32; 2] {
259 self.texture_size
260 }
261
262 pub fn line_height(&self) -> u32 {
264 self.line_height
265 }
266
267 pub fn glyph(&self, c: char) -> Option<&Glyph> {
268 self.glyphs.get(&c)
269 }
270
271 pub fn measure(&self, s: &str, scale: u32) -> [u32; 2] {
276 let mut width = 0i64;
277 let mut lines = 0u32;
278 for line in text_lines(s) {
279 lines = lines.saturating_add(1);
280 let mut pen = 0i64;
281 let mut left = 0i64;
282 let mut right = 0i64;
283 self.layout_line(line, |g, x| {
284 let x0 = i64::from(x) + i64::from(g.offset[0]);
285 left = left.min(x0);
286 right = right.max(x0 + i64::from(g.src_size[0]));
287 pen = i64::from(x) + i64::from(g.advance);
288 });
289 width = width.max(pen.max(right) - left);
290 }
291 let scale = scale.max(1);
292 let width = u32::try_from(width.max(0)).unwrap_or(u32::MAX);
293 [
294 width.saturating_mul(scale),
295 lines.saturating_mul(self.line_height).saturating_mul(scale),
296 ]
297 }
298
299 pub(crate) fn layout_line(&self, line: &str, mut f: impl FnMut(&Glyph, i32)) {
301 let mut pen = 0;
302 let mut prev: Option<char> = None;
303 for c in line.chars() {
304 let Some((c, g)) = self.glyphs.get(&c).map(|g| (c, g)).or_else(|| {
305 self.fallback
306 .and_then(|f| self.glyphs.get(&f).map(|g| (f, g)))
307 }) else {
308 continue;
309 };
310 if let Some(p) = prev {
311 pen += self.kerning.get(&(p, c)).copied().unwrap_or(0);
312 }
313 f(g, pen);
314 pen += g.advance;
315 prev = Some(c);
316 }
317 }
318}
319
320pub(crate) fn text_lines(s: &str) -> impl Iterator<Item = &str> {
323 s.split('\n').map(|l| l.strip_suffix('\r').unwrap_or(l))
324}
325
326fn parse_line(line: &str) -> Option<(&str, HashMap<String, String>)> {
328 let line = line.trim();
329 let (tag, mut rest) = line.split_once(char::is_whitespace).unwrap_or((line, ""));
330 if tag.is_empty() {
331 return None;
332 }
333 let mut kv = HashMap::new();
334 loop {
335 rest = rest.trim_start();
336 let Some((key, after)) = rest.split_once('=') else {
337 break;
338 };
339 let (value, next) = if let Some(quoted) = after.strip_prefix('"') {
340 match quoted.split_once('"') {
341 Some((v, n)) => (v, n),
342 None => (quoted, ""),
343 }
344 } else {
345 after.split_once(char::is_whitespace).unwrap_or((after, ""))
346 };
347 kv.insert(key.trim().to_owned(), value.to_owned());
348 rest = next;
349 }
350 Some((tag, kv))
351}
352
353#[rustfmt::skip]
355const BUILTIN_GLYPHS: [[u8; 7]; 95] = [
356 [0x00,0x00,0x00,0x00,0x00,0x00,0x00], [0x04,0x04,0x04,0x04,0x04,0x00,0x04], [0x0A,0x0A,0x00,0x00,0x00,0x00,0x00], [0x0A,0x0A,0x1F,0x0A,0x1F,0x0A,0x0A], [0x04,0x0F,0x14,0x0E,0x05,0x1E,0x04], [0x19,0x19,0x02,0x04,0x08,0x13,0x13], [0x08,0x14,0x14,0x08,0x15,0x12,0x0D], [0x04,0x04,0x00,0x00,0x00,0x00,0x00], [0x02,0x04,0x08,0x08,0x08,0x04,0x02], [0x08,0x04,0x02,0x02,0x02,0x04,0x08], [0x00,0x04,0x15,0x0E,0x15,0x04,0x00], [0x00,0x04,0x04,0x1F,0x04,0x04,0x00], [0x00,0x00,0x00,0x00,0x06,0x04,0x08], [0x00,0x00,0x00,0x1F,0x00,0x00,0x00], [0x00,0x00,0x00,0x00,0x00,0x06,0x06], [0x01,0x01,0x02,0x04,0x08,0x10,0x10], [0x0E,0x11,0x13,0x15,0x19,0x11,0x0E], [0x04,0x0C,0x04,0x04,0x04,0x04,0x0E], [0x0E,0x11,0x01,0x06,0x08,0x10,0x1F], [0x1F,0x01,0x02,0x06,0x01,0x11,0x0E], [0x02,0x06,0x0A,0x12,0x1F,0x02,0x02], [0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E], [0x06,0x08,0x10,0x1E,0x11,0x11,0x0E], [0x1F,0x01,0x02,0x04,0x08,0x08,0x08], [0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E], [0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C], [0x00,0x06,0x06,0x00,0x06,0x06,0x00], [0x00,0x06,0x06,0x00,0x06,0x04,0x08], [0x02,0x04,0x08,0x10,0x08,0x04,0x02], [0x00,0x00,0x1F,0x00,0x1F,0x00,0x00], [0x08,0x04,0x02,0x01,0x02,0x04,0x08], [0x0E,0x11,0x01,0x02,0x04,0x00,0x04], [0x0E,0x11,0x17,0x15,0x17,0x10,0x0E], [0x0E,0x11,0x11,0x1F,0x11,0x11,0x11], [0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E], [0x0E,0x11,0x10,0x10,0x10,0x11,0x0E], [0x1E,0x11,0x11,0x11,0x11,0x11,0x1E], [0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F], [0x1F,0x10,0x10,0x1E,0x10,0x10,0x10], [0x0E,0x11,0x10,0x17,0x11,0x11,0x0F], [0x11,0x11,0x11,0x1F,0x11,0x11,0x11], [0x0E,0x04,0x04,0x04,0x04,0x04,0x0E], [0x07,0x02,0x02,0x02,0x02,0x12,0x0C], [0x11,0x12,0x14,0x18,0x14,0x12,0x11], [0x10,0x10,0x10,0x10,0x10,0x10,0x1F], [0x11,0x1B,0x15,0x15,0x11,0x11,0x11], [0x11,0x11,0x19,0x15,0x13,0x11,0x11], [0x0E,0x11,0x11,0x11,0x11,0x11,0x0E], [0x1E,0x11,0x11,0x1E,0x10,0x10,0x10], [0x0E,0x11,0x11,0x11,0x15,0x12,0x0D], [0x1E,0x11,0x11,0x1E,0x14,0x12,0x11], [0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E], [0x1F,0x04,0x04,0x04,0x04,0x04,0x04], [0x11,0x11,0x11,0x11,0x11,0x11,0x0E], [0x11,0x11,0x11,0x11,0x11,0x0A,0x04], [0x11,0x11,0x11,0x15,0x15,0x15,0x0A], [0x11,0x11,0x0A,0x04,0x0A,0x11,0x11], [0x11,0x11,0x0A,0x04,0x04,0x04,0x04], [0x1F,0x01,0x02,0x04,0x08,0x10,0x1F], [0x0E,0x08,0x08,0x08,0x08,0x08,0x0E], [0x10,0x10,0x08,0x04,0x02,0x01,0x01], [0x0E,0x02,0x02,0x02,0x02,0x02,0x0E], [0x04,0x0A,0x11,0x00,0x00,0x00,0x00], [0x00,0x00,0x00,0x00,0x00,0x00,0x1F], [0x08,0x04,0x00,0x00,0x00,0x00,0x00], [0x00,0x00,0x0E,0x01,0x0F,0x11,0x0F], [0x10,0x10,0x16,0x19,0x11,0x11,0x1E], [0x00,0x00,0x0E,0x10,0x10,0x11,0x0E], [0x01,0x01,0x0D,0x13,0x11,0x11,0x0F], [0x00,0x00,0x0E,0x11,0x1F,0x10,0x0E], [0x06,0x09,0x08,0x1C,0x08,0x08,0x08], [0x00,0x0F,0x11,0x11,0x0F,0x01,0x0E], [0x10,0x10,0x16,0x19,0x11,0x11,0x11], [0x04,0x00,0x0C,0x04,0x04,0x04,0x0E], [0x02,0x00,0x06,0x02,0x02,0x12,0x0C], [0x10,0x10,0x12,0x14,0x18,0x14,0x12], [0x0C,0x04,0x04,0x04,0x04,0x04,0x0E], [0x00,0x00,0x1A,0x15,0x15,0x11,0x11], [0x00,0x00,0x16,0x19,0x11,0x11,0x11], [0x00,0x00,0x0E,0x11,0x11,0x11,0x0E], [0x00,0x00,0x1E,0x11,0x1E,0x10,0x10], [0x00,0x00,0x0D,0x13,0x0F,0x01,0x01], [0x00,0x00,0x16,0x19,0x10,0x10,0x10], [0x00,0x00,0x0E,0x10,0x0E,0x01,0x1E], [0x08,0x08,0x1C,0x08,0x08,0x09,0x06], [0x00,0x00,0x11,0x11,0x11,0x13,0x0D], [0x00,0x00,0x11,0x11,0x11,0x0A,0x04], [0x00,0x00,0x11,0x11,0x15,0x15,0x0A], [0x00,0x00,0x11,0x0A,0x04,0x0A,0x11], [0x00,0x00,0x11,0x11,0x0F,0x01,0x0E], [0x00,0x00,0x1F,0x02,0x04,0x08,0x1F], [0x02,0x04,0x04,0x08,0x04,0x04,0x02], [0x04,0x04,0x04,0x04,0x04,0x04,0x04], [0x08,0x04,0x04,0x02,0x04,0x04,0x08], [0x00,0x00,0x08,0x15,0x02,0x00,0x00], ];
452
453pub(crate) fn builtin_sheet() -> ([u32; 2], Vec<u8>) {
455 const COLS: u32 = 16;
456 let size = [COLS * 6, 6 * 8];
457 let mut rgba = vec![0u8; (size[0] * size[1] * 4) as usize];
458 for (i, rows) in BUILTIN_GLYPHS.iter().enumerate() {
459 let (cx, cy) = ((i as u32 % COLS) * 6, (i as u32 / COLS) * 8);
460 for (dy, bits) in rows.iter().enumerate() {
461 for dx in 0..5u32 {
462 if bits & (0x10 >> dx) != 0 {
463 let o = (((cy + dy as u32) * size[0] + cx + dx) * 4) as usize;
464 rgba[o..o + 4].copy_from_slice(&[255; 4]);
465 }
466 }
467 }
468 }
469 (size, rgba)
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475
476 fn builtin() -> BitmapFont {
477 let (size, rgba) = builtin_sheet();
478 BitmapFont::from_grid_rgba(TextureId(0), size, &rgba, &GridLayout::ascii(6, 8))
479 }
480
481 #[test]
482 fn builtin_measures_whole_pixels() {
483 let f = builtin();
484 assert_eq!(f.line_height(), 8);
485 assert_eq!(f.measure("Hi!", 1), [18, 8]);
486 assert_eq!(f.measure("Hi!", 3), [54, 24]);
487 assert_eq!(f.measure("ab\nabcd", 2), [48, 32]);
488 assert_eq!(f.measure("é", 1), [6, 8]);
490 }
491
492 #[test]
493 fn proportional_grid_trims_glyphs() {
494 let (size, rgba) = builtin_sheet();
495 let layout = GridLayout {
496 proportional: true,
497 ..GridLayout::ascii(6, 8)
498 };
499 let f = BitmapFont::from_grid_rgba(TextureId(0), size, &rgba, &layout);
500 assert_eq!(f.glyph('i').map(|g| g.advance), Some(4));
502 assert_eq!(f.glyph('M').map(|g| g.advance), Some(6));
503 assert_eq!(f.glyph(' ').map(|g| g.advance), Some(3));
504 }
505
506 #[test]
507 fn bmfont_text_parses() {
508 let fnt = r#"info face="Tiny" size=8
509common lineHeight=10 base=8 scaleW=64 scaleH=64 pages=1
510page id=0 file="tiny_0.png"
511chars count=2
512char id=65 x=0 y=0 width=5 height=7 xoffset=0 yoffset=1 xadvance=6 page=0
513char id=86 x=6 y=0 width=5 height=7 xoffset=0 yoffset=1 xadvance=6 page=0
514kernings count=1
515kerning first=65 second=86 amount=-1
516"#;
517 assert_eq!(
518 BitmapFont::bmfont_page_file(fnt).as_deref(),
519 Some("tiny_0.png")
520 );
521 let f = BitmapFont::parse_bmfont(TextureId(0), [64, 64], fnt).unwrap();
522 assert_eq!(f.line_height(), 10);
523 assert_eq!(
524 f.glyph('A'),
525 Some(&Glyph {
526 src_pos: [0, 0],
527 src_size: [5, 7],
528 offset: [0, 1],
529 advance: 6
530 })
531 );
532 assert_eq!(f.measure("AV", 1), [11, 10]);
534 }
535
536 #[test]
537 fn measure_counts_left_overhang_and_crlf() {
538 let fnt = "common lineHeight=8 pages=1
539char id=106 x=0 y=0 width=4 height=8 xoffset=-2 yoffset=0 xadvance=3
540";
541 let f = BitmapFont::parse_bmfont(TextureId(0), [16, 16], fnt).unwrap();
542 assert_eq!(f.measure("j", 1), [5, 8]);
544 let b = builtin();
545 assert_eq!(b.measure("ab\r\ncd", 1), b.measure("ab\ncd", 1));
546 assert_eq!(b.measure("W", u32::MAX)[0], u32::MAX);
547 }
548
549 #[test]
550 #[should_panic(expected = "need")]
551 fn short_rgba_is_rejected() {
552 let _ = BitmapFont::from_grid_rgba(TextureId(0), [6, 8], &[0; 4], &GridLayout::ascii(6, 8));
553 }
554}