gfx: font: begin adding text shaping via harfbuzz

Helps hexops/mach#877

Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
Stephen Gutekanst 2023-10-03 04:33:26 -07:00
parent 3bcbdc4682
commit 87c3de78f5
7 changed files with 333 additions and 9 deletions

204
src/gfx/font/main.zig Normal file
View file

@ -0,0 +1,204 @@
const mach = @import("../../main.zig");
const testing = mach.testing;
const math = mach.math;
const vec2 = math.vec2;
const Vec2 = math.Vec2;
pub const Font = FontInterface(if (@import("builtin").cpu.arch == .wasm32) {
@panic("TODO: implement wasm/Font.zig");
} else @import("native/Font.zig"));
pub const TextRun = TextRunInterface(if (@import("builtin").cpu.arch == .wasm32)
{
@panic("TODO: implement wasm/TextRun.zig");
} else @import("native/TextRun.zig"));
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);
return T;
}
fn TextRunInterface(comptime T: type) type {
assertField(T, "font_size_px", u32);
assertField(T, "px_density", u8);
assertDecl(T, "init", fn () anyerror!T);
assertDecl(T, "addText", fn (s: *const T, []const u8) void);
assertDecl(T, "next", fn (s: *T) ?Glyph);
assertDecl(T, "deinit", fn (s: *const T) void);
return T;
}
fn assertDecl(comptime T: anytype, comptime name: []const u8, comptime Decl: type) void {
if (!@hasDecl(T, name)) @compileError("Interface missing declaration: " ++ name ++ @typeName(Decl));
const Found = @TypeOf(@field(T, name));
if (Found != Decl) @compileError("Interface decl '" ++ name ++ "'\n\texpected type: " ++ @typeName(Decl) ++ "\n\t found type: " ++ @typeName(Found));
}
fn assertField(comptime T: anytype, comptime name: []const u8, comptime Field: type) void {
if (!@hasField(T, name)) @compileError("Interface missing field: ." ++ name ++ @typeName(Field));
const Found = @TypeOf(@field(@as(T, undefined), name));
if (Found != Field) @compileError("Interface field '" ++ name ++ "'\n\texpected type: " ++ @typeName(Field) ++ "\n\t found type: " ++ @typeName(Found));
}
/// The number of pixels per point, e.g. a 12pt font em box size multiplied by this number tells a
/// the em box size is 16px.
pub const px_per_pt = 4.0 / 3.0;
pub const Glyph = struct {
glyph_index: u21,
cluster: u32,
advance: Vec2,
offset: Vec2,
// TODO: https://github.com/hexops/mach/issues/1048
// TODO: https://github.com/hexops/mach/issues/1049
//
// pub fn eql(a: Glyph, b: Glyph) bool {
// if (a.glyph_index != b.glyph_index) return false;
// if (a.cluster != b.cluster) return false;
// // TODO: add Vec2.eql method
// if (a.advance.v[0] != b.advance.v[0] or a.advance.v[1] != b.advance.v[1]) return false;
// if (a.offset.v[0] != b.offset.v[0] or a.offset.v[1] != b.offset.v[1]) return false;
// return true;
// }
};
test {
const std = @import("std");
std.testing.refAllDeclsRecursive(@This());
// Load a font
const font_bytes = @import("font-assets").fira_sans_regular_ttf;
const font = try Font.initBytes(font_bytes);
defer font.deinit();
// Create a text shaper
var run = try TextRun.init();
run.font_size_px = 12 * px_per_pt;
run.px_density = 1;
defer run.deinit();
const text = "h👩🚀ello world!";
run.addText(text);
try font.shape(&run);
// TODO: https://github.com/hexops/mach/issues/1048
// TODO: https://github.com/hexops/mach/issues/1049
//
// try testing.expect(Glyph, .{
// .glyph_index = 176,
// .cluster = 0,
// .advance = vec2(7.03, 0.00),
// .offset = vec2(0.00, 0.00),
// }).eql(run.next().?);
// try testing.expect(Glyph, .{
// .glyph_index = 0,
// .cluster = 1,
// .advance = vec2(7.99, 0.00),
// .offset = vec2(0.00, 0.00),
// }).eql(run.next().?);
// try testing.expect(Glyph, .{
// .glyph_index = 3,
// .cluster = 1,
// .advance = vec2(0.00, 0.00),
// .offset = vec2(0.00, 0.00),
// }).eql(run.next().?);
// try testing.expect(Glyph, .{
// .glyph_index = 0,
// .cluster = 1,
// .advance = vec2(7.99, 0.00),
// .offset = vec2(0.00, 0.00),
// }).eql(run.next().?);
// try testing.expect(Glyph, .{
// .glyph_index = 3,
// .cluster = 1,
// .advance = vec2(0.00, 0.00),
// .offset = vec2(0.00, 0.00),
// }).eql(run.next().?);
// try testing.expect(Glyph, .{
// .glyph_index = 160,
// .cluster = 15,
// .advance = vec2(6.60, 0.00),
// .offset = vec2(0.00, 0.00),
// }).eql(run.next().?);
// try testing.expect(Glyph, .{
// .glyph_index = 197,
// .cluster = 16,
// .advance = vec2(3.52, 0.00),
// .offset = vec2(0.00, 0.00),
// }).eql(run.next().?);
// try testing.expect(Glyph, .{
// .glyph_index = 197,
// .cluster = 17,
// .advance = vec2(3.39, 0.00),
// .offset = vec2(0.00, 0.00),
// }).eql(run.next().?);
// try testing.expect(Glyph, .{
// .glyph_index = 211,
// .cluster = 18,
// .advance = vec2(7.01, 0.00),
// .offset = vec2(0.00, 0.00),
// }).eql(run.next().?);
// try testing.expect(Glyph, .{
// .glyph_index = 3,
// .cluster = 19,
// .advance = vec2(3.18, 0.00),
// .offset = vec2(0.00, 0.00),
// }).eql(run.next().?);
// try testing.expect(Glyph, .{
// .glyph_index = 254,
// .cluster = 20,
// .advance = vec2(8.48, 0.00),
// .offset = vec2(0.00, 0.00),
// }).eql(run.next().?);
// try testing.expect(Glyph, .{
// .glyph_index = 211,
// .cluster = 21,
// .advance = vec2(7.01, 0.00),
// .offset = vec2(0.00, 0.00),
// }).eql(run.next().?);
// try testing.expect(Glyph, .{
// .glyph_index = 226,
// .cluster = 22,
// .advance = vec2(4.63, 0.00),
// .offset = vec2(0.00, 0.00),
// }).eql(run.next().?);
// try testing.expect(Glyph, .{
// .glyph_index = 197,
// .cluster = 23,
// .advance = vec2(3.39, 0.00),
// .offset = vec2(0.00, 0.00),
// }).eql(run.next().?);
// try testing.expect(Glyph, .{
// .glyph_index = 156,
// .cluster = 24,
// .advance = vec2(7.18, 0.00),
// .offset = vec2(0.00, 0.00),
// }).eql(run.next().?);
// try testing.expect(Glyph, .{
// .glyph_index = 988,
// .cluster = 25,
// .advance = vec2(2.89, 0.00),
// .offset = vec2(0.00, 0.00),
// }).eql(run.next().?);
// var cluster_start: usize = 0;
// while (run.next()) |glyph| {
// if (glyph.cluster != cluster_start) {
// const str = text[cluster_start..glyph.cluster];
// cluster_start = glyph.cluster;
// std.debug.print("^ string: '{s}' (hex: {s})\n", .{ str, std.fmt.fmtSliceHexUpper(str) });
// }
// std.debug.print(".{{ .glyph_index={}, .cluster={}, .advance=vec2({d:.2},{d:.2}), .offset=vec2({d:.2},{d:.2}), }}\n", .{
// glyph.glyph_index,
// glyph.cluster,
// glyph.advance.x(),
// glyph.advance.y(),
// glyph.offset.x(),
// glyph.offset.y(),
// });
// }
}

View file

@ -0,0 +1,57 @@
const std = @import("std");
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 Font = @This();
var freetype_ready_mu: std.Thread.Mutex = .{};
var freetype_ready: bool = false;
var freetype: ft.Library = undefined;
face: ft.Face,
pub fn initFreetype() !void {
freetype_ready_mu.lock();
defer freetype_ready_mu.unlock();
if (!freetype_ready) {
freetype = try ft.Library.init();
freetype_ready = true;
}
}
pub fn initBytes(font_bytes: []const u8) anyerror!Font {
try initFreetype();
return .{
.face = try freetype.createFaceMemory(font_bytes, 0),
};
}
pub fn shape(f: *const Font, r: *TextRun) anyerror!void {
// Guess text segment properties.
r.buffer.guessSegmentProps();
// TODO: Optionally override specific text segment properties?
// buffer.setDirection(.ltr);
// buffer.setScript(.latin);
// buffer.setLanguage(harfbuzz.Language.fromString("en"));
const hb_face = harfbuzz.Face.fromFreetypeFace(f.face);
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);
hb_font.setPTEM(font_size_pt);
// TODO: optionally pass shaping features?
hb_font.shape(r.buffer, null);
r.index = 0;
r.infos = r.buffer.getGlyphInfos();
r.positions = r.buffer.getGlyphPositions() orelse return error.OutOfMemory;
}
pub fn deinit(f: *const Font) void {
f.face.deinit();
}

View file

@ -0,0 +1,48 @@
const std = @import("std");
const harfbuzz = @import("mach-harfbuzz");
const math = @import("../../../main.zig").math;
const vec2 = math.vec2;
const Vec2 = math.Vec2;
const Glyph = @import("../main.zig").Glyph;
const TextRun = @This();
font_size_px: u32 = 16.0,
px_density: u8 = 1,
// Internal / private fields.
buffer: harfbuzz.Buffer,
index: usize = 0,
infos: []harfbuzz.GlyphInfo = undefined,
positions: []harfbuzz.GlyphPosition = undefined,
pub fn init() anyerror!TextRun {
return TextRun{
.buffer = harfbuzz.Buffer.init() orelse return error.OutOfMemory,
};
}
pub fn addText(s: *const TextRun, utf8_text: []const u8) void {
s.buffer.addUTF8(utf8_text, 0, null);
}
pub fn next(s: *TextRun) ?Glyph {
if (s.index >= s.infos.len) return null;
const info = s.infos[s.index];
const pos = s.positions[s.index];
s.index += 1;
return Glyph{
.glyph_index = @intCast(info.codepoint),
// TODO: should we expose this? Is there a browser equivalent? do we need it?
// .var1 = @intCast(info.var1),
// .var2 = @intCast(info.var2),
.cluster = info.cluster,
.advance = vec2(@floatFromInt(pos.x_advance), @floatFromInt(pos.y_advance)).div(&Vec2.splat(256.0)),
.offset = vec2(@floatFromInt(pos.x_offset), @floatFromInt(pos.y_offset)).div(&Vec2.splat(256.0)),
};
}
pub fn deinit(s: *const TextRun) void {
s.buffer.deinit();
return;
}

View file

@ -1,11 +1,17 @@
pub const util = @import("util.zig");
pub const util = @import("util.zig"); // TODO: banish 2-level deep namespaces
pub const Sprite = @import("Sprite.zig");
pub const Atlas = @import("atlas/Atlas.zig");
pub const Text = @import("Text.zig");
pub const FontRenderer = @import("font.zig").FontRenderer;
pub const RGBA32 = @import("font.zig").RGBA32;
pub const Glyph = @import("font.zig").Glyph;
pub const GlyphMetrics = @import("font.zig").GlyphMetrics;
// TODO: integrate font rendering
// pub const RGBA32 = @import("font.zig").RGBA32;
// pub const FontRenderer = @import("font.zig").FontRenderer;
// pub const Glyph = @import("font.zig").Glyph;
// pub const GlyphMetrics = @import("font.zig").GlyphMetrics;
pub const Font = @import("font/main.zig").Font;
pub const TextRun = @import("font/main.zig").TextRun;
pub const Glyph = @import("font/main.zig").Glyph;
test {
const std = @import("std");
@ -13,9 +19,9 @@ test {
// std.testing.refAllDeclsRecursive(@This());
std.testing.refAllDeclsRecursive(util);
// std.testing.refAllDeclsRecursive(Sprite);
std.testing.refAllDeclsRecursive(Atlas);
// std.testing.refAllDeclsRecursive(Text);
std.testing.refAllDeclsRecursive(FontRenderer);
std.testing.refAllDeclsRecursive(RGBA32);
std.testing.refAllDeclsRecursive(Font);
std.testing.refAllDeclsRecursive(TextRun);
std.testing.refAllDeclsRecursive(Glyph);
std.testing.refAllDeclsRecursive(GlyphMetrics);
}