a73x

61494a58

feat(font): freetype face + rasterize

a73x   2026-04-08 06:19

Commit message
feat(font): freetype face + rasterize

src/font.zig
Old New
@@ -44,6 +44,102 @@ pub fn lookupMonospace(alloc: std.mem.Allocator) !FontLookup {
44 return .{ .path = dup, .index = index }; 44 return .{ .path = dup, .index = index };
45 } 45 }
46 46
47 pub const Glyph = struct {
48 codepoint: u21,
49 width: u32,
50 height: u32,
51 bearing_x: i32,
52 bearing_y: i32,
53 advance_x: i32,
54 bitmap: []u8, // R8, owned
55 };
56
57 pub const Face = struct {
58 alloc: std.mem.Allocator,
59 library: c.FT_Library,
60 face: c.FT_Face,
61 px_size: u32,
62
63 pub fn init(alloc: std.mem.Allocator, path: [:0]const u8, index: c_int, px_size: u32) !Face {
64 var library: c.FT_Library = null;
65 if (c.FT_Init_FreeType(&library) != 0) return error.FtInitFailed;
66 errdefer _ = c.FT_Done_FreeType(library);
67
68 var face: c.FT_Face = null;
69 if (c.FT_New_Face(library, path.ptr, index, &face) != 0) return error.FtNewFaceFailed;
70 errdefer _ = c.FT_Done_Face(face);
71
72 if (c.FT_Set_Pixel_Sizes(face, 0, px_size) != 0) return error.FtSetPixelSizesFailed;
73
74 return .{
75 .alloc = alloc,
76 .library = library,
77 .face = face,
78 .px_size = px_size,
79 };
80 }
81
82 pub fn deinit(self: *Face) void {
83 _ = c.FT_Done_Face(self.face);
84 _ = c.FT_Done_FreeType(self.library);
85 }
86
87 pub fn rasterize(self: *Face, codepoint: u21) !Glyph {
88 const glyph_index = c.FT_Get_Char_Index(self.face, codepoint);
89 if (c.FT_Load_Glyph(self.face, glyph_index, c.FT_LOAD_RENDER) != 0) {
90 return error.FtLoadGlyphFailed;
91 }
92
93 const slot = self.face.*.glyph;
94 const bitmap = slot.*.bitmap;
95 const w: u32 = bitmap.width;
96 const h: u32 = bitmap.rows;
97
98 const pixels = try self.alloc.alloc(u8, @as(usize, w) * @as(usize, h));
99
100 if (w > 0 and h > 0) {
101 // Apply Correction C10: handle nullable buffer + negative pitch
102 const buffer = bitmap.buffer orelse return error.FtBitmapNull;
103 const pitch: i32 = bitmap.pitch;
104 const abs_pitch: u32 = @intCast(@abs(pitch));
105 const top_down = pitch > 0;
106
107 var y: u32 = 0;
108 while (y < h) : (y += 1) {
109 const src_y = if (top_down) y else (h - 1 - y);
110 const src_row = buffer + @as(usize, src_y) * abs_pitch;
111 const dst_row = pixels.ptr + @as(usize, y) * w;
112 @memcpy(dst_row[0..w], src_row[0..w]);
113 }
114 }
115
116 return .{
117 .codepoint = codepoint,
118 .width = w,
119 .height = h,
120 .bearing_x = slot.*.bitmap_left,
121 .bearing_y = slot.*.bitmap_top,
122 .advance_x = @intCast(slot.*.advance.x >> 6),
123 .bitmap = pixels,
124 };
125 }
126
127 pub fn freeGlyph(self: *Face, glyph: Glyph) void {
128 self.alloc.free(glyph.bitmap);
129 }
130
131 pub fn cellWidth(self: *Face) u32 {
132 const m_index = c.FT_Get_Char_Index(self.face, 'M');
133 _ = c.FT_Load_Glyph(self.face, m_index, c.FT_LOAD_DEFAULT);
134 return @intCast(self.face.*.glyph.*.advance.x >> 6);
135 }
136
137 pub fn cellHeight(self: *Face) u32 {
138 const metrics = self.face.*.size.*.metrics;
139 return @intCast((metrics.ascender - metrics.descender) >> 6);
140 }
141 };
142
47 test "lookupMonospace returns a valid font path" { 143 test "lookupMonospace returns a valid font path" {
48 var lookup = try lookupMonospace(std.testing.allocator); 144 var lookup = try lookupMonospace(std.testing.allocator);
49 defer lookup.deinit(std.testing.allocator); 145 defer lookup.deinit(std.testing.allocator);
@@ -52,3 +148,17 @@ test "lookupMonospace returns a valid font path" {
52 const file = try std.fs.openFileAbsolute(lookup.path, .{}); 148 const file = try std.fs.openFileAbsolute(lookup.path, .{});
53 file.close(); 149 file.close();
54 } 150 }
151
152 test "Face rasterizes glyph 'M'" {
153 var lookup = try lookupMonospace(std.testing.allocator);
154 defer lookup.deinit(std.testing.allocator);
155
156 var face = try Face.init(std.testing.allocator, lookup.path, lookup.index, 14);
157 defer face.deinit();
158
159 const glyph = try face.rasterize('M');
160 defer face.freeGlyph(glyph);
161 try std.testing.expect(glyph.width > 0);
162 try std.testing.expect(glyph.height > 0);
163 try std.testing.expect(glyph.bitmap.len == @as(usize, glyph.width) * @as(usize, glyph.height));
164 }