gfx: font: add glyph rendering functionality
Helps hexops/mach#877 Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
parent
87c3de78f5
commit
dfbf3e3603
3 changed files with 88 additions and 13 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
|
const std = @import("std");
|
||||||
const mach = @import("../../main.zig");
|
const mach = @import("../../main.zig");
|
||||||
const testing = mach.testing;
|
const testing = mach.testing;
|
||||||
const math = mach.math;
|
const math = mach.math;
|
||||||
|
|
@ -16,12 +17,13 @@ pub const TextRun = TextRunInterface(if (@import("builtin").cpu.arch == .wasm32)
|
||||||
fn FontInterface(comptime T: type) type {
|
fn FontInterface(comptime T: type) type {
|
||||||
assertDecl(T, "initBytes", fn (font_bytes: []const u8) anyerror!T);
|
assertDecl(T, "initBytes", fn (font_bytes: []const u8) anyerror!T);
|
||||||
assertDecl(T, "shape", fn (f: *const T, r: *TextRun) anyerror!void);
|
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;
|
return T;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn TextRunInterface(comptime T: type) type {
|
fn TextRunInterface(comptime T: type) type {
|
||||||
assertField(T, "font_size_px", u32);
|
assertField(T, "font_size_px", f32);
|
||||||
assertField(T, "px_density", u8);
|
assertField(T, "px_density", u8);
|
||||||
assertDecl(T, "init", fn () anyerror!T);
|
assertDecl(T, "init", fn () anyerror!T);
|
||||||
assertDecl(T, "addText", fn (s: *const T, []const u8) void);
|
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 px_per_pt = 4.0 / 3.0;
|
||||||
|
|
||||||
pub const Glyph = struct {
|
pub const Glyph = struct {
|
||||||
glyph_index: u21,
|
glyph_index: u32,
|
||||||
cluster: u32,
|
cluster: u32,
|
||||||
advance: Vec2,
|
advance: Vec2,
|
||||||
offset: 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 {
|
test {
|
||||||
const std = @import("std");
|
|
||||||
std.testing.refAllDeclsRecursive(@This());
|
std.testing.refAllDeclsRecursive(@This());
|
||||||
|
|
||||||
// Load a font
|
// Load a font
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
const font_bytes = @import("font-assets").fira_sans_regular_ttf;
|
const font_bytes = @import("font-assets").fira_sans_regular_ttf;
|
||||||
const font = try Font.initBytes(font_bytes);
|
var font = try Font.initBytes(font_bytes);
|
||||||
defer font.deinit();
|
defer font.deinit(allocator);
|
||||||
|
|
||||||
// Create a text shaper
|
// Create a text shaper
|
||||||
var run = try TextRun.init();
|
var run = try TextRun.init();
|
||||||
|
|
@ -85,6 +104,10 @@ test {
|
||||||
run.addText(text);
|
run.addText(text);
|
||||||
try font.shape(&run);
|
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/1048
|
||||||
// TODO: https://github.com/hexops/mach/issues/1049
|
// TODO: https://github.com/hexops/mach/issues/1049
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ const ft = @import("mach-freetype");
|
||||||
const harfbuzz = @import("mach-harfbuzz");
|
const harfbuzz = @import("mach-harfbuzz");
|
||||||
const TextRun = @import("TextRun.zig");
|
const TextRun = @import("TextRun.zig");
|
||||||
const px_per_pt = @import("../main.zig").px_per_pt;
|
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();
|
const Font = @This();
|
||||||
|
|
||||||
|
|
@ -11,6 +14,7 @@ var freetype_ready: bool = false;
|
||||||
var freetype: ft.Library = undefined;
|
var freetype: ft.Library = undefined;
|
||||||
|
|
||||||
face: ft.Face,
|
face: ft.Face,
|
||||||
|
bitmap: std.ArrayListUnmanaged(RGBA32) = .{},
|
||||||
|
|
||||||
pub fn initFreetype() !void {
|
pub fn initFreetype() !void {
|
||||||
freetype_ready_mu.lock();
|
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);
|
const hb_font = harfbuzz.Font.init(hb_face);
|
||||||
defer hb_font.deinit();
|
defer hb_font.deinit();
|
||||||
|
|
||||||
const font_size_pt = @as(f32, @floatFromInt(r.font_size_px)) / px_per_pt;
|
const font_size_pt = 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_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);
|
hb_font.setPTEM(font_size_pt);
|
||||||
|
|
||||||
// TODO: optionally pass shaping features?
|
// 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;
|
r.positions = r.buffer.getGlyphPositions() orelse return error.OutOfMemory;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(f: *const Font) void {
|
pub fn render(f: *Font, allocator: std.mem.Allocator, glyph_index: u32, opt: RenderOptions) anyerror!RenderedGlyph {
|
||||||
f.face.deinit();
|
// 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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,14 @@ const Glyph = @import("../main.zig").Glyph;
|
||||||
|
|
||||||
const TextRun = @This();
|
const TextRun = @This();
|
||||||
|
|
||||||
font_size_px: u32 = 16.0,
|
font_size_px: f32 = 16.0,
|
||||||
px_density: u8 = 1,
|
px_density: u8 = 1,
|
||||||
|
|
||||||
// Internal / private fields.
|
// Internal / private fields.
|
||||||
buffer: harfbuzz.Buffer,
|
buffer: harfbuzz.Buffer,
|
||||||
index: usize = 0,
|
index: usize = 0,
|
||||||
infos: []harfbuzz.GlyphInfo = undefined,
|
infos: []harfbuzz.GlyphInfo = undefined,
|
||||||
positions: []harfbuzz.GlyphPosition = undefined,
|
positions: []harfbuzz.Position = undefined,
|
||||||
|
|
||||||
pub fn init() anyerror!TextRun {
|
pub fn init() anyerror!TextRun {
|
||||||
return TextRun{
|
return TextRun{
|
||||||
|
|
@ -32,7 +32,7 @@ pub fn next(s: *TextRun) ?Glyph {
|
||||||
const pos = s.positions[s.index];
|
const pos = s.positions[s.index];
|
||||||
s.index += 1;
|
s.index += 1;
|
||||||
return Glyph{
|
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?
|
// TODO: should we expose this? Is there a browser equivalent? do we need it?
|
||||||
// .var1 = @intCast(info.var1),
|
// .var1 = @intCast(info.var1),
|
||||||
// .var2 = @intCast(info.var2),
|
// .var2 = @intCast(info.var2),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue