Skip to content

Commit ed7716b

Browse files
committed
replace harfbuzz with kb-text-shape
Signed-off-by: Emi <emi@hexops.com>
1 parent 373e57f commit ed7716b

6 files changed

Lines changed: 110 additions & 83 deletions

File tree

build.zig

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,10 @@ pub fn build(b: *std.Build) !void {
126126
.target = target,
127127
.optimize = optimize,
128128
})) |dep| module.linkLibrary(dep.artifact("freetype"));
129-
if (b.lazyDependency("harfbuzz", .{
129+
if (b.lazyDependency("kb-text-shape", .{
130130
.target = target,
131131
.optimize = optimize,
132-
.enable_freetype = true,
133-
})) |dep| module.linkLibrary(dep.artifact("harfbuzz"));
132+
})) |dep| module.linkLibrary(dep.artifact("kb-text-shape"));
134133
if (b.lazyDependency("opusfile", .{
135134
.target = target,
136135
.optimize = .ReleaseFast,
@@ -140,7 +139,7 @@ pub fn build(b: *std.Build) !void {
140139
.optimize = .ReleaseFast,
141140
})) |dep| module.linkLibrary(dep.artifact("opusenc"));
142141
}
143-
142+
144143
if (want_examples) {
145144
for (examples) |example| b.getInstallStep().dependOn(example.install_step);
146145
}
@@ -244,11 +243,10 @@ pub fn build(b: *std.Build) !void {
244243
.target = target,
245244
.optimize = optimize,
246245
})) |dep| unit_tests.root_module.linkLibrary(dep.artifact("freetype"));
247-
if (b.lazyDependency("harfbuzz", .{
246+
if (b.lazyDependency("kb-text-shape", .{
248247
.target = target,
249248
.optimize = optimize,
250-
.enable_freetype = true,
251-
})) |dep| unit_tests.root_module.linkLibrary(dep.artifact("harfbuzz"));
249+
})) |dep| unit_tests.root_module.linkLibrary(dep.artifact("kb-text-shape"));
252250
}
253251

254252
// Documentation

build.zig.zon

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
.hash = "12204cba3a237cd2c4ab983f5a28d9b54e7a9912d8c7c6e38e23140b0171d6e1ebf8",
1818
.lazy = true,
1919
},
20-
.harfbuzz = .{
21-
.url = "https://pkg.hexops.org/pkg/hexops/harfbuzz/c514da98afcf5d9ad6854a7f09192f9ecfaeb061.tar.gz",
22-
.hash = "1220203218ac17e497f3399e08115e73cb9505f1b9f07738eb0c5cc38ca443dec953",
20+
21+
.@"kb-text-shape" = .{
22+
.url = "https://pkg.hexops.org/pkg/hexops/kb-text-shape/89affbcf1b82a3846d43d112e2943db438a3896a.tar.gz",
23+
.hash = "122046928448999745c1d175432205ac99e4cee6e32e0babf0e004340205ab5ba187",
2324
.lazy = true,
2425
},
2526
.mach_objc = .{

src/gfx/font/main.zig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ pub const TextRun = TextRunInterface(if (@import("builtin").cpu.arch == .wasm32)
1111

1212
fn FontInterface(comptime T: type) type {
1313
assertDecl(T, "initBytes", fn (font_bytes: []const u8) anyerror!T);
14-
assertDecl(T, "shape", fn (f: *const T, r: *TextRun) anyerror!void);
14+
assertDecl(T, "shape", fn (f: *T, r: *TextRun) anyerror!void);
1515
assertDecl(T, "render", fn (f: *T, allocator: std.mem.Allocator, glyph_index: u32, opt: RenderOptions) anyerror!RenderedGlyph);
1616
assertDecl(T, "deinit", fn (*T, allocator: std.mem.Allocator) void);
1717
return T;
@@ -21,7 +21,7 @@ fn TextRunInterface(comptime T: type) type {
2121
assertField(T, "font_size_px", f32);
2222
assertField(T, "px_density", u8);
2323
assertDecl(T, "init", fn () anyerror!T);
24-
assertDecl(T, "addText", fn (s: *const T, []const u8) void);
24+
assertDecl(T, "addText", fn (s: *T, []const u8) void);
2525
assertDecl(T, "next", fn (s: *T) ?Glyph);
2626
assertDecl(T, "deinit", fn (s: *const T) void);
2727
return T;

src/gfx/font/native/Font.zig

Lines changed: 37 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const std = @import("std");
2-
const c = @import("ft.zig").c;
2+
const ft = @import("ft.zig").c;
3+
const kb = @import("ft.zig").kb;
34
const TextRun = @import("TextRun.zig");
45
const px_per_pt = @import("../main.zig").px_per_pt;
56
const RenderedGlyph = @import("../main.zig").RenderedGlyph;
@@ -10,75 +11,64 @@ const Font = @This();
1011

1112
var freetype_ready_mu: std.Thread.Mutex = .{};
1213
var freetype_ready: bool = false;
13-
var ft_library: c.FT_Library = null;
14+
var ft_library: ft.FT_Library = null;
1415

15-
face: c.FT_Face,
16+
face: ft.FT_Face,
17+
font_bytes: []const u8,
1618
bitmap: std.ArrayListUnmanaged(RGBA32) = .{},
1719

1820
pub fn initFreetype() !void {
1921
freetype_ready_mu.lock();
2022
defer freetype_ready_mu.unlock();
2123
if (!freetype_ready) {
22-
if (c.FT_Init_FreeType(&ft_library) != 0) return error.FreetypeInitFailed;
24+
if (ft.FT_Init_FreeType(&ft_library) != 0) return error.FreetypeInitFailed;
2325
freetype_ready = true;
2426
}
2527
}
2628

29+
/// font_bytes must remain valid until .deinit() is called.
2730
pub fn initBytes(font_bytes: []const u8) anyerror!Font {
2831
try initFreetype();
29-
var face: c.FT_Face = null;
30-
if (c.FT_New_Memory_Face(ft_library, font_bytes.ptr, @intCast(font_bytes.len), 0, &face) != 0)
32+
var face: ft.FT_Face = null;
33+
if (ft.FT_New_Memory_Face(ft_library, font_bytes.ptr, @intCast(font_bytes.len), 0, &face) != 0)
3134
return error.FreetypeError;
32-
return .{ .face = face };
35+
return .{ .face = face, .font_bytes = font_bytes };
3336
}
3437

35-
pub fn shape(f: *const Font, r: *TextRun) anyerror!void {
36-
// Guess text segment properties.
37-
c.hb_buffer_guess_segment_properties(r.buffer);
38-
// TODO: Optionally override specific text segment properties?
39-
// hb_buffer_set_direction(r.buffer, ...);
40-
// hb_buffer_set_script(r.buffer, ...);
41-
// hb_buffer_set_language(r.buffer, hb_language_from_string("en", -1));
42-
38+
pub fn shape(f: *Font, r: *TextRun) anyerror!void {
39+
const kb_font = kb.kbts_ShapePushFontFromMemory(
40+
r.context,
41+
@constCast(f.font_bytes.ptr),
42+
@intCast(f.font_bytes.len),
43+
0,
44+
) orelse return error.RenderError;
45+
46+
// Get font metrics to know UnitsPerEm for scaling.
47+
var info: kb.kbts_font_info2_1 = std.mem.zeroes(kb.kbts_font_info2_1);
48+
info.Base.Size = @sizeOf(kb.kbts_font_info2_1);
49+
kb.kbts_GetFontInfo2(kb_font, @ptrCast(&info));
50+
if (info.UnitsPerEm == 0) return error.InvalidFont;
51+
r.units_per_em = @floatFromInt(info.UnitsPerEm);
52+
53+
// Set FreeType face size for glyph rendering (still needed for rasterization).
4354
const font_size_pt = r.font_size_px / px_per_pt;
4455
const font_size_pt_frac: i32 = @intFromFloat(font_size_pt * 64.0);
45-
if (c.FT_Set_Char_Size(f.face, font_size_pt_frac, font_size_pt_frac, 0, 0) != 0)
56+
if (ft.FT_Set_Char_Size(f.face, font_size_pt_frac, font_size_pt_frac, 0, 0) != 0)
4657
return error.RenderError;
4758

48-
const hb_face = c.hb_ft_face_create_referenced(f.face) orelse return error.RenderError;
49-
const hb_font = c.hb_font_create(hb_face) orelse return error.RenderError;
50-
defer c.hb_font_destroy(hb_font);
51-
52-
c.hb_font_set_scale(hb_font, font_size_pt_frac, font_size_pt_frac);
53-
c.hb_font_set_ptem(hb_font, font_size_pt);
54-
55-
// TODO: optionally pass shaping features?
56-
c.hb_shape(hb_font, r.buffer, null, 0);
57-
58-
r.index = 0;
59-
var info_count: u32 = 0;
60-
const infos_ptr = c.hb_buffer_get_glyph_infos(r.buffer, &info_count);
61-
r.infos = if (infos_ptr) |p| p[0..info_count] else return error.OutOfMemory;
62-
63-
var pos_count: u32 = 0;
64-
const pos_ptr = c.hb_buffer_get_glyph_positions(r.buffer, &pos_count);
65-
r.positions = if (pos_ptr) |p| p[0..pos_count] else return error.OutOfMemory;
66-
67-
for (r.positions, r.infos) |*pos, info| {
68-
const glyph_index = info.codepoint;
69-
if (c.FT_Load_Glyph(f.face, glyph_index, c.FT_LOAD_DEFAULT) != 0)
70-
return error.RenderError;
71-
const glyph = f.face.*.glyph;
72-
const metrics = glyph.*.metrics;
73-
pos.*.x_offset += @intCast(metrics.horiBearingX);
74-
pos.*.y_offset += @intCast(metrics.horiBearingY);
75-
// TODO: use vertBearingX / vertBearingY for vertical layouts
76-
}
59+
// Store FreeType face for bearing lookups during glyph iteration.
60+
r.ft_face = f.face;
61+
62+
// Perform the full shaping pass: begin → add text → end.
63+
// TODO(font): allow configuration of direction/language by user
64+
kb.kbts_ShapeBegin(r.context, kb.KBTS_DIRECTION_DONT_KNOW, kb.KBTS_LANGUAGE_DONT_KNOW);
65+
kb.kbts_ShapeUtf8(r.context, r.utf8_text.ptr, @intCast(r.utf8_text.len), kb.KBTS_USER_ID_GENERATION_MODE_CODEPOINT_INDEX);
66+
kb.kbts_ShapeEnd(r.context);
7767
}
7868

7969
pub fn render(f: *Font, allocator: std.mem.Allocator, glyph_index: u32, opt: RenderOptions) anyerror!RenderedGlyph {
8070
_ = opt;
81-
if (c.FT_Load_Glyph(f.face, glyph_index, c.FT_LOAD_RENDER) != 0)
71+
if (ft.FT_Load_Glyph(f.face, glyph_index, ft.FT_LOAD_RENDER) != 0)
8272
return error.RenderError;
8373

8474
const glyph = f.face.*.glyph;
@@ -120,6 +110,6 @@ pub fn render(f: *Font, allocator: std.mem.Allocator, glyph_index: u32, opt: Ren
120110
}
121111

122112
pub fn deinit(f: *Font, allocator: std.mem.Allocator) void {
123-
_ = c.FT_Done_Face(f.face);
113+
_ = ft.FT_Done_Face(f.face);
124114
f.bitmap.deinit(allocator);
125115
}

src/gfx/font/native/TextRun.zig

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,83 @@
11
const std = @import("std");
2-
const c = @import("ft.zig").c;
2+
const ft = @import("ft.zig").c;
3+
const kb = @import("ft.zig").kb;
34
const math = @import("../../../main.zig").math;
45
const vec2 = math.vec2;
56
const Vec2 = math.Vec2;
67
const Glyph = @import("../main.zig").Glyph;
8+
const px_per_pt = @import("../main.zig").px_per_pt;
79

810
const TextRun = @This();
911

1012
font_size_px: f32 = 16.0,
1113
px_density: u8 = 1,
1214

1315
// Internal / private fields.
14-
buffer: *c.hb_buffer_t,
15-
index: usize = 0,
16-
infos: []c.hb_glyph_info_t = undefined,
17-
positions: []c.hb_glyph_position_t = undefined,
16+
context: *kb.kbts_shape_context,
17+
utf8_text: []const u8 = &.{},
18+
run: kb.kbts_run = undefined,
19+
has_run: bool = false,
20+
units_per_em: f32 = undefined,
21+
ft_face: ft.FT_Face = null,
1822

1923
pub fn init() anyerror!TextRun {
2024
return TextRun{
21-
.buffer = c.hb_buffer_create() orelse return error.OutOfMemory,
25+
.context = kb.kbts_CreateShapeContext(null, null) orelse return error.OutOfMemory,
2226
};
2327
}
2428

25-
pub fn addText(s: *const TextRun, utf8_text: []const u8) void {
26-
c.hb_buffer_add_utf8(s.buffer, utf8_text.ptr, @intCast(utf8_text.len), 0, @intCast(utf8_text.len));
29+
/// utf8_text must remain valid until .deinit() is called.
30+
pub fn addText(s: *TextRun, utf8_text: []const u8) void {
31+
s.utf8_text = utf8_text;
2732
}
2833

2934
pub fn next(s: *TextRun) ?Glyph {
30-
if (s.index >= s.infos.len) return null;
31-
const info = s.infos[s.index];
32-
const pos = s.positions[s.index];
33-
s.index += 1;
34-
return Glyph{
35-
.glyph_index = info.codepoint,
36-
// TODO: should we expose this? Is there a browser equivalent? do we need it?
37-
// .var1 = @intCast(info.var1),
38-
// .var2 = @intCast(info.var2),
39-
.cluster = info.cluster,
40-
.advance = vec2(@floatFromInt(pos.x_advance), @floatFromInt(pos.y_advance)).div(&Vec2.splat(64.0)),
41-
.offset = vec2(@floatFromInt(pos.x_offset), @floatFromInt(pos.y_offset)).div(&Vec2.splat(64.0)),
42-
};
35+
while (true) {
36+
if (s.has_run) {
37+
var glyph_ptr: ?*kb.kbts_glyph = null;
38+
if (kb.kbts_GlyphIteratorNext(&s.run.Glyphs, &glyph_ptr) != 0) {
39+
const glyph = glyph_ptr.?;
40+
// Scale from font design units to points (not pixels) to match
41+
// the convention used by FreeType's bearing metrics.
42+
const font_size_pt = s.font_size_px / px_per_pt;
43+
const scale = font_size_pt / s.units_per_em;
44+
45+
// Get FreeType bearing offsets for baseline-relative positioning.
46+
// kb handles shaping but not rasterization metrics; FreeType's
47+
// horiBearingX/Y position the bitmap relative to the pen/baseline.
48+
var bearing_x: f32 = 0;
49+
var bearing_y: f32 = 0;
50+
if (ft.FT_Load_Glyph(s.ft_face, glyph.Id, ft.FT_LOAD_DEFAULT) == 0) {
51+
const metrics = s.ft_face.*.glyph.*.metrics;
52+
bearing_x = @as(f32, @floatFromInt(metrics.horiBearingX)) / 64.0;
53+
bearing_y = @as(f32, @floatFromInt(metrics.horiBearingY)) / 64.0;
54+
}
55+
56+
return Glyph{
57+
.glyph_index = @intCast(glyph.Id),
58+
.cluster = @intCast(glyph.UserIdOrCodepointIndex),
59+
.advance = vec2(
60+
@as(f32, @floatFromInt(glyph.AdvanceX)) * scale,
61+
@as(f32, @floatFromInt(glyph.AdvanceY)) * scale,
62+
),
63+
.offset = vec2(
64+
@as(f32, @floatFromInt(glyph.OffsetX)) * scale + bearing_x,
65+
@as(f32, @floatFromInt(glyph.OffsetY)) * scale + bearing_y,
66+
),
67+
};
68+
}
69+
}
70+
// Try to get the next run.
71+
if (kb.kbts_ShapeRun(s.context, &s.run) != 0) {
72+
s.has_run = true;
73+
continue;
74+
}
75+
// No more runs.
76+
s.has_run = false;
77+
return null;
78+
}
4379
}
4480

4581
pub fn deinit(s: *const TextRun) void {
46-
c.hb_buffer_destroy(s.buffer);
82+
kb.kbts_DestroyShapeContext(s.context);
4783
}

src/gfx/font/native/ft.zig

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
pub const c = @cImport({
22
@cInclude("ft2build.h");
33
@cInclude("freetype/freetype.h");
4-
@cInclude("harfbuzz/hb.h");
5-
@cInclude("harfbuzz/hb-ft.h");
4+
});
5+
6+
pub const kb = @cImport({
7+
@cInclude("kb_text_shape.h");
68
});

0 commit comments

Comments
 (0)