a73x

61494a58

feat(font): freetype face + rasterize

a73x   2026-04-08 06:19


diff --git a/src/font.zig b/src/font.zig
index 7dcb9ca..cd75d12 100644
--- a/src/font.zig
+++ b/src/font.zig
@@ -44,6 +44,102 @@ pub fn lookupMonospace(alloc: std.mem.Allocator) !FontLookup {
    return .{ .path = dup, .index = index };
}

pub const Glyph = struct {
    codepoint: u21,
    width: u32,
    height: u32,
    bearing_x: i32,
    bearing_y: i32,
    advance_x: i32,
    bitmap: []u8, // R8, owned
};

pub const Face = struct {
    alloc: std.mem.Allocator,
    library: c.FT_Library,
    face: c.FT_Face,
    px_size: u32,

    pub fn init(alloc: std.mem.Allocator, path: [:0]const u8, index: c_int, px_size: u32) !Face {
        var library: c.FT_Library = null;
        if (c.FT_Init_FreeType(&library) != 0) return error.FtInitFailed;
        errdefer _ = c.FT_Done_FreeType(library);

        var face: c.FT_Face = null;
        if (c.FT_New_Face(library, path.ptr, index, &face) != 0) return error.FtNewFaceFailed;
        errdefer _ = c.FT_Done_Face(face);

        if (c.FT_Set_Pixel_Sizes(face, 0, px_size) != 0) return error.FtSetPixelSizesFailed;

        return .{
            .alloc = alloc,
            .library = library,
            .face = face,
            .px_size = px_size,
        };
    }

    pub fn deinit(self: *Face) void {
        _ = c.FT_Done_Face(self.face);
        _ = c.FT_Done_FreeType(self.library);
    }

    pub fn rasterize(self: *Face, codepoint: u21) !Glyph {
        const glyph_index = c.FT_Get_Char_Index(self.face, codepoint);
        if (c.FT_Load_Glyph(self.face, glyph_index, c.FT_LOAD_RENDER) != 0) {
            return error.FtLoadGlyphFailed;
        }

        const slot = self.face.*.glyph;
        const bitmap = slot.*.bitmap;
        const w: u32 = bitmap.width;
        const h: u32 = bitmap.rows;

        const pixels = try self.alloc.alloc(u8, @as(usize, w) * @as(usize, h));

        if (w > 0 and h > 0) {
            // Apply Correction C10: handle nullable buffer + negative pitch
            const buffer = bitmap.buffer orelse return error.FtBitmapNull;
            const pitch: i32 = bitmap.pitch;
            const abs_pitch: u32 = @intCast(@abs(pitch));
            const top_down = pitch > 0;

            var y: u32 = 0;
            while (y < h) : (y += 1) {
                const src_y = if (top_down) y else (h - 1 - y);
                const src_row = buffer + @as(usize, src_y) * abs_pitch;
                const dst_row = pixels.ptr + @as(usize, y) * w;
                @memcpy(dst_row[0..w], src_row[0..w]);
            }
        }

        return .{
            .codepoint = codepoint,
            .width = w,
            .height = h,
            .bearing_x = slot.*.bitmap_left,
            .bearing_y = slot.*.bitmap_top,
            .advance_x = @intCast(slot.*.advance.x >> 6),
            .bitmap = pixels,
        };
    }

    pub fn freeGlyph(self: *Face, glyph: Glyph) void {
        self.alloc.free(glyph.bitmap);
    }

    pub fn cellWidth(self: *Face) u32 {
        const m_index = c.FT_Get_Char_Index(self.face, 'M');
        _ = c.FT_Load_Glyph(self.face, m_index, c.FT_LOAD_DEFAULT);
        return @intCast(self.face.*.glyph.*.advance.x >> 6);
    }

    pub fn cellHeight(self: *Face) u32 {
        const metrics = self.face.*.size.*.metrics;
        return @intCast((metrics.ascender - metrics.descender) >> 6);
    }
};

test "lookupMonospace returns a valid font path" {
    var lookup = try lookupMonospace(std.testing.allocator);
    defer lookup.deinit(std.testing.allocator);
@@ -52,3 +148,17 @@ test "lookupMonospace returns a valid font path" {
    const file = try std.fs.openFileAbsolute(lookup.path, .{});
    file.close();
}

test "Face rasterizes glyph 'M'" {
    var lookup = try lookupMonospace(std.testing.allocator);
    defer lookup.deinit(std.testing.allocator);

    var face = try Face.init(std.testing.allocator, lookup.path, lookup.index, 14);
    defer face.deinit();

    const glyph = try face.rasterize('M');
    defer face.freeGlyph(glyph);
    try std.testing.expect(glyph.width > 0);
    try std.testing.expect(glyph.height > 0);
    try std.testing.expect(glyph.bitmap.len == @as(usize, glyph.width) * @as(usize, glyph.height));
}