Merge pull request #60 from mitchellh/font-canvas

wasm: Canvas-based Rasterizer
This commit is contained in:
Mitchell Hashimoto
2022-12-05 17:02:37 -08:00
committed by GitHub
20 changed files with 5174 additions and 93 deletions

3
.gitmodules vendored
View File

@ -34,3 +34,6 @@
[submodule "vendor/pixman"] [submodule "vendor/pixman"]
path = vendor/pixman path = vendor/pixman
url = https://github.com/freedesktop/pixman.git url = https://github.com/freedesktop/pixman.git
[submodule "vendor/zig-js"]
path = vendor/zig-js
url = https://github.com/mitchellh/zig-js.git

View File

@ -7,6 +7,7 @@ const fontconfig = @import("pkg/fontconfig/build.zig");
const freetype = @import("pkg/freetype/build.zig"); const freetype = @import("pkg/freetype/build.zig");
const harfbuzz = @import("pkg/harfbuzz/build.zig"); const harfbuzz = @import("pkg/harfbuzz/build.zig");
const imgui = @import("pkg/imgui/build.zig"); const imgui = @import("pkg/imgui/build.zig");
const js = @import("vendor/zig-js/build.zig");
const libxml2 = @import("vendor/zig-libxml2/libxml2.zig"); const libxml2 = @import("vendor/zig-libxml2/libxml2.zig");
const libuv = @import("pkg/libuv/build.zig"); const libuv = @import("pkg/libuv/build.zig");
const libpng = @import("pkg/libpng/build.zig"); const libpng = @import("pkg/libpng/build.zig");
@ -120,7 +121,10 @@ pub fn build(b: *std.build.Builder) !void {
wasm.setBuildMode(mode); wasm.setBuildMode(mode);
wasm.setOutputDir("zig-out"); wasm.setOutputDir("zig-out");
wasm.addOptions("build_options", exe_options);
// Wasm-specific deps // Wasm-specific deps
wasm.addPackage(js.pkg);
wasm.addPackage(tracylib.pkg); wasm.addPackage(tracylib.pkg);
wasm.addPackage(utf8proc.pkg); wasm.addPackage(utf8proc.pkg);
_ = try utf8proc.link(b, wasm); _ = try utf8proc.link(b, wasm);
@ -135,6 +139,7 @@ pub fn build(b: *std.build.Builder) !void {
const main_test = b.addTest("src/main_wasm.zig"); const main_test = b.addTest("src/main_wasm.zig");
main_test.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .wasi }); main_test.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .wasi });
main_test.addOptions("build_options", exe_options); main_test.addOptions("build_options", exe_options);
main_test.addPackage(js.pkg);
test_step.dependOn(&main_test.step); test_step.dependOn(&main_test.step);
} }
@ -235,6 +240,11 @@ fn addDeps(
_ = try macos.link(b, step, .{}); _ = try macos.link(b, step, .{});
} }
// Wasm
if (step.target.getCpuArch() == .wasm32) {
step.addPackage(js.pkg);
}
// We always statically compile glad // We always statically compile glad
step.addIncludePath("vendor/glad/include/"); step.addIncludePath("vendor/glad/include/");
step.addCSourceFile("vendor/glad/src/gl.c", &.{}); step.addCSourceFile("vendor/glad/src/gl.c", &.{});

4
example/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.parcel-cache/
dist/
node_modules/
example.wasm*

69
example/app.ts Normal file
View File

@ -0,0 +1,69 @@
import { ZigJS } from 'zig-js';
const zjs = new ZigJS();
const importObject = {
module: {},
env: {
log: (ptr: number, len: number) => {
const view = new DataView(zjs.memory.buffer, ptr, Number(len));
const str = new TextDecoder('utf-8').decode(view);
console.log(str);
},
},
...zjs.importObject(),
};
const url = new URL('ghostty-wasm.wasm', import.meta.url);
fetch(url.href).then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes, importObject)
).then(results => {
const {
memory,
malloc,
free,
face_new,
face_free,
face_render_glyph,
face_debug_canvas,
atlas_new,
atlas_free,
atlas_debug_canvas,
} = results.instance.exports;
// Give us access to the zjs value for debugging.
globalThis.zjs = zjs;
console.log(zjs);
// Initialize our zig-js memory
zjs.memory = memory;
// Create our atlas
const atlas = atlas_new(512, 0 /* greyscale */);
// Create some memory for our string
const font = new TextEncoder().encode("monospace");
const font_ptr = malloc(font.byteLength);
new Uint8Array(memory.buffer, font_ptr).set(font);
// Initialize our font face
const face = face_new(font_ptr, font.byteLength, 72 /* size in px */);
free(font_ptr);
// Render a glyph
for (let i = 33; i <= 126; i++) {
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);
// Debug our atlas canvas
const id = atlas_debug_canvas(atlas);
document.getElementById("atlas-canvas").append(zjs.deleteValue(id));
//face_free(face);
});

View File

@ -1,36 +1,15 @@
<!DOCTYPE html> <!doctype html>
<html> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8"/>
<title>WASM Example</title> <title>Ghostty Example</title>
<script type="module" src="app.ts"></script>
</head> </head>
<body> <body>
<script> <p>Open your console, we are just debugging here.</p>
const importObject = { <p>The font rendering canvas should show below. This shows a single glyph.</p>
module: {}, <div><div id="face-canvas" style="display: inline-block; border: 1px solid red;"></div></div>
env: { <p>The current font atlas is rendered below.</p>
// memory: new WebAssembly.Memory({ initial: 256 }), <div><div id="atlas-canvas" style="display: inline-block; border: 1px solid green;"></div></div>
}
};
fetch('ghostty-wasm.wasm').then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes, importObject)
).then(results => {
const {
atlas_new,
atlas_free,
atlas_reserve,
free,
memory,
} = results.instance.exports;
const atlas = atlas_new(512, 0);
const reg = atlas_reserve(atlas, 10, 10);
free(reg);
atlas_free(atlas);
});
</script>
</body> </body>
</html> </html>

4436
example/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

22
example/package.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "ghostty example",
"version": "0.1.0",
"description": "Example showing ghostty and wasm.",
"source": "index.html",
"browserslist": "> 0.5%, last 2 versions, not dead",
"scripts": {
"start": "parcel",
"build": "parcel build",
"check": "tsc --noEmit"
},
"author": "Mitchell Hashimoto",
"license": "MIT",
"devDependencies": {
"@parcel/transformer-inline-string": "^2.8.0",
"parcel": "^2.8.0",
"typescript": "^4.9.3"
},
"dependencies": {
"zig-js": "file:../vendor/zig-js/js"
}
}

View File

@ -21,6 +21,8 @@ const Allocator = std.mem.Allocator;
const testing = std.testing; const testing = std.testing;
const fastmem = @import("../fastmem.zig"); const fastmem = @import("../fastmem.zig");
const log = std.log.scoped(.atlas);
/// Data is the raw texture data. /// Data is the raw texture data.
data: []u8, data: []u8,
@ -307,8 +309,9 @@ pub fn clear(self: *Atlas) void {
pub const Wasm = struct { pub const Wasm = struct {
// If you're copying this file (Atlas.zig) out to a separate project, // If you're copying this file (Atlas.zig) out to a separate project,
// just replace this with the allocator you want to use. // just replace this with the allocator you want to use.
const wasm = @import("../wasm.zig"); const wasm = @import("../os/wasm.zig");
const alloc = wasm.alloc; const alloc = wasm.alloc;
const js = @import("zig-js");
export fn atlas_new(size: u32, format: u8) ?*Atlas { export fn atlas_new(size: u32, format: u8) ?*Atlas {
const atlas = init( const atlas = init(
@ -321,6 +324,13 @@ pub const Wasm = struct {
return result; return result;
} }
export fn atlas_free(ptr: ?*Atlas) void {
if (ptr) |v| {
v.deinit(alloc);
alloc.destroy(v);
}
}
/// The return value for this should be freed by the caller with "free". /// The return value for this should be freed by the caller with "free".
export fn atlas_reserve(self: *Atlas, width: u32, height: u32) ?*Region { export fn atlas_reserve(self: *Atlas, width: u32, height: u32) ?*Region {
return atlas_reserve_(self, width, height) catch return null; return atlas_reserve_(self, width, height) catch return null;
@ -348,11 +358,91 @@ pub const Wasm = struct {
self.clear(); self.clear();
} }
export fn atlas_free(ptr: ?*Atlas) void { /// This creates a Canvas element identified by the id returned that
if (ptr) |v| { /// the caller can draw into the DOM to visualize the atlas. The returned
v.deinit(alloc); /// ID must be freed from the JS runtime by calling "zigjs.deleteValue".
alloc.destroy(v); export fn atlas_debug_canvas(self: *Atlas) u32 {
return atlas_debug_canvas_(self) catch |err| {
log.warn("error dumping atlas canvas err={}", .{err});
return 0;
};
} }
fn atlas_debug_canvas_(self: *Atlas) !u32 {
// Create our canvas
const doc = try js.global.get(js.Object, "document");
defer doc.deinit();
const canvas = try doc.call(js.Object, "createElement", .{js.string("canvas")});
errdefer canvas.deinit();
// Setup our canvas size
{
try canvas.set("width", self.size);
try canvas.set("height", self.size);
const width_str = try std.fmt.allocPrint(alloc, "{d}px", .{self.size});
defer alloc.free(width_str);
const style = try canvas.get(js.Object, "style");
defer style.deinit();
try style.set("width", js.string(width_str));
try style.set("height", js.string(width_str));
}
// This will return the same context on subsequent calls so it
// is important to reset it.
const ctx = try canvas.call(js.Object, "getContext", .{js.string("2d")});
defer ctx.deinit();
// We need to draw pixels so this is format dependent.
var buf: []u8 = switch (self.format) {
// RGBA is the native ImageData format
.rgba => self.data,
.greyscale => buf: {
// Convert from A8 to RGBA so every 4th byte is set to a value.
var buf: []u8 = try alloc.alloc(u8, self.data.len * 4);
errdefer alloc.free(buf);
std.mem.set(u8, buf, 0);
for (self.data) |value, i| {
buf[(i * 4) + 3] = value;
}
break :buf buf;
},
else => return error.UnsupportedAtlasFormat,
};
defer if (buf.ptr != self.data.ptr) alloc.free(buf);
// Create an ImageData from our buffer and then write it to the canvas
const image_data: js.Object = data: {
// Get our runtime memory
const mem = try js.runtime.get(js.Object, "memory");
defer mem.deinit();
const mem_buf = try mem.get(js.Object, "buffer");
defer mem_buf.deinit();
// Create an array that points to our buffer
const Uint8ClampedArray = try js.global.get(js.Object, "Uint8ClampedArray");
defer Uint8ClampedArray.deinit();
const arr = try Uint8ClampedArray.new(.{ mem_buf, buf.ptr, buf.len });
defer arr.deinit();
// Create the image data from our array
const ImageData = try js.global.get(js.Object, "ImageData");
defer ImageData.deinit();
const data = try ImageData.new(.{ arr, self.size, self.size });
errdefer data.deinit();
break :data data;
};
defer image_data.deinit();
// Draw it
try ctx.call(void, "putImageData", .{ image_data, 0, 0 });
const id = @bitCast(js.Ref, @enumToInt(canvas.value)).id;
return id;
} }
test "happy path" { test "happy path" {

View File

@ -24,8 +24,8 @@ fc: if (options.backend == .fontconfig_freetype) ?Fontconfig else void =
if (options.backend == .fontconfig_freetype) null else {}, if (options.backend == .fontconfig_freetype) null else {},
/// CoreText /// CoreText
ct: if (options.backend == .coretext) ?CoreText else void = ct: if (font.Discover == font.discovery.CoreText) ?CoreText else void =
if (options.backend == .coretext) null else {}, if (font.Discover == font.discovery.CoreText) 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 {
@ -66,7 +66,7 @@ pub fn deinit(self: *DeferredFace) void {
if (self.face) |*face| face.deinit(); if (self.face) |*face| face.deinit();
switch (options.backend) { switch (options.backend) {
.fontconfig_freetype => if (self.fc) |*fc| fc.deinit(), .fontconfig_freetype => if (self.fc) |*fc| fc.deinit(),
.coretext => if (self.ct) |*ct| ct.deinit(), .coretext, .coretext_freetype => if (self.ct) |*ct| ct.deinit(),
.freetype => {}, .freetype => {},
// TODO // TODO
.web_canvas => unreachable, .web_canvas => unreachable,
@ -86,7 +86,7 @@ pub fn name(self: DeferredFace) ![:0]const u8 {
.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,
.coretext => if (self.ct) |ct| { .coretext, .coretext_freetype => if (self.ct) |ct| {
const display_name = ct.font.copyDisplayName(); const display_name = ct.font.copyDisplayName();
return display_name.cstringPtr(.utf8) orelse "<unsupported internal encoding>"; return display_name.cstringPtr(.utf8) orelse "<unsupported internal encoding>";
}, },
@ -116,14 +116,12 @@ pub fn load(
}, },
.coretext => { .coretext => {
// It is possible to use CoreText with Freetype so we support try self.loadCoreText(lib, size);
// both here. return;
switch (font.Face) { },
@import("face/freetype.zig").Face => try self.loadCoreTextFreetype(lib, size),
@import("face/coretext.zig").Face => try self.loadCoreText(lib, size),
else => unreachable,
}
.coretext_freetype => {
try self.loadCoreTextFreetype(lib, size);
return; return;
}, },
@ -239,7 +237,7 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool {
} }
}, },
.coretext => { .coretext, .coretext_freetype => {
// If we are using coretext, we check the loaded CT font. // If we are using coretext, we check the loaded CT font.
if (self.ct) |ct| { if (self.ct) |ct| {
// Turn UTF-32 into UTF-16 for CT API // Turn UTF-32 into UTF-16 for CT API

View File

@ -285,6 +285,26 @@ pub fn renderGlyph(
return try face.face.?.renderGlyph(alloc, atlas, glyph_index, max_height); return try face.face.?.renderGlyph(alloc, atlas, glyph_index, max_height);
} }
/// The wasm-compatible API.
pub const Wasm = struct {
const wasm = @import("../os/wasm.zig");
const alloc = wasm.alloc;
export fn group_new(pts: u16) ?*Group {
return group_new_(pts) catch null;
}
fn group_new_(pts: u16) !*Group {
var group = try Group.init(alloc, .{}, .{ .points = pts });
errdefer group.deinit();
var result = try alloc.create(Group);
errdefer alloc.destroy(result);
result.* = group;
return result;
}
};
test { test {
const testing = std.testing; const testing = std.testing;
const alloc = testing.allocator; const alloc = testing.allocator;

View File

@ -1,19 +0,0 @@
//! A library represents the shared state that the underlying font
//! library implementation(s) require per-process.
//!
//! In the future, this will be abstracted so that the underlying text
//! engine might not be Freetype and may be something like Core Text,
//! but the API will remain the same.
const Library = @This();
const freetype = @import("freetype");
lib: freetype.Library,
pub fn init() freetype.Error!Library {
return Library{ .lib = try freetype.Library.init() };
}
pub fn deinit(self: *Library) void {
self.lib.deinit();
}

View File

@ -2,15 +2,17 @@ const builtin = @import("builtin");
const options = @import("main.zig").options; const options = @import("main.zig").options;
const freetype = @import("face/freetype.zig"); const freetype = @import("face/freetype.zig");
const coretext = @import("face/coretext.zig"); const coretext = @import("face/coretext.zig");
const web_canvas = @import("face/web_canvas.zig"); pub const web_canvas = @import("face/web_canvas.zig");
/// Face implementation for the compile options. /// Face implementation for the compile options.
pub const Face = switch (options.backend) { pub const Face = switch (options.backend) {
.fontconfig_freetype => freetype.Face, .freetype,
.coretext => freetype.Face, .fontconfig_freetype,
//.coretext => coretext.Face, .coretext_freetype,
=> freetype.Face,
.coretext => coretext.Face,
.web_canvas => web_canvas.Face, .web_canvas => web_canvas.Face,
else => unreachable,
}; };
/// If a DPI can't be calculated, this DPI is used. This is probably /// If a DPI can't be calculated, this DPI is used. This is probably

View File

@ -113,7 +113,7 @@ pub const Face = struct {
/// Returns true if this font is colored. This can be used by callers to /// Returns true if this font is colored. This can be used by callers to
/// determine what kind of atlas to pass in. /// determine what kind of atlas to pass in.
pub fn hasColor(self: Face) bool { fn hasColor(self: Face) bool {
return self.face.hasColor(); return self.face.hasColor();
} }

View File

@ -1,11 +1,390 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const assert = std.debug.assert; const assert = std.debug.assert;
const testing = std.testing;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const js = @import("zig-js");
const font = @import("../main.zig"); const font = @import("../main.zig");
const log = std.log.scoped(.font_face); const log = std.log.scoped(.font_face);
pub const Face = struct { pub const Face = struct {
// TODO /// The web canvas face makes use of an allocator when interacting
/// with the JS environment.
alloc: Allocator,
/// The CSS "font" attribute, excluding size.
font_str: []const u8,
/// The size we currently have set.
size: font.face.DesiredSize,
/// The presentation for this font. This is a heuristic since fonts don't have
/// a way to declare this. We just assume a font with color is an emoji font.
presentation: font.Presentation,
/// Metrics for this font face. These are useful for renderers.
metrics: font.face.Metrics,
/// The canvas element that we will reuse to render glyphs
canvas: js.Object,
/// Initialize a web canvas font with a "raw" value. The "raw" value can
/// be any valid value for a CSS "font" property EXCLUDING the size. The
/// size is always added via the `size` parameter.
///
/// The raw value is copied so the caller can free it after it is gone.
pub fn initNamed(
alloc: Allocator,
raw: []const u8,
size: font.face.DesiredSize,
) !Face {
// Copy our font string because we're going to have to reuse it.
const font_str = try alloc.dupe(u8, raw);
errdefer alloc.free(font_str);
// Create our canvas that we're going to continue to reuse.
const doc = try js.global.get(js.Object, "document");
defer doc.deinit();
const canvas = try doc.call(js.Object, "createElement", .{js.string("canvas")});
errdefer canvas.deinit();
var result = Face{
.alloc = alloc,
.font_str = font_str,
.size = size,
// TODO: figure out how we're going to do emoji with web canvas
.presentation = .text,
.canvas = canvas,
// We're going to calculate these right after initialization.
.metrics = undefined,
};
try result.calcMetrics();
log.debug("face initialized: {s}", .{raw});
return result;
}
pub fn deinit(self: *Face) void {
self.alloc.free(self.font_str);
self.canvas.deinit();
self.* = undefined;
}
/// Resize the font in-place. If this succeeds, the caller is responsible
/// for clearing any glyph caches, font atlas data, etc.
pub fn setSize(self: *Face, size: font.face.DesiredSize) !void {
const old = self.size;
self.size = size;
errdefer self.size = old;
try self.calcMetrics();
}
/// Returns the glyph index for the given Unicode code point. For canvas,
/// we support every glyph and the ID is just the codepoint since we don't
/// have access to the underlying tables anyways. We let the browser deal
/// with bad codepoints.
pub fn glyphIndex(self: Face, cp: u32) ?u32 {
_ = self;
return cp;
}
/// Render a glyph using the glyph index. The rendered glyph is stored in the
/// given texture atlas.
pub fn renderGlyph(
self: Face,
alloc: Allocator,
atlas: *font.Atlas,
glyph_index: u32,
max_height: ?u16,
) !font.Glyph {
_ = max_height;
// Encode our glyph into UTF-8 so we can build a JS string out of it.
var utf8: [4]u8 = undefined;
const utf8_len = try std.unicode.utf8Encode(@intCast(u21, glyph_index), &utf8);
const glyph_str = js.string(utf8[0..utf8_len]);
// Get our drawing context
const measure_ctx = try self.context();
defer measure_ctx.deinit();
// Get the width and height of the render
const metrics = try measure_ctx.call(js.Object, "measureText", .{glyph_str});
defer metrics.deinit();
const width: u32 = @floatToInt(u32, @ceil(width: {
// We prefer the bounding box since it is tighter but certain
// text such as emoji do not have a bounding box set so we use
// the full run width instead.
const bounding_right = try metrics.get(f32, "actualBoundingBoxRight");
if (bounding_right > 0) break :width bounding_right;
break :width try metrics.get(f32, "width");
})) + 1;
// Height is our ascender + descender for this char
const asc = try metrics.get(f32, "actualBoundingBoxAscent");
const desc = try metrics.get(f32, "actualBoundingBoxDescent");
const left = try metrics.get(f32, "actualBoundingBoxLeft");
const height = @floatToInt(u32, @ceil(asc + desc)) + 1;
// Note: width and height both get "+ 1" added to them above. This
// is important so that there is a 1px border around the glyph to avoid
// any clipping in the atlas.
// Resize canvas to match the glyph size exactly
{
try self.canvas.set("width", width);
try self.canvas.set("height", height);
const width_str = try std.fmt.allocPrint(alloc, "{d}px", .{width});
defer alloc.free(width_str);
const height_str = try std.fmt.allocPrint(alloc, "{d}px", .{height});
defer alloc.free(height_str);
const style = try self.canvas.get(js.Object, "style");
defer style.deinit();
try style.set("width", js.string(width_str));
try style.set("height", js.string(height_str));
}
// Reload our context since we resized the canvas
const ctx = try self.context();
defer ctx.deinit();
// Draw background
try ctx.set("fillStyle", js.string("transparent"));
try ctx.call(void, "fillRect", .{
@as(u32, 0),
@as(u32, 0),
width,
height,
});
// Draw glyph
try ctx.set("fillStyle", js.string("black"));
try ctx.call(void, "fillText", .{
glyph_str,
left + 1,
asc + 1,
});
// Read the image data and get it into a []u8 on our side
const bitmap: []u8 = bitmap: {
// Read the raw bitmap data and get the "data" value which is a
// Uint8ClampedArray.
const data = try ctx.call(js.Object, "getImageData", .{ 0, 0, width, height });
defer data.deinit();
const src_array = try data.get(js.Object, "data");
defer src_array.deinit();
// Allocate our local memory to copy the data to.
const len = try src_array.get(u32, "length");
var bitmap = try alloc.alloc(u8, @intCast(usize, len));
errdefer alloc.free(bitmap);
// Create our target Uint8Array that we can use to copy from src.
const mem_array = mem_array: {
// Get our runtime memory
const mem = try js.runtime.get(js.Object, "memory");
defer mem.deinit();
const buf = try mem.get(js.Object, "buffer");
defer buf.deinit();
// Construct our array to peer into our memory
const Uint8Array = try js.global.get(js.Object, "Uint8Array");
defer Uint8Array.deinit();
const mem_array = try Uint8Array.new(.{ buf, bitmap.ptr });
errdefer mem_array.deinit();
break :mem_array mem_array;
};
defer mem_array.deinit();
// Copy
try mem_array.call(void, "set", .{src_array});
break :bitmap bitmap;
};
defer alloc.free(bitmap);
// The bitmap is in RGBA format and we just want alpha8.
assert(@mod(bitmap.len, 4) == 0);
var bitmap_a8 = try alloc.alloc(u8, bitmap.len / 4);
defer alloc.free(bitmap_a8);
var i: usize = 0;
while (i < bitmap_a8.len) : (i += 1) {
bitmap_a8[i] = bitmap[(i * 4) + 3];
}
// Put it in our atlas
const region = try atlas.reserve(alloc, width, height);
if (region.width > 0 and region.height > 0) atlas.set(region, bitmap_a8);
return font.Glyph{
.width = width,
.height = height,
// TODO: this can't be right
.offset_x = 0,
.offset_y = 0,
.atlas_x = region.x,
.atlas_y = region.y,
.advance_x = 0,
};
}
/// Calculate the metrics associated with a given face.
fn calcMetrics(self: *Face) !void {
const ctx = try self.context();
defer ctx.deinit();
// Cell width is the width of our M text
const cell_width: f32 = cell_width: {
const metrics = try ctx.call(js.Object, "measureText", .{js.string("M")});
defer metrics.deinit();
// We prefer the bounding box since it is tighter but certain
// text such as emoji do not have a bounding box set so we use
// the full run width instead.
const bounding_right = try metrics.get(f32, "actualBoundingBoxRight");
if (bounding_right > 0) break :cell_width bounding_right;
break :cell_width try metrics.get(f32, "width");
};
// To get the cell height we render a high and low character and get
// the total of the ascent and descent. This should equal our
// pixel height but this is a more surefire way to get it.
const height_metrics = try ctx.call(js.Object, "measureText", .{js.string("M_")});
defer height_metrics.deinit();
const asc = try height_metrics.get(f32, "actualBoundingBoxAscent");
const desc = try height_metrics.get(f32, "actualBoundingBoxDescent");
const cell_height = asc + desc;
const cell_baseline = desc;
// There isn't a declared underline position for canvas measurements
// so we just go 1 under the cell height to match freetype logic
// at this time (our freetype logic).
const underline_position = cell_height - 1;
const underline_thickness: f32 = 1;
self.metrics = .{
.cell_width = cell_width,
.cell_height = cell_height,
.cell_baseline = cell_baseline,
.underline_position = underline_position,
.underline_thickness = underline_thickness,
.strikethrough_position = underline_position,
.strikethrough_thickness = underline_thickness,
};
log.debug("metrics font={s} value={}", .{ self.font_str, self.metrics });
}
/// Returns the 2d context configured for drawing
fn context(self: Face) !js.Object {
// This will return the same context on subsequent calls so it
// is important to reset it.
const ctx = try self.canvas.call(js.Object, "getContext", .{js.string("2d")});
errdefer ctx.deinit();
// Clear the canvas
{
const width = try self.canvas.get(f64, "width");
const height = try self.canvas.get(f64, "height");
try ctx.call(void, "clearRect", .{ 0, 0, width, height });
}
// Set our context font
var font_val = try std.fmt.allocPrint(
self.alloc,
"{d}px {s}",
.{ self.size.points, self.font_str },
);
defer self.alloc.free(font_val);
try ctx.set("font", js.string(font_val));
// If the font property didn't change, then the font set didn't work.
// We do this check because it is very easy to put an invalid font
// in and this at least makes it show up in the logs.
const check = try ctx.getAlloc(js.String, self.alloc, "font");
defer self.alloc.free(check);
if (!std.mem.eql(u8, font_val, check)) {
log.warn("canvas font didn't set, fonts may be broken, expected={s} got={s}", .{
font_val,
check,
});
}
return ctx;
}
};
/// The wasm-compatible API.
pub const Wasm = struct {
const wasm = @import("../../os/wasm.zig");
const alloc = wasm.alloc;
export fn face_new(ptr: [*]const u8, len: usize, pts: u16) ?*Face {
return face_new_(ptr, len, pts) catch null;
}
fn face_new_(ptr: [*]const u8, len: usize, pts: u16) !*Face {
var face = try Face.initNamed(alloc, ptr[0..len], .{ .points = pts });
errdefer face.deinit();
var result = try alloc.create(Face);
errdefer alloc.destroy(result);
result.* = face;
return result;
}
export fn face_free(ptr: ?*Face) void {
if (ptr) |v| {
v.deinit();
alloc.destroy(v);
}
}
/// Resulting pointer must be freed using the global "free".
export fn face_render_glyph(
face: *Face,
atlas: *font.Atlas,
codepoint: u32,
) ?*font.Glyph {
return face_render_glyph_(face, atlas, codepoint) catch |err| {
log.warn("error rendering glyph err={}", .{err});
return null;
};
}
export fn face_debug_canvas(face: *Face) void {
face_debug_canvas_(face) catch |err| {
log.warn("error adding debug canvas err={}", .{err});
};
}
fn face_debug_canvas_(face: *Face) !void {
const doc = try js.global.get(js.Object, "document");
defer doc.deinit();
const elem = try doc.call(
?js.Object,
"getElementById",
.{js.string("face-canvas")},
) orelse return error.CanvasContainerNotFound;
defer elem.deinit();
try elem.call(void, "append", .{face.canvas});
}
fn face_render_glyph_(face: *Face, atlas: *font.Atlas, codepoint: u32) !*font.Glyph {
const glyph = try face.renderGlyph(alloc, atlas, codepoint, null);
const result = try alloc.create(font.Glyph);
errdefer alloc.destroy(result);
_ = try wasm.toHostOwned(result);
result.* = glyph;
return result;
}
}; };

42
src/font/library.zig Normal file
View File

@ -0,0 +1,42 @@
//! A library represents the shared state that the underlying font
//! library implementation(s) require per-process.
const builtin = @import("builtin");
const options = @import("main.zig").options;
const freetype = @import("freetype");
const font = @import("main.zig");
/// Library implementation for the compile options.
pub const Library = switch (options.backend) {
// Freetype requires a state library
.freetype,
.fontconfig_freetype,
.coretext_freetype,
=> FreetypeLibrary,
// Some backends such as CT and Canvas don't have a "library"
.coretext,
.web_canvas,
=> NoopLibrary,
};
pub const FreetypeLibrary = struct {
lib: freetype.Library,
pub fn init() freetype.Error!Library {
return Library{ .lib = try freetype.Library.init() };
}
pub fn deinit(self: *Library) void {
self.lib.deinit();
}
};
pub const NoopLibrary = struct {
pub fn init() !Library {
return Library{};
}
pub fn deinit(self: *Library) void {
_ = self;
}
};

View File

@ -10,15 +10,18 @@ pub const Face = face.Face;
pub const Group = @import("Group.zig"); pub const Group = @import("Group.zig");
pub const GroupCache = @import("GroupCache.zig"); pub const GroupCache = @import("GroupCache.zig");
pub const Glyph = @import("Glyph.zig"); pub const Glyph = @import("Glyph.zig");
pub const Library = @import("Library.zig");
pub const Shaper = @import("Shaper.zig"); pub const Shaper = @import("Shaper.zig");
pub const sprite = @import("sprite.zig"); pub const sprite = @import("sprite.zig");
pub const Sprite = sprite.Sprite; pub const Sprite = sprite.Sprite;
pub const Descriptor = discovery.Descriptor; pub const Descriptor = discovery.Descriptor;
pub const Discover = discovery.Discover; pub const Discover = discovery.Discover;
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 if (builtin.target.isWasm()) struct {
pub usingnamespace Atlas.Wasm; pub usingnamespace Atlas.Wasm;
pub usingnamespace Group.Wasm;
pub usingnamespace face.web_canvas.Wasm;
} else struct {}; } else struct {};
/// Build options /// Build options
@ -35,9 +38,12 @@ pub const Backend = enum {
/// Fontconfig for font discovery and FreeType for font rendering. /// Fontconfig for font discovery and FreeType for font rendering.
fontconfig_freetype, fontconfig_freetype,
/// CoreText for both font discovery and rendering (macOS). /// CoreText for both font discovery for rendering (macOS).
coretext, coretext,
/// CoreText for font discovery and FreeType for rendering (macOS).
coretext_freetype,
/// Use the browser font system and the Canvas API (wasm). This limits /// Use the browser font system and the Canvas API (wasm). This limits
/// the available fonts to browser fonts (anything Canvas natively /// the available fonts to browser fonts (anything Canvas natively
/// supports). /// supports).

View File

@ -1,5 +1,17 @@
// This is the main file for the WASM module. The WASM module has to // This is the main file for the WASM module. The WASM module has to
// export a C ABI compatible API. // export a C ABI compatible API.
const std = @import("std");
const builtin = @import("builtin");
pub usingnamespace @import("wasm.zig"); pub usingnamespace @import("os/wasm.zig");
pub usingnamespace @import("os/wasm/log.zig");
pub usingnamespace @import("font/main.zig"); pub usingnamespace @import("font/main.zig");
// Set our log level. We try to get as much logging as possible but in
// ReleaseSmall mode where we're optimizing for space, we elevate the
// log level.
pub const log_level: std.log.Level = switch (builtin.mode) {
.Debug => .debug,
.ReleaseSmall => .warn,
else => .info,
};

View File

@ -37,6 +37,18 @@ pub export fn malloc(len: usize) ?[*]u8 {
return alloc_(len) catch return null; return alloc_(len) catch return null;
} }
fn alloc_(len: usize) ![*]u8 {
// Create the allocation
const slice = try alloc.alloc(u8, len);
errdefer alloc.free(slice);
// Store the size so we can deallocate later
try allocs.putNoClobber(alloc, slice.ptr, slice.len);
errdefer _ = allocs.remove(slice.ptr);
return slice.ptr;
}
/// Free an allocation from malloc. /// Free an allocation from malloc.
pub export fn free(ptr: ?[*]u8) void { pub export fn free(ptr: ?[*]u8) void {
if (ptr) |v| { if (ptr) |v| {
@ -79,18 +91,6 @@ pub fn toModuleOwned(ptr: anytype) void {
_ = allocs.remove(casted); _ = allocs.remove(casted);
} }
fn alloc_(len: usize) ![*]u8 {
// Create the allocation
const slice = try alloc.alloc(u8, len);
errdefer alloc.free(slice);
// Store the size so we can deallocate later
try allocs.putNoClobber(alloc, slice.ptr, slice.len);
errdefer _ = allocs.remove(slice.ptr);
return slice.ptr;
}
test "basics" { test "basics" {
const testing = std.testing; const testing = std.testing;
var buf = malloc(32).?; var buf = malloc(32).?;

27
src/os/wasm/log.zig Normal file
View File

@ -0,0 +1,27 @@
const std = @import("std");
const builtin = @import("builtin");
const wasm = @import("../wasm.zig");
// The function std.log will call.
pub fn log(
comptime level: std.log.Level,
comptime scope: @TypeOf(.EnumLiteral),
comptime format: []const u8,
args: anytype,
) void {
// Build the string
const level_txt = comptime level.asText();
const prefix = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";
const txt = level_txt ++ prefix ++ format;
const str = nosuspend std.fmt.allocPrint(wasm.alloc, txt, args) catch return;
defer wasm.alloc.free(str);
// Send it over to the JS side
JS.log(str.ptr, str.len);
}
// We wrap our externs in this namespace so we can reuse symbols, otherwise
// "log" would collide.
const JS = struct {
extern "env" fn log(ptr: [*]const u8, len: usize) void;
};

1
vendor/zig-js vendored Submodule

@ -0,0 +1 @@
Subproject commit 52eed4daddcf9fa974ad4457691de26ef2351c56