61494a58
feat(font): freetype face + rasterize
a73x 2026-04-08 06:19
Commit message
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 | } | ||