Skip to main content

badterm_core/
fontfile.rs

1//! Font files from disk, for a directory entry's font: raw DOS fonts
2//! (`.fnt`, `.f08`, `.f14`, `.f16`: 256 glyphs of 8 pixels a row, one byte
3//! per row) and Linux console fonts (`.psf`, versions 1 and 2), 8 pixels
4//! wide. They become 8x8, 8x14 or 8x16 fonts, as CTerm's loaded fonts are
5//! (see [`crate::screen::Look::load_font`]); other heights are stretched
6//! to the nearest of those.
7
8use anyhow::{Result, bail};
9
10/// The extensions of the files read.
11pub const EXTENSIONS: &[&str] = &["fnt", "f08", "f14", "f16", "psf", "psfu"];
12
13/// The 256 glyphs, ready for [`crate::screen::Look::load_font`].
14pub fn parse(data: &[u8]) -> Result<Vec<u8>> {
15    let (height, glyphs) = if data.starts_with(&[0x36, 0x04]) && data.len() >= 4 {
16        // PSF 1: mode, then the bytes (and rows) per glyph.
17        let h = data[3] as usize;
18        (h, &data[4..])
19    } else if data.starts_with(&[0x72, 0xB5, 0x4A, 0x86]) && data.len() >= 32 {
20        let word = |i: usize| u32::from_le_bytes([data[i], data[i + 1], data[i + 2], data[i + 3]]) as usize;
21        let (header, count, size, h, w) = (word(8), word(16), word(20), word(24), word(28));
22        if w > 8 {
23            bail!("the font is {w} pixels wide; BadTerm's cells are 8");
24        }
25        if size != h || count < 256 {
26            bail!("not a font of 256 or more glyphs of one byte a row");
27        }
28        (h, data.get(header..).unwrap_or_default())
29    } else if !data.is_empty() && data.len().is_multiple_of(256) {
30        (data.len() / 256, data)
31    } else {
32        bail!("not a font file BadTerm reads (.fnt, .f08, .f14, .f16 or .psf)");
33    };
34    if !(4..=32).contains(&height) || glyphs.len() < 256 * height {
35        bail!("not a font file BadTerm reads (glyphs {height} rows high)");
36    }
37    let to = match height {
38        ..=10 => 8,
39        11..=14 => 14,
40        _ => 16,
41    };
42    Ok((0..256).flat_map(|c| (0..to).map(move |y| glyphs[c * height + y * height / to])).collect())
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48
49    #[test]
50    fn raw_and_psf() {
51        let raw: Vec<u8> = (0..256 * 16).map(|i| (i % 251) as u8).collect();
52        assert_eq!(parse(&raw).unwrap(), raw);
53        let mut psf1 = vec![0x36, 0x04, 0, 14];
54        psf1.extend(std::iter::repeat_n(0x18, 256 * 14));
55        assert_eq!(parse(&psf1).unwrap().len(), 256 * 14);
56        // PSF 2, 512 glyphs 8x12: the first 256, stretched to 14 rows.
57        let mut psf2 = vec![0x72, 0xB5, 0x4A, 0x86];
58        for v in [0u32, 32, 0, 512, 12, 12, 8] {
59            psf2.extend(v.to_le_bytes());
60        }
61        psf2.extend((0..512 * 12).map(|i| (i / 12) as u8));
62        let f = parse(&psf2).unwrap();
63        assert_eq!((f.len(), f[14 * 65]), (256 * 14, 65));
64        // 9 pixels wide, or not a font.
65        psf2[28] = 9;
66        assert!(parse(&psf2).is_err());
67        assert!(parse(b"hello").is_err());
68    }
69}