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