const std = @import("std"); /// PSF font-file loading, into an easy to render format /// /// {buildFont} takes a path to a font file, and returns a struct wrapping that /// font with the ability to easily get an iterator of pixels over charactors /// /// @see https://www.win.tue.nl/~aeb/linux/kbd/font-formats-1.html const PSF1_MAGIC: [2]u8 = .{ 0x36, 0x04 }; const PSF2_MAGIC: [4]u8 = .{ 0x72, 0xb5, 0x4a, 0x86 }; const PSF1_MODE_HAS512 = 0x01; const PSF1_MODE_HASTAB = 0x02; const PSF1_MODE_HASSEQ = 0x04; /// build an iterator that can walk over the pixel data in a given PSFFont /// iterates over single bits, aligning forward a bite when hitting the glyph /// width. /// /// user assumes responsibility for coordinating iteration in x/y coordinates, /// at the row pitch level, no signal for "end of row" is provided. fn PSFPixelIterator(comptime T: type) type { return struct { const Self = @This(); font: *const T, glyph: u32, index: usize = 0, // the current index into the glyph byte array bitcount: u8 = 0, // the number of bits we've read out of the working glyph workglyph: u8 = undefined, // the byte we're currently destructing to get bits /// Resets the iterator to the initial state. pub fn reset(self: *Self) void { self.resetIndex(0); } /// aligns to the next byte pub fn alignForward(self: *Self) void { if (self.bitcount > 0) { self.resetIndex(self.index + 1); } } /// Returns whether the next pixel is set or not, /// or null if we've read all pixels for the glyph pub fn next(self: *Self) ?bool { if (self.index >= self.font.glyph_size) return null; defer { self.bitcount += 1; // todo: memorize the min? this happens on every iteration if (self.bitcount >= std.math.min(8, self.font.glyph_width)) { self.resetIndex(self.index + 1); } } return @shlWithOverflow(u8, self.workglyph, 1, &self.workglyph); } // reset to the given index // used for full resets and byte-to-byte transitions fn resetIndex(self: *Self, index: usize) void { self.index = index; self.bitcount = 0; // if we're about to roll out of the glyph, don't // otherwise, the last iteration (which would return null) panics for out-of-bounds if (index < self.font.glyph_size) { self.workglyph = self.font.glyphs[self.glyph][index]; } } }; } /// build a font struct from the given file /// will cause a compile error if the file is not parsable as a PSF v1 or v2 pub fn buildFont(comptime path: []const u8) type { const file = @embedFile(path); if (std.mem.eql(u8, file[0..2], PSF1_MAGIC[0..2])) { return buildPSF1Font(file); } if (std.mem.eql(u8, file[0..4], PSF2_MAGIC[0..4])) { return buildPSF2Font(file); } @compileError("file isn't PSF (no matching magic)"); } // options for the common PSF struct generator const PSFHeaderMetrics = struct { file: []const u8, header_size: u32, glyph_count: u32, glyph_size: u32, glyph_width: u32, glyph_height: u32, }; // given font metrics, generate a struct type which can read font glyphs at compile time fn buildPSFCommon(comptime options: PSFHeaderMetrics) type { return struct { const Self = @This(); const PixelIterator = PSFPixelIterator(Self); // explicitly sized per the header file pub const Glyph = [options.glyph_size]u8; pub const GlyphSet = [options.glyph_count]Glyph; glyphs: GlyphSet, // todo: unicode table glyph_count: u32, glyph_width: u32, glyph_height: u32, glyph_size: u32, pub fn init() Self { // get a stream over the embedded file and skip the header var glyphStream = std.io.fixedBufferStream(options.file); glyphStream.seekTo(options.header_size) catch unreachable; comptime var index = 0; var data: GlyphSet = undefined; // then read every glyph out of the file into the struct // without the eval branch quota, compiler freaks out in read for backtracking @setEvalBranchQuota(100000); inline while(index < options.glyph_count) : (index += 1) { _ = glyphStream.read(data[index][0..]) catch unreachable; } return Self{ .glyphs = data, .glyph_count = options.glyph_count, .glyph_width = options.glyph_width, .glyph_height = options.glyph_height, .glyph_size = options.glyph_size, }; } pub fn pixelIterator(self: *const Self, glyph: u32) PixelIterator { var iter = PixelIterator{ .font = self, .glyph = glyph }; iter.reset(); return iter; } }; } /// return a PSF2 font struct, fn buildPSF2Font(comptime file: []const u8) type { var stream = std.io.fixedBufferStream(file); var reader = stream.reader(); _ = try reader.readIntLittle(u32); // magic (already validated) _ = try reader.readIntLittle(u32); // version const header_size = try reader.readIntLittle(u32); _ = try reader.readIntLittle(u32); // flags (1 if unicode table) const glyph_count = try reader.readIntLittle(u32); const glyph_size = try reader.readIntLittle(u32); const glyph_height = try reader.readIntLittle(u32); const glyph_width = try reader.readIntLittle(u32); return buildPSFCommon(.{ .file = file, .header_size = header_size, // 8 u32 fields, = 32 bytes .glyph_count = glyph_count, .glyph_size = glyph_size, .glyph_width = glyph_width, .glyph_height = glyph_height, }); } fn buildPSF1Font(comptime file: []const u8) type { var stream = std.io.fixedBufferStream(file); var reader = stream.reader(); _ = try reader.readIntLittle(u16); // magic (already validated) const font_mode = try reader.readIntLittle(u8); // version const glyph_height = try reader.readIntLittle(u8); const glyph_count = if (font_mode & PSF1_MODE_HAS512 == 1) 512 else 256; return buildPSFCommon(.{ .file = file, .header_size = 4, // bytes .glyph_count = glyph_count, // always 256, unless 512 mode .glyph_size = glyph_height, // because each row is always 1 byte, so it takes height bytes for a glyph .glyph_width = 8, .glyph_height = glyph_height, }); }