Skip to content

Instantly share code, notes, and snippets.

@lygaret
Created September 16, 2022 03:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lygaret/8c2ba2ab7eb24c4cd91de2fc45acd5a3 to your computer and use it in GitHub Desktop.
Save lygaret/8c2ba2ab7eb24c4cd91de2fc45acd5a3 to your computer and use it in GitHub Desktop.
PSF (PC Screen Font) comptime loader in zig
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,
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment