diff --git a/example/app.ts b/example/app.ts index ab6ab65d0..a8d665ffc 100644 --- a/example/app.ts +++ b/example/app.ts @@ -63,17 +63,18 @@ fetch(url.href).then(response => new Uint8Array(memory.buffer, font_ptr).set(font); // Initialize our deferred face - const df = deferred_face_new(font_ptr, font.byteLength); + // const df = deferred_face_new(font_ptr, font.byteLength, 0 /* text */); //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); + //free(font_ptr); // Create our group const group = group_new(72 /* size */); - group_add_face(group, 0, df); + group_add_face(group, 0 /* regular */, deferred_face_new(font_ptr, font.byteLength, 0 /* text */)); + group_add_face(group, 0 /* regular */, deferred_face_new(font_ptr, font.byteLength, 1 /* emoji */)); // Create our group cache const group_cache = group_cache_new(group); @@ -84,6 +85,14 @@ fetch(url.href).then(response => group_cache_render_glyph(group_cache, font_idx, i, 0); //face_render_glyph(face, atlas, i); } + + const emoji = ["🐏","🌞","🌚","🍱","💿","🐈","📃","📀","🕡","🙃"]; + for (let i = 0; i < emoji.length; i++) { + const cp = emoji[i].codePointAt(0); + const font_idx = group_cache_index_for_codepoint(group_cache, cp, 0, 1 /* emoji */); + group_cache_render_glyph(group_cache, font_idx, cp, 0); + } + //face_render_glyph(face, atlas, "橋".codePointAt(0)); //face_render_glyph(face, atlas, "p".codePointAt(0)); diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index 23d8598ae..38edd28ba 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -71,6 +71,9 @@ pub const WebCanvas = struct { /// The string to use for the "font" attribute for the canvas font_str: [:0]const u8, + /// The presentation for this font. + presentation: Presentation, + pub fn deinit(self: *WebCanvas) void { self.alloc.free(self.font_str); self.* = undefined; @@ -227,7 +230,7 @@ fn loadWebCanvas( ) !void { assert(self.face == null); const wc = self.wc.?; - self.face = try Face.initNamed(wc.alloc, wc.font_str, size); + self.face = try Face.initNamed(wc.alloc, wc.font_str, size, wc.presentation); } /// Returns true if this face can satisfy the given codepoint and @@ -283,7 +286,13 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool { // Canvas always has the codepoint because we have no way of // really checking and we let the browser handle it. - .web_canvas => return true, + .web_canvas => { + if (self.wc) |wc| { + if (p) |desired| if (wc.presentation != desired) return false; + } + + return true; + }, .freetype => {}, } @@ -298,14 +307,14 @@ 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| { + export fn deferred_face_new(ptr: [*]const u8, len: usize, presentation: u16) ?*DeferredFace { + return deferred_face_new_(ptr, len, presentation) catch |err| { log.warn("error creating deferred face err={}", .{err}); return null; }; } - fn deferred_face_new_(ptr: [*]const u8, len: usize) !*DeferredFace { + fn deferred_face_new_(ptr: [*]const u8, len: usize, presentation: u16) !*DeferredFace { var font_str = try alloc.dupeZ(u8, ptr[0..len]); errdefer alloc.free(font_str); @@ -313,6 +322,7 @@ pub const Wasm = struct { .wc = .{ .alloc = alloc, .font_str = font_str, + .presentation = @intToEnum(font.Presentation, presentation), }, }; errdefer face.deinit(); diff --git a/src/font/face/web_canvas.zig b/src/font/face/web_canvas.zig index 6f5e86a2d..c27a68aa4 100644 --- a/src/font/face/web_canvas.zig +++ b/src/font/face/web_canvas.zig @@ -19,8 +19,7 @@ pub const Face = struct { /// 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. + /// The presentation for this font. presentation: font.Presentation, /// Metrics for this font face. These are useful for renderers. @@ -34,10 +33,15 @@ pub const Face = struct { /// size is always added via the `size` parameter. /// /// The raw value is copied so the caller can free it after it is gone. + /// + /// The presentation is given here directly because the browser gives + /// us no easy way to determine the presentation we want for this font. + /// Callers should just tell us what to expect. pub fn initNamed( alloc: Allocator, raw: []const u8, size: font.face.DesiredSize, + presentation: font.Presentation, ) !Face { // Copy our font string because we're going to have to reuse it. const font_str = try alloc.dupe(u8, raw); @@ -53,8 +57,7 @@ pub const Face = struct { .alloc = alloc, .font_str = font_str, .size = size, - // TODO: figure out how we're going to do emoji with web canvas - .presentation = .text, + .presentation = presentation, .canvas = canvas, @@ -123,11 +126,17 @@ pub const Face = struct { break :width try metrics.get(f32, "width"); })) + 1; - // Height is our ascender + descender for this char + const left = try metrics.get(f32, "actualBoundingBoxLeft"); 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; + + // On Firefox on Linux, the bounding box is broken in some cases for + // ideographic glyphs (such as emoji). We detect this and behave + // differently. + const broken_bbox = asc + desc < 0.001; + + // Height is our ascender + descender for this char + const height = if (!broken_bbox) @floatToInt(u32, @ceil(asc + desc)) + 1 else width; // 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 @@ -153,6 +162,12 @@ pub const Face = struct { const ctx = try self.context(); defer ctx.deinit(); + // For the broken bounding box case we render ideographic baselines + // so that we just render the glyph fully in the box with no offsets. + if (broken_bbox) { + try ctx.set("textBaseline", js.string("ideographic")); + } + // Draw background try ctx.set("fillStyle", js.string("transparent")); try ctx.call(void, "fillRect", .{ @@ -167,7 +182,7 @@ pub const Face = struct { try ctx.call(void, "fillText", .{ glyph_str, left + 1, - asc + 1, + if (!broken_bbox) asc + 1 else @intToFloat(f32, height), }); // Read the image data and get it into a []u8 on our side @@ -209,18 +224,31 @@ pub const Face = struct { }; 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]; - } + // Convert the format of the bitmap if necessary + const bitmap_formatted: []u8 = switch (atlas.format) { + // Bitmap is already in RGBA + .rgba => bitmap, + + // Convert down to A8 + .greyscale => a8: { + assert(@mod(bitmap.len, 4) == 0); + var bitmap_a8 = try alloc.alloc(u8, bitmap.len / 4); + errdefer alloc.free(bitmap_a8); + var i: usize = 0; + while (i < bitmap_a8.len) : (i += 1) { + bitmap_a8[i] = bitmap[(i * 4) + 3]; + } + + break :a8 bitmap_a8; + }, + + else => return error.UnsupportedAtlasFormat, + }; + defer if (bitmap_formatted.ptr != bitmap.ptr) alloc.free(bitmap_formatted); // 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); + if (region.width > 0 and region.height > 0) atlas.set(region, bitmap_formatted); return font.Glyph{ .width = width, @@ -325,12 +353,17 @@ 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; + export fn face_new(ptr: [*]const u8, len: usize, pts: u16, p: u16) ?*Face { + return face_new_(ptr, len, pts, p) catch null; } - fn face_new_(ptr: [*]const u8, len: usize, pts: u16) !*Face { - var face = try Face.initNamed(alloc, ptr[0..len], .{ .points = pts }); + fn face_new_(ptr: [*]const u8, len: usize, pts: u16, presentation: u16) !*Face { + var face = try Face.initNamed( + alloc, + ptr[0..len], + .{ .points = pts }, + @intToEnum(font.Presentation, presentation), + ); errdefer face.deinit(); var result = try alloc.create(Face);