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}