psf_rs/
lib.rs

1//! A super simple no std psf2 parser for rust.
2//!
3//! The psfu format is what's used in the linux tty.
4//! You can find the built in psf2 fonts in /usr/share/kbd/consolefonts.
5//!
6//! This doesn't support the original psf.
7
8#![no_std]
9#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
10#![allow(
11    clippy::cast_possible_truncation,
12    clippy::indexing_slicing,
13    clippy::as_conversions,
14    clippy::cast_lossless
15)]
16
17use core::panic;
18
19mod tests;
20
21type HashMap = heapless::IndexMap<[u8; 4], usize, hash32::BuildHasherDefault<ahash::AHasher>, 1024>;
22
23/// Magic bytes that identify psf2.
24const MAGIC: [u8; 4] = [0x72, 0xb5, 0x4a, 0x86];
25
26/// Font flags.
27///
28/// Currently, there is only one flag that specifies
29/// whether there is a unicode table or not.
30#[derive(Clone, Copy, Debug)]
31pub struct Flags {
32    /// Whether a unicode table is present or not.
33    pub unicode: bool,
34}
35
36impl Flags {
37    /// Parses the flags from four bytes.
38    const fn parse(raw: &[u8]) -> Self {
39        Self {
40            unicode: raw[0] == 1,
41        }
42    }
43}
44
45/// The font header.
46#[derive(Clone, Copy, Debug)]
47pub struct Header {
48    /// Magic that is consistent among all psfu files.
49    pub magic: [u8; 4],
50
51    /// The version of psfu used. Currently it's always 0.
52    pub version: u32,
53
54    /// The size of the header in bytes. Pretty much always 32.
55    pub size: u32,
56
57    /// Flags that specify a few things about the font. Currently there's only one.
58    pub flags: Flags,
59
60    /// The number of glyphs.
61    pub length: u32,
62
63    /// The size in bytes of each glyph.
64    pub glyph_size: u32,
65
66    /// The height of each glyph. In this library it always equals `glyph_size`.
67    pub glyph_height: u32,
68
69    /// The width of the glyphs.
70    pub glyph_width: u32,
71}
72
73/// The structure for the font.
74///
75/// # Example
76///
77/// ```rust
78/// use psf_rs::Font;
79///
80/// let font = Font::load(include_bytes!("../test.psfu"));
81///
82/// font.display_glyph('A', |bit, x, y| {
83///    // Stuff
84/// });
85/// ```
86#[derive(Debug)]
87pub struct Font<'a> {
88    /// The font header for this font.
89    pub header: Header,
90
91    /// The data NOT including the header.
92    data: &'a [u8],
93
94    /// The parsed unicode table.
95    unicode: Option<HashMap>,
96}
97
98impl<'a> Font<'a> {
99    /// Converts the unicode table in a font to a hashmap.
100    ///
101    /// # Arguments
102    ///
103    /// * `table` - A byte slice of the actual unicode table.
104    fn parse_unicode_table(table: &[u8]) -> HashMap {
105        let mut result: HashMap = HashMap::new();
106
107        for (i, entry) in table.split(|x| x == &0xff).enumerate() {
108            let mut iter = entry.iter().enumerate();
109            while let Some((j, byte)) = iter.next() {
110                let utf8_len = match byte >> 4usize {
111                    0xc | 0xd => 2,
112                    0xe => 3,
113                    0xf => 4,
114                    _ => 1,
115                };
116
117                let mut key = [0; 4];
118
119                key[..utf8_len].copy_from_slice(&entry[j..j + utf8_len]);
120                result.insert(key, i).unwrap();
121
122                for _ in 0..utf8_len - 1 {
123                    if iter.next().is_none() {
124                        break;
125                    }
126                }
127            }
128        }
129
130        result
131    }
132
133    /// Gets the glyph index of a character by using the fonts own unicode table.
134    /// This index is where the glyph in the font itself.
135    ///
136    /// # Arguments
137    ///
138    /// * `char` - The Unicode Scalar Value of the character you want the index of. (Just cast a char to u32)
139    ///
140    /// # Panics
141    ///
142    /// * If the unicode table flag is set to true, but the table hasn't yet been defined.
143    fn glyph_index(&self, char: u32) -> Option<usize> {
144        // Should work for basic ASCII.
145        if !self.header.flags.unicode || char < 128 {
146            return Some(char as usize);
147        }
148
149        let mut utf8 = [0; 4];
150        char::from_u32(char).unwrap().encode_utf8(&mut utf8);
151
152        self.unicode
153            .as_ref()
154            .expect("unicode table doesn't exist, but header states otherwise")
155            .get(&utf8)
156            .copied()
157    }
158
159    /// Displays a glyph.
160    /// This will NOT trim the glyph, so you will still get the vertical padding.
161    ///
162    /// # Arguments
163    ///
164    /// * `char` - Pretty self explanitory. A character or integer, that must represent a glyph on the ASCII table.
165    /// * `action` - A closure that takes in 3 values, the bit (always 0 or 1), the x, and the y.
166    ///
167    /// # Panics
168    ///
169    /// * If the character can't be properly converted into a u32.
170    /// * If the character can't be described with 2 bytes or less in UTF-8.
171    pub fn display_glyph<T: TryInto<u32>>(&self, char: T, mut action: impl FnMut(u8, u8, u8)) {
172        let Ok(char) = TryInto::<u32>::try_into(char) else {
173            panic!("invalid character index")
174        };
175
176        let char = self.glyph_index(char).map_or('?' as usize, |value| value) as u32;
177
178        let from = self.header.glyph_size * (char);
179        let to = self.header.glyph_size * (char + 1);
180
181        let data = &self.data[from as usize..to as usize];
182        let bytes_in_row = ((self.header.glyph_width as usize + 7) & !7) / 8;
183
184        for (i, row) in data.chunks(bytes_in_row).enumerate() {
185            'row: for (j, byte) in row.iter().enumerate() {
186                for k in 0..8 {
187                    let x = (j as u8 * 8) + k;
188
189                    if x as u32 > self.header.glyph_width {
190                        break 'row;
191                    }
192
193                    // Bit is a u8 that is always either a 0 or a 1.
194                    // "But why not use a boolean?" I hear you ask.
195                    // Every variable in rust is always at least one byte in size,
196                    // So it doesn't do much for saving memory.
197                    let bit = (byte >> (7 - k)) & 1;
198
199                    action(bit, x, i as u8);
200                }
201            }
202        }
203    }
204
205    /// Loads a font.
206    ///
207    /// # Arguments
208    ///
209    /// * `raw` - The raw bytes for the font file itself.
210    ///
211    /// # Panics
212    ///
213    /// * If the file header is incomplete/corrupted in pretty much any way.
214    /// * If the magic doesn't match.
215    /// * If the file size doesn't is bigger than 0x4000 (16384) bytes.
216    #[must_use]
217    pub fn load(raw: &'a [u8]) -> Self {
218        let header_size = as_u32_le(&raw[0x8..0xc]);
219        let header = Header {
220            magic: [raw[0x0], raw[0x1], raw[0x2], raw[0x3]],
221            version: as_u32_le(&raw[0x4..0x8]),
222            size: header_size,
223            flags: Flags::parse(&raw[0xc..0x10]),
224            length: as_u32_le(&raw[0x10..0x14]),
225            glyph_size: as_u32_le(&raw[0x14..0x18]),
226            glyph_height: as_u32_le(&raw[0x18..0x1c]),
227            glyph_width: as_u32_le(&raw[0x1c..0x20]),
228        };
229        let data = &raw[header_size as usize..];
230
231        let font = Self {
232            header,
233            data,
234            unicode: Some(Self::parse_unicode_table(
235                &raw[(header.glyph_size * header.length) as usize..],
236            )),
237        };
238
239        assert!(
240            font.header.magic == MAGIC,
241            "header magic does not match, is this a psf2 font?"
242        );
243
244        font
245    }
246}
247
248/// Converts an array of u8's into one u32.
249const fn as_u32_le(array: &[u8]) -> u32 {
250    assert!(
251        array.len() > 3,
252        "`array` needs to have four elements or more"
253    );
254
255    (array[0] as u32)
256        + ((array[1] as u32) << 8u32)
257        + ((array[2] as u32) << 16u32)
258        + ((array[3] as u32) << 24u32)
259}