psf format font loader for zig

published
permalink
https://accidental.cc/notes/2022/psf-zig/

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


I’ve been playing with a kernel, and wanted to be able to embed a font at comptime; so far, it’s kinda working, but this little library has been helpful.

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,
    });
}