From 52915012516d05beee86772840d2c6b4b71d0b6f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 5 Dec 2022 20:28:50 -0800 Subject: [PATCH 1/4] font: DeferredFace is wasm-compatible --- example/app.ts | 11 +++- src/font/DeferredFace.zig | 102 ++++++++++++++++++++++++++++++++++---- src/font/main.zig | 1 + 3 files changed, 103 insertions(+), 11 deletions(-) diff --git a/example/app.ts b/example/app.ts index 4026bbc04..5dcd949be 100644 --- a/example/app.ts +++ b/example/app.ts @@ -28,6 +28,10 @@ fetch(url.href).then(response => face_free, face_render_glyph, face_debug_canvas, + deferred_face_new, + deferred_face_free, + deferred_face_load, + deferred_face_face, atlas_new, atlas_free, atlas_debug_canvas, @@ -47,8 +51,13 @@ fetch(url.href).then(response => const font_ptr = malloc(font.byteLength); 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 - 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); // Render a glyph diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index 0c8fd709e..23d8598ae 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -8,6 +8,7 @@ const DeferredFace = @This(); const std = @import("std"); const assert = std.debug.assert; +const Allocator = std.mem.Allocator; const fontconfig = @import("fontconfig"); const macos = @import("macos"); const font = @import("main.zig"); @@ -16,6 +17,8 @@ const Library = @import("main.zig").Library; const Face = @import("main.zig").Face; const Presentation = @import("main.zig").Presentation; +const log = std.log.scoped(.deferred_face); + /// The loaded face (once loaded). 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 = 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. pub const Fontconfig = struct { /// 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 /// takes ownership over the loaded face, deinit will deinit the loaded face. pub fn initLoaded(face: Face) DeferredFace { @@ -68,8 +89,7 @@ pub fn deinit(self: *DeferredFace) void { .fontconfig_freetype => if (self.fc) |*fc| fc.deinit(), .coretext, .coretext_freetype => if (self.ct) |*ct| ct.deinit(), .freetype => {}, - // TODO - .web_canvas => unreachable, + .web_canvas => if (self.wc) |*wc| wc.deinit(), } self.* = undefined; } @@ -83,6 +103,8 @@ pub inline fn loaded(self: DeferredFace) bool { /// face so it doesn't have to be freed. pub fn name(self: DeferredFace) ![:0]const u8 { switch (options.backend) { + .freetype => {}, + .fontconfig_freetype => if (self.fc) |fc| 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 ""; }, - .freetype => {}, - - // TODO - .web_canvas => unreachable, + .web_canvas => if (self.wc) |wc| return wc.font_str, } return "TODO: built-in font names"; @@ -125,8 +144,10 @@ pub fn load( return; }, - // TODO - .web_canvas => unreachable, + .web_canvas => { + try self.loadWebCanvas(size); + return; + }, // Unreachable because we must be already loaded or have the // 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); } +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 /// presentation. If presentation is null, then it just checks if the /// codepoint is present at all. @@ -251,8 +281,9 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool { } }, - // TODO - .web_canvas => unreachable, + // Canvas always has the codepoint because we have no way of + // really checking and we let the browser handle it. + .web_canvas => return true, .freetype => {}, } @@ -262,6 +293,57 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool { 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" { const testing = std.testing; const testFont = @import("test.zig").fontRegular; diff --git a/src/font/main.zig b/src/font/main.zig index 008b955c0..4ef05e25a 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -20,6 +20,7 @@ pub usingnamespace @import("library.zig"); /// If we're targeting wasm then we export some wasm APIs. pub usingnamespace if (builtin.target.isWasm()) struct { pub usingnamespace Atlas.Wasm; + pub usingnamespace DeferredFace.Wasm; pub usingnamespace Group.Wasm; pub usingnamespace face.web_canvas.Wasm; } else struct {}; From aaa0d46b5d221408d1a36ca796243ab9c28ba8ca Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 5 Dec 2022 20:37:17 -0800 Subject: [PATCH 2/4] font: web canvas doesn't support discovery --- src/font/discovery.zig | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/font/discovery.zig b/src/font/discovery.zig index eefb857b3..25d4a318c 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -10,9 +10,10 @@ const log = std.log.scoped(.discovery); /// Discover implementation for the compile options. pub const Discover = switch (options.backend) { + .freetype => void, // no discovery .fontconfig_freetype => Fontconfig, - .coretext => CoreText, - else => void, + .coretext, .coretext_freetype => CoreText, + .web_canvas => void, // no discovery }; /// Descriptor is used to search for fonts. The only required field From 62990bb33e8c87938f3795a8f4123c59dd53b047 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 5 Dec 2022 20:52:03 -0800 Subject: [PATCH 3/4] font: Group is now wasm-compatible --- example/app.ts | 19 ++++++++--- src/font/Group.zig | 81 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 88 insertions(+), 12 deletions(-) diff --git a/example/app.ts b/example/app.ts index 5dcd949be..85eee4d5b 100644 --- a/example/app.ts +++ b/example/app.ts @@ -32,6 +32,11 @@ fetch(url.href).then(response => deferred_face_free, deferred_face_load, deferred_face_face, + group_new, + group_free, + group_add_face, + group_index_for_codepoint, + group_render_glyph, atlas_new, atlas_free, atlas_debug_canvas, @@ -53,22 +58,28 @@ fetch(url.href).then(response => // 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); + //deferred_face_load(df, 72 /* size */); + //const face = deferred_face_face(df); // Initialize our font face //const face = face_new(font_ptr, font.byteLength, 72 /* size in px */); free(font_ptr); + // Create our group + const group = group_new(72 /* size */); + group_add_face(group, 0, df); + // Render a glyph for (let i = 33; i <= 126; i++) { - face_render_glyph(face, atlas, i); + const font_idx = group_index_for_codepoint(group, i, 0, -1); + group_render_glyph(group, atlas, font_idx, i, 0); + //face_render_glyph(face, atlas, i); } //face_render_glyph(face, atlas, "橋".codePointAt(0)); //face_render_glyph(face, atlas, "p".codePointAt(0)); // Debug our canvas - face_debug_canvas(face); + //face_debug_canvas(face); // Debug our atlas canvas const id = atlas_debug_canvas(atlas); diff --git a/src/font/Group.zig b/src/font/Group.zig index 92fbf7100..dbe9cd499 100644 --- a/src/font/Group.zig +++ b/src/font/Group.zig @@ -119,7 +119,7 @@ pub fn setSize(self: *Group, size: font.face.DesiredSize) !void { } /// 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. const idx_bits = 8 - @typeInfo(@typeInfo(Style).Enum.tag_type).Int.bits; pub const IndexInt = @Type(.{ .Int = .{ .signedness = .unsigned, .bits = idx_bits } }); @@ -272,13 +272,16 @@ pub fn renderGlyph( max_height: ?u16, ) !Glyph { // Special-case fonts are rendered directly. - if (index.special()) |sp| switch (sp) { - .sprite => return try self.sprite.?.renderGlyph( - alloc, - atlas, - glyph_index, - ), - }; + // TODO: web_canvas + if (options.backend != .web_canvas) { + if (index.special()) |sp| switch (sp) { + .sprite => return try self.sprite.?.renderGlyph( + alloc, + atlas, + glyph_index, + ), + }; + } const face = &self.faces.get(index.style).items[@intCast(usize, index.idx)]; try face.load(self.lib, self.size); @@ -303,6 +306,68 @@ pub const Wasm = struct { result.* = group; 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 { From 5993528f3319fb1671c540ec52e9420daee96212 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 5 Dec 2022 21:01:56 -0800 Subject: [PATCH 4/4] font: GroupCache is wasm compatible --- example/app.ts | 15 ++++++-- src/font/GroupCache.zig | 82 +++++++++++++++++++++++++++++++++++++++++ src/font/main.zig | 1 + 3 files changed, 95 insertions(+), 3 deletions(-) diff --git a/example/app.ts b/example/app.ts index 85eee4d5b..77d01fb96 100644 --- a/example/app.ts +++ b/example/app.ts @@ -37,6 +37,11 @@ fetch(url.href).then(response => 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_free, atlas_debug_canvas, @@ -49,7 +54,7 @@ fetch(url.href).then(response => zjs.memory = memory; // Create our atlas - const atlas = atlas_new(512, 0 /* greyscale */); + // const atlas = atlas_new(512, 0 /* greyscale */); // Create some memory for our string const font = new TextEncoder().encode("monospace"); @@ -69,10 +74,13 @@ fetch(url.href).then(response => 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 for (let i = 33; i <= 126; i++) { - const font_idx = group_index_for_codepoint(group, i, 0, -1); - group_render_glyph(group, atlas, font_idx, i, 0); + 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)); @@ -82,6 +90,7 @@ fetch(url.href).then(response => //face_debug_canvas(face); // Debug our atlas canvas + const atlas = group_cache_atlas_greyscale(group_cache); const id = atlas_debug_canvas(atlas); document.getElementById("atlas-canvas").append(zjs.deleteValue(id)); diff --git a/src/font/GroupCache.zig b/src/font/GroupCache.zig index c6fd1d2c5..5af5de4cd 100644 --- a/src/font/GroupCache.zig +++ b/src/font/GroupCache.zig @@ -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" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/font/main.zig b/src/font/main.zig index 4ef05e25a..9a6a2b16f 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -22,6 +22,7 @@ pub usingnamespace if (builtin.target.isWasm()) struct { pub usingnamespace Atlas.Wasm; pub usingnamespace DeferredFace.Wasm; pub usingnamespace Group.Wasm; + pub usingnamespace GroupCache.Wasm; pub usingnamespace face.web_canvas.Wasm; } else struct {};