Created
September 16, 2022 03:27
-
-
Save lygaret/8c2ba2ab7eb24c4cd91de2fc45acd5a3 to your computer and use it in GitHub Desktop.
PSF (PC Screen Font) comptime loader in zig
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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