Merge pull request #61 from mitchellh/font-group

wasm: Discovery, DeferredFace, Group, GroupCache
This commit is contained in:
Mitchell Hashimoto
2022-12-05 21:18:55 -08:00
committed by GitHub
6 changed files with 285 additions and 24 deletions

View File

@ -28,6 +28,20 @@ fetch(url.href).then(response =>
face_free, face_free,
face_render_glyph, face_render_glyph,
face_debug_canvas, face_debug_canvas,
deferred_face_new,
deferred_face_free,
deferred_face_load,
deferred_face_face,
group_new,
group_free,
group_add_face,
group_index_for_codepoint,
group_render_glyph,
group_cache_new,
group_cache_free,
group_cache_index_for_codepoint,
group_cache_render_glyph,
group_cache_atlas_greyscale,
atlas_new, atlas_new,
atlas_free, atlas_free,
atlas_debug_canvas, atlas_debug_canvas,
@ -40,28 +54,43 @@ fetch(url.href).then(response =>
zjs.memory = memory; zjs.memory = memory;
// Create our atlas // Create our atlas
const atlas = atlas_new(512, 0 /* greyscale */); // const atlas = atlas_new(512, 0 /* greyscale */);
// Create some memory for our string // Create some memory for our string
const font = new TextEncoder().encode("monospace"); const font = new TextEncoder().encode("monospace");
const font_ptr = malloc(font.byteLength); const font_ptr = malloc(font.byteLength);
new Uint8Array(memory.buffer, font_ptr).set(font); new Uint8Array(memory.buffer, font_ptr).set(font);
// Initialize our deferred face
const df = deferred_face_new(font_ptr, font.byteLength);
//deferred_face_load(df, 72 /* size */);
//const face = deferred_face_face(df);
// Initialize our font face // Initialize our font face
const face = face_new(font_ptr, font.byteLength, 72 /* size in px */); //const face = face_new(font_ptr, font.byteLength, 72 /* size in px */);
free(font_ptr); free(font_ptr);
// Create our group
const group = group_new(72 /* size */);
group_add_face(group, 0, df);
// Create our group cache
const group_cache = group_cache_new(group);
// Render a glyph // Render a glyph
for (let i = 33; i <= 126; i++) { for (let i = 33; i <= 126; i++) {
face_render_glyph(face, atlas, i); const font_idx = group_cache_index_for_codepoint(group_cache, i, 0, -1);
group_cache_render_glyph(group_cache, font_idx, i, 0);
//face_render_glyph(face, atlas, i);
} }
//face_render_glyph(face, atlas, "橋".codePointAt(0)); //face_render_glyph(face, atlas, "橋".codePointAt(0));
//face_render_glyph(face, atlas, "p".codePointAt(0)); //face_render_glyph(face, atlas, "p".codePointAt(0));
// Debug our canvas // Debug our canvas
face_debug_canvas(face); //face_debug_canvas(face);
// Debug our atlas canvas // Debug our atlas canvas
const atlas = group_cache_atlas_greyscale(group_cache);
const id = atlas_debug_canvas(atlas); const id = atlas_debug_canvas(atlas);
document.getElementById("atlas-canvas").append(zjs.deleteValue(id)); document.getElementById("atlas-canvas").append(zjs.deleteValue(id));

View File

@ -8,6 +8,7 @@ const DeferredFace = @This();
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const fontconfig = @import("fontconfig"); const fontconfig = @import("fontconfig");
const macos = @import("macos"); const macos = @import("macos");
const font = @import("main.zig"); const font = @import("main.zig");
@ -16,6 +17,8 @@ const Library = @import("main.zig").Library;
const Face = @import("main.zig").Face; const Face = @import("main.zig").Face;
const Presentation = @import("main.zig").Presentation; const Presentation = @import("main.zig").Presentation;
const log = std.log.scoped(.deferred_face);
/// The loaded face (once loaded). /// The loaded face (once loaded).
face: ?Face = null, face: ?Face = null,
@ -27,6 +30,10 @@ fc: if (options.backend == .fontconfig_freetype) ?Fontconfig else void =
ct: if (font.Discover == font.discovery.CoreText) ?CoreText else void = ct: if (font.Discover == font.discovery.CoreText) ?CoreText else void =
if (font.Discover == font.discovery.CoreText) null else {}, if (font.Discover == font.discovery.CoreText) null else {},
/// Canvas
wc: if (options.backend == .web_canvas) ?WebCanvas else void =
if (options.backend == .web_canvas) null else {},
/// Fontconfig specific data. This is only present if building with fontconfig. /// Fontconfig specific data. This is only present if building with fontconfig.
pub const Fontconfig = struct { pub const Fontconfig = struct {
/// The pattern for this font. This must be the "render prepared" pattern. /// The pattern for this font. This must be the "render prepared" pattern.
@ -56,6 +63,20 @@ pub const CoreText = struct {
} }
}; };
/// WebCanvas specific data. This is only present when building with canvas.
pub const WebCanvas = struct {
/// The allocator to use for fonts
alloc: Allocator,
/// The string to use for the "font" attribute for the canvas
font_str: [:0]const u8,
pub fn deinit(self: *WebCanvas) void {
self.alloc.free(self.font_str);
self.* = undefined;
}
};
/// Initialize a deferred face that is already pre-loaded. The deferred face /// Initialize a deferred face that is already pre-loaded. The deferred face
/// takes ownership over the loaded face, deinit will deinit the loaded face. /// takes ownership over the loaded face, deinit will deinit the loaded face.
pub fn initLoaded(face: Face) DeferredFace { pub fn initLoaded(face: Face) DeferredFace {
@ -68,8 +89,7 @@ pub fn deinit(self: *DeferredFace) void {
.fontconfig_freetype => if (self.fc) |*fc| fc.deinit(), .fontconfig_freetype => if (self.fc) |*fc| fc.deinit(),
.coretext, .coretext_freetype => if (self.ct) |*ct| ct.deinit(), .coretext, .coretext_freetype => if (self.ct) |*ct| ct.deinit(),
.freetype => {}, .freetype => {},
// TODO .web_canvas => if (self.wc) |*wc| wc.deinit(),
.web_canvas => unreachable,
} }
self.* = undefined; self.* = undefined;
} }
@ -83,6 +103,8 @@ pub inline fn loaded(self: DeferredFace) bool {
/// face so it doesn't have to be freed. /// face so it doesn't have to be freed.
pub fn name(self: DeferredFace) ![:0]const u8 { pub fn name(self: DeferredFace) ![:0]const u8 {
switch (options.backend) { switch (options.backend) {
.freetype => {},
.fontconfig_freetype => if (self.fc) |fc| .fontconfig_freetype => if (self.fc) |fc|
return (try fc.pattern.get(.fullname, 0)).string, return (try fc.pattern.get(.fullname, 0)).string,
@ -91,10 +113,7 @@ pub fn name(self: DeferredFace) ![:0]const u8 {
return display_name.cstringPtr(.utf8) orelse "<unsupported internal encoding>"; return display_name.cstringPtr(.utf8) orelse "<unsupported internal encoding>";
}, },
.freetype => {}, .web_canvas => if (self.wc) |wc| return wc.font_str,
// TODO
.web_canvas => unreachable,
} }
return "TODO: built-in font names"; return "TODO: built-in font names";
@ -125,8 +144,10 @@ pub fn load(
return; return;
}, },
// TODO .web_canvas => {
.web_canvas => unreachable, try self.loadWebCanvas(size);
return;
},
// Unreachable because we must be already loaded or have the // Unreachable because we must be already loaded or have the
// proper configuration for one of the other deferred mechanisms. // proper configuration for one of the other deferred mechanisms.
@ -200,6 +221,15 @@ fn loadCoreTextFreetype(
self.face = try Face.initFile(lib, buf[0..path_slice.len :0], 0, size); self.face = try Face.initFile(lib, buf[0..path_slice.len :0], 0, size);
} }
fn loadWebCanvas(
self: *DeferredFace,
size: font.face.DesiredSize,
) !void {
assert(self.face == null);
const wc = self.wc.?;
self.face = try Face.initNamed(wc.alloc, wc.font_str, size);
}
/// Returns true if this face can satisfy the given codepoint and /// Returns true if this face can satisfy the given codepoint and
/// presentation. If presentation is null, then it just checks if the /// presentation. If presentation is null, then it just checks if the
/// codepoint is present at all. /// codepoint is present at all.
@ -251,8 +281,9 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool {
} }
}, },
// TODO // Canvas always has the codepoint because we have no way of
.web_canvas => unreachable, // really checking and we let the browser handle it.
.web_canvas => return true,
.freetype => {}, .freetype => {},
} }
@ -262,6 +293,57 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool {
unreachable; unreachable;
} }
/// The wasm-compatible API.
pub const Wasm = struct {
const wasm = @import("../os/wasm.zig");
const alloc = wasm.alloc;
export fn deferred_face_new(ptr: [*]const u8, len: usize) ?*DeferredFace {
return deferred_face_new_(ptr, len) catch |err| {
log.warn("error creating deferred face err={}", .{err});
return null;
};
}
fn deferred_face_new_(ptr: [*]const u8, len: usize) !*DeferredFace {
var font_str = try alloc.dupeZ(u8, ptr[0..len]);
errdefer alloc.free(font_str);
var face: DeferredFace = .{
.wc = .{
.alloc = alloc,
.font_str = font_str,
},
};
errdefer face.deinit();
var result = try alloc.create(DeferredFace);
errdefer alloc.destroy(result);
result.* = face;
return result;
}
export fn deferred_face_free(ptr: ?*DeferredFace) void {
if (ptr) |v| {
v.deinit();
alloc.destroy(v);
}
}
export fn deferred_face_load(self: *DeferredFace, pts: u16) void {
self.load(.{}, .{ .points = pts }) catch |err| {
log.warn("error loading deferred face err={}", .{err});
return;
};
}
/// Caller should not free this, the face is owned by the deferred face.
export fn deferred_face_face(self: *DeferredFace) ?*Face {
assert(self.loaded());
return &self.face.?;
}
};
test "preloaded" { test "preloaded" {
const testing = std.testing; const testing = std.testing;
const testFont = @import("test.zig").fontRegular; const testFont = @import("test.zig").fontRegular;

View File

@ -119,7 +119,7 @@ pub fn setSize(self: *Group, size: font.face.DesiredSize) !void {
} }
/// This represents a specific font in the group. /// This represents a specific font in the group.
pub const FontIndex = packed struct { pub const FontIndex = packed struct(u8) {
/// The number of bits we use for the index. /// The number of bits we use for the index.
const idx_bits = 8 - @typeInfo(@typeInfo(Style).Enum.tag_type).Int.bits; const idx_bits = 8 - @typeInfo(@typeInfo(Style).Enum.tag_type).Int.bits;
pub const IndexInt = @Type(.{ .Int = .{ .signedness = .unsigned, .bits = idx_bits } }); pub const IndexInt = @Type(.{ .Int = .{ .signedness = .unsigned, .bits = idx_bits } });
@ -272,13 +272,16 @@ pub fn renderGlyph(
max_height: ?u16, max_height: ?u16,
) !Glyph { ) !Glyph {
// Special-case fonts are rendered directly. // Special-case fonts are rendered directly.
if (index.special()) |sp| switch (sp) { // TODO: web_canvas
.sprite => return try self.sprite.?.renderGlyph( if (options.backend != .web_canvas) {
alloc, if (index.special()) |sp| switch (sp) {
atlas, .sprite => return try self.sprite.?.renderGlyph(
glyph_index, alloc,
), atlas,
}; glyph_index,
),
};
}
const face = &self.faces.get(index.style).items[@intCast(usize, index.idx)]; const face = &self.faces.get(index.style).items[@intCast(usize, index.idx)];
try face.load(self.lib, self.size); try face.load(self.lib, self.size);
@ -303,6 +306,68 @@ pub const Wasm = struct {
result.* = group; result.* = group;
return result; return result;
} }
export fn group_free(ptr: ?*Group) void {
if (ptr) |v| {
v.deinit();
alloc.destroy(v);
}
}
export fn group_add_face(self: *Group, style: u16, face: *font.DeferredFace) void {
return self.addFace(alloc, @intToEnum(Style, style), face.*) catch |err| {
log.warn("error adding face to group err={}", .{err});
return;
};
}
export fn group_set_size(self: *Group, size: u16) void {
return self.setSize(.{ .points = size }) catch |err| {
log.warn("error setting group size err={}", .{err});
return;
};
}
/// Presentation is negative for doesn't matter.
export fn group_index_for_codepoint(self: *Group, cp: u32, style: u16, p: i16) i16 {
const presentation = if (p < 0) null else @intToEnum(Presentation, p);
const idx = self.indexForCodepoint(
cp,
@intToEnum(Style, style),
presentation,
) orelse return -1;
return @intCast(i16, @bitCast(u8, idx));
}
export fn group_render_glyph(
self: *Group,
atlas: *font.Atlas,
idx: i16,
cp: u32,
max_height: u16,
) ?*Glyph {
return group_render_glyph_(self, atlas, idx, cp, max_height) catch |err| {
log.warn("error rendering group glyph err={}", .{err});
return null;
};
}
fn group_render_glyph_(
self: *Group,
atlas: *font.Atlas,
idx_: i16,
cp: u32,
max_height_: u16,
) !*Glyph {
const idx = @bitCast(FontIndex, @intCast(u8, idx_));
const max_height = if (max_height_ <= 0) null else max_height_;
const glyph = try self.renderGlyph(alloc, atlas, idx, cp, max_height);
var result = try alloc.create(Glyph);
errdefer alloc.destroy(result);
result.* = glyph;
return result;
}
}; };
test { test {

View File

@ -231,6 +231,88 @@ test {
} }
} }
/// The wasm-compatible API.
pub const Wasm = struct {
const wasm = @import("../os/wasm.zig");
const alloc = wasm.alloc;
export fn group_cache_new(group: *Group) ?*GroupCache {
return group_cache_new_(group) catch null;
}
fn group_cache_new_(group: *Group) !*GroupCache {
var gc = try GroupCache.init(alloc, group.*);
errdefer gc.deinit(alloc);
var result = try alloc.create(GroupCache);
errdefer alloc.destroy(result);
result.* = gc;
return result;
}
export fn group_cache_free(ptr: ?*GroupCache) void {
if (ptr) |v| {
v.deinit(alloc);
alloc.destroy(v);
}
}
export fn group_cache_set_size(self: *GroupCache, size: u16) void {
return self.setSize(.{ .points = size }) catch |err| {
log.warn("error setting group cache size err={}", .{err});
return;
};
}
/// Presentation is negative for doesn't matter.
export fn group_cache_index_for_codepoint(self: *GroupCache, cp: u32, style: u16, p: i16) i16 {
const presentation = if (p < 0) null else @intToEnum(Presentation, p);
if (self.indexForCodepoint(
alloc,
cp,
@intToEnum(Style, style),
presentation,
)) |idx| {
return @intCast(i16, @bitCast(u8, idx orelse return -1));
} else |err| {
log.warn("error getting index for codepoint from group cache size err={}", .{err});
return -1;
}
}
export fn group_cache_render_glyph(
self: *GroupCache,
idx: i16,
cp: u32,
max_height: u16,
) ?*Glyph {
return group_cache_render_glyph_(self, idx, cp, max_height) catch |err| {
log.warn("error rendering group cache glyph err={}", .{err});
return null;
};
}
fn group_cache_render_glyph_(
self: *GroupCache,
idx_: i16,
cp: u32,
max_height_: u16,
) !*Glyph {
const idx = @bitCast(Group.FontIndex, @intCast(u8, idx_));
const max_height = if (max_height_ <= 0) null else max_height_;
const glyph = try self.renderGlyph(alloc, idx, cp, max_height);
var result = try alloc.create(Glyph);
errdefer alloc.destroy(result);
result.* = glyph;
return result;
}
export fn group_cache_atlas_greyscale(self: *GroupCache) *font.Atlas {
return &self.atlas_greyscale;
}
};
test "resize" { test "resize" {
const testing = std.testing; const testing = std.testing;
const alloc = testing.allocator; const alloc = testing.allocator;

View File

@ -10,9 +10,10 @@ const log = std.log.scoped(.discovery);
/// Discover implementation for the compile options. /// Discover implementation for the compile options.
pub const Discover = switch (options.backend) { pub const Discover = switch (options.backend) {
.freetype => void, // no discovery
.fontconfig_freetype => Fontconfig, .fontconfig_freetype => Fontconfig,
.coretext => CoreText, .coretext, .coretext_freetype => CoreText,
else => void, .web_canvas => void, // no discovery
}; };
/// Descriptor is used to search for fonts. The only required field /// Descriptor is used to search for fonts. The only required field

View File

@ -20,7 +20,9 @@ pub usingnamespace @import("library.zig");
/// If we're targeting wasm then we export some wasm APIs. /// If we're targeting wasm then we export some wasm APIs.
pub usingnamespace if (builtin.target.isWasm()) struct { pub usingnamespace if (builtin.target.isWasm()) struct {
pub usingnamespace Atlas.Wasm; pub usingnamespace Atlas.Wasm;
pub usingnamespace DeferredFace.Wasm;
pub usingnamespace Group.Wasm; pub usingnamespace Group.Wasm;
pub usingnamespace GroupCache.Wasm;
pub usingnamespace face.web_canvas.Wasm; pub usingnamespace face.web_canvas.Wasm;
} else struct {}; } else struct {};