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:
parent
3bcbdc4682
commit
87c3de78f5
7 changed files with 333 additions and 9 deletions
204
src/gfx/font/main.zig
Normal file
204
src/gfx/font/main.zig
Normal 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(),
|
||||
// });
|
||||
// }
|
||||
}
|
||||
57
src/gfx/font/native/Font.zig
Normal file
57
src/gfx/font/native/Font.zig
Normal 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();
|
||||
}
|
||||
48
src/gfx/font/native/TextRun.zig
Normal file
48
src/gfx/font/native/TextRun.zig
Normal 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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue