diff --git a/src/gfx/font/main.zig b/src/gfx/font/main.zig index 9a29470d..91cb6c51 100644 --- a/src/gfx/font/main.zig +++ b/src/gfx/font/main.zig @@ -1,3 +1,4 @@ +const std = @import("std"); const mach = @import("../../main.zig"); const testing = mach.testing; const math = mach.math; @@ -16,12 +17,13 @@ pub const TextRun = TextRunInterface(if (@import("builtin").cpu.arch == .wasm32) fn FontInterface(comptime T: type) type { assertDecl(T, "initBytes", fn (font_bytes: []const u8) anyerror!T); assertDecl(T, "shape", fn (f: *const T, r: *TextRun) anyerror!void); - assertDecl(T, "deinit", fn (*const T) void); + assertDecl(T, "render", fn (f: *T, allocator: std.mem.Allocator, glyph_index: u32, opt: RenderOptions) anyerror!RenderedGlyph); + assertDecl(T, "deinit", fn (*T, allocator: std.mem.Allocator) void); return T; } fn TextRunInterface(comptime T: type) type { - assertField(T, "font_size_px", u32); + assertField(T, "font_size_px", f32); assertField(T, "px_density", u8); assertDecl(T, "init", fn () anyerror!T); assertDecl(T, "addText", fn (s: *const T, []const u8) void); @@ -47,7 +49,7 @@ fn assertField(comptime T: anytype, comptime name: []const u8, comptime Field: t pub const px_per_pt = 4.0 / 3.0; pub const Glyph = struct { - glyph_index: u21, + glyph_index: u32, cluster: u32, advance: Vec2, offset: Vec2, @@ -65,14 +67,31 @@ pub const Glyph = struct { // } }; +pub const RGBA32 = extern struct { + r: u8, + g: u8, + b: u8, + a: u8, +}; + +pub const RenderOptions = struct { + font_size_px: f32, +}; + +pub const RenderedGlyph = struct { + bitmap: ?[]const RGBA32, + width: u32, + height: u32, +}; + test { - const std = @import("std"); std.testing.refAllDeclsRecursive(@This()); // Load a font + const allocator = std.testing.allocator; const font_bytes = @import("font-assets").fira_sans_regular_ttf; - const font = try Font.initBytes(font_bytes); - defer font.deinit(); + var font = try Font.initBytes(font_bytes); + defer font.deinit(allocator); // Create a text shaper var run = try TextRun.init(); @@ -85,6 +104,10 @@ test { run.addText(text); try font.shape(&run); + // Test rendering the first glyph + const rendered = try font.render(allocator, 176, .{ .font_size_px = run.font_size_px }); + _ = rendered; + // TODO: https://github.com/hexops/mach/issues/1048 // TODO: https://github.com/hexops/mach/issues/1049 // diff --git a/src/gfx/font/native/Font.zig b/src/gfx/font/native/Font.zig index f2ff714e..efe12582 100644 --- a/src/gfx/font/native/Font.zig +++ b/src/gfx/font/native/Font.zig @@ -3,6 +3,9 @@ const ft = @import("mach-freetype"); const harfbuzz = @import("mach-harfbuzz"); const TextRun = @import("TextRun.zig"); const px_per_pt = @import("../main.zig").px_per_pt; +const RenderedGlyph = @import("../main.zig").RenderedGlyph; +const RenderOptions = @import("../main.zig").RenderOptions; +const RGBA32 = @import("../main.zig").RGBA32; const Font = @This(); @@ -11,6 +14,7 @@ var freetype_ready: bool = false; var freetype: ft.Library = undefined; face: ft.Face, +bitmap: std.ArrayListUnmanaged(RGBA32) = .{}, pub fn initFreetype() !void { freetype_ready_mu.lock(); @@ -40,8 +44,9 @@ pub fn shape(f: *const Font, r: *TextRun) anyerror!void { const hb_font = harfbuzz.Font.init(hb_face); defer hb_font.deinit(); - const font_size_pt = @as(f32, @floatFromInt(r.font_size_px)) / px_per_pt; - hb_font.setScale(@as(i32, @intFromFloat(font_size_pt)) * 256, @as(i32, @intFromFloat(font_size_pt)) * 256); + const font_size_pt = r.font_size_px / px_per_pt; + const font_size_pt_frac: i32 = @intFromFloat(font_size_pt * 256.0); + hb_font.setScale(font_size_pt_frac, font_size_pt_frac); hb_font.setPTEM(font_size_pt); // TODO: optionally pass shaping features? @@ -52,6 +57,53 @@ pub fn shape(f: *const Font, r: *TextRun) anyerror!void { r.positions = r.buffer.getGlyphPositions() orelse return error.OutOfMemory; } -pub fn deinit(f: *const Font) void { - f.face.deinit(); +pub fn render(f: *Font, allocator: std.mem.Allocator, glyph_index: u32, opt: RenderOptions) anyerror!RenderedGlyph { + // TODO: DPI configuration + const dpi = 72; + const font_size_pt = opt.font_size_px / px_per_pt; + const font_size_pt_frac: i32 = @intFromFloat(font_size_pt * 64.0); + f.face.setCharSize(font_size_pt_frac, font_size_pt_frac, dpi, dpi) catch return error.RenderError; + f.face.loadGlyph(glyph_index, .{ .render = true }) catch return error.RenderError; + + const glyph = f.face.glyph(); + const glyph_bitmap = glyph.bitmap(); + const buffer = glyph_bitmap.buffer(); + const width = glyph_bitmap.width(); + const height = glyph_bitmap.rows(); + const margin = 1; + + if (buffer == null) return RenderedGlyph{ + .bitmap = null, + .width = width + (margin * 2), + .height = height + (margin * 2), + }; + + // Add 1 pixel padding to texture to avoid bleeding over other textures. This is part of the + // render() API contract. + f.bitmap.clearRetainingCapacity(); + const num_pixels = (width + (margin * 2)) * (height + (margin * 2)); + // TODO: handle OOM here + f.bitmap.ensureTotalCapacity(allocator, num_pixels) catch return error.RenderError; + f.bitmap.resize(allocator, num_pixels) catch return error.RenderError; + for (f.bitmap.items, 0..) |*data, i| { + const x = i % (width + (margin * 2)); + const y = i / (width + (margin * 2)); + if (x < margin or x > (width + margin) or y < margin or y > (height + margin)) { + data.* = RGBA32{ .r = 0, .g = 0, .b = 0, .a = 0 }; + } else { + const alpha = buffer.?[((y - margin) * width + (x - margin)) % buffer.?.len]; + data.* = RGBA32{ .r = 0, .g = 0, .b = 0, .a = alpha }; + } + } + + return RenderedGlyph{ + .bitmap = f.bitmap.items, + .width = width + (margin * 2), + .height = height + (margin * 2), + }; +} + +pub fn deinit(f: *Font, allocator: std.mem.Allocator) void { + f.face.deinit(); + f.bitmap.deinit(allocator); } diff --git a/src/gfx/font/native/TextRun.zig b/src/gfx/font/native/TextRun.zig index 94bfe523..e624dec3 100644 --- a/src/gfx/font/native/TextRun.zig +++ b/src/gfx/font/native/TextRun.zig @@ -7,14 +7,14 @@ const Glyph = @import("../main.zig").Glyph; const TextRun = @This(); -font_size_px: u32 = 16.0, +font_size_px: f32 = 16.0, px_density: u8 = 1, // Internal / private fields. buffer: harfbuzz.Buffer, index: usize = 0, infos: []harfbuzz.GlyphInfo = undefined, -positions: []harfbuzz.GlyphPosition = undefined, +positions: []harfbuzz.Position = undefined, pub fn init() anyerror!TextRun { return TextRun{ @@ -32,7 +32,7 @@ pub fn next(s: *TextRun) ?Glyph { const pos = s.positions[s.index]; s.index += 1; return Glyph{ - .glyph_index = @intCast(info.codepoint), + .glyph_index = info.codepoint, // TODO: should we expose this? Is there a browser equivalent? do we need it? // .var1 = @intCast(info.var1), // .var2 = @intCast(info.var2),