diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/font/face | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (diff) | |
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/font/face')
| -rw-r--r-- | vendor/libghostty-vt/src/font/face/coretext.zig | 1162 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/font/face/freetype.zig | 1439 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/font/face/web_canvas.zig | 561 |
3 files changed, 3162 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/font/face/coretext.zig b/vendor/libghostty-vt/src/font/face/coretext.zig new file mode 100644 index 00000000..1d133388 --- /dev/null +++ b/vendor/libghostty-vt/src/font/face/coretext.zig @@ -0,0 +1,1162 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const assert = @import("../../quirks.zig").inlineAssert; +const Allocator = std.mem.Allocator; +const macos = @import("macos"); +const harfbuzz = @import("harfbuzz"); +const font = @import("../main.zig"); +const opentype = @import("../opentype.zig"); +const quirks = @import("../../quirks.zig"); + +const log = std.log.scoped(.font_face); + +pub const Face = struct { + /// Our font face + font: *macos.text.Font, + + /// Harfbuzz font corresponding to this face. We only use this + /// if we're using Harfbuzz. + hb_font: if (harfbuzz_shaper) harfbuzz.Font else void, + + /// Set quirks.disableDefaultFontFeatures + quirks_disable_default_font_features: bool = false, + + /// True if this font face should be rasterized with a synthetic bold + /// effect. This is used for fonts that don't have a bold variant. + synthetic_bold: ?f64 = null, + + /// If the face can possibly be colored, then this is the state + /// used to check for color information. This is null if the font + /// can't possibly be colored (i.e. doesn't have SVG, sbix, etc + /// tables). + color: ?ColorState = null, + + /// The current size this font is set to. + size: font.face.DesiredSize, + + /// True if our build is using Harfbuzz. If we're not, we can avoid + /// some Harfbuzz-specific code paths. + const harfbuzz_shaper = font.options.backend.hasHarfbuzz(); + + /// The matrix applied to a regular font to auto-italicize it. + pub const italic_skew = macos.graphics.AffineTransform{ + .a = 1, + .b = 0, + .c = 0.267949, // approx. tan(15) + .d = 1, + .tx = 0, + .ty = 0, + }; + + /// Initialize a CoreText-based font from a TTF/TTC in memory. + pub fn init( + lib: font.Library, + source: [:0]const u8, + opts: font.face.Options, + ) !Face { + _ = lib; + + const data = try macos.foundation.Data.createWithBytesNoCopy(source); + defer data.release(); + + const desc = macos.text.createFontDescriptorFromData(data) orelse + return error.FontInitFailure; + defer desc.release(); + + const ct_font = try macos.text.Font.createWithFontDescriptor(desc, 12); + defer ct_font.release(); + + return try initFontCopy(ct_font, opts); + } + + /// Initialize a CoreText-based face from another initialized font face + /// but with a new size. This is often how CoreText fonts are initialized + /// because the font is loaded at a default size during discovery, and then + /// adjusted to the final size for final load. + pub fn initFontCopy(base: *macos.text.Font, opts: font.face.Options) !Face { + // Create a copy. The copyWithAttributes docs say the size is in points, + // but we need to scale the points by the DPI and to do that we use our + // function called "pixels". + const ct_font = try base.copyWithAttributes( + opts.size.pixels(), + null, + null, + ); + errdefer ct_font.release(); + + return try initFont(ct_font, opts); + } + + /// Initialize a face with a CTFont. This will take ownership over + /// the CTFont. This does NOT copy or retain the CTFont. + pub fn initFont(ct_font: *macos.text.Font, opts: font.face.Options) !Face { + const traits = ct_font.getSymbolicTraits(); + + var hb_font = if (comptime harfbuzz_shaper) font: { + var hb_font = try harfbuzz.coretext.createFont(ct_font); + const pixels: opentype.sfnt.F26Dot6 = .from(opts.size.pixels()); + hb_font.setScale(@bitCast(pixels), @bitCast(pixels)); + break :font hb_font; + } else {}; + errdefer if (comptime harfbuzz_shaper) hb_font.destroy(); + + const color: ?ColorState = if (traits.color_glyphs) + try .init(ct_font) + else + null; + errdefer if (color) |v| v.deinit(); + + var result: Face = .{ + .font = ct_font, + .hb_font = hb_font, + .color = color, + .size = opts.size, + }; + result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result); + + // In debug mode, we output information about available variation axes, + // if they exist. + if (comptime builtin.mode == .Debug) { + if (ct_font.copyAttribute(.variation_axes)) |axes| { + defer axes.release(); + + var buf: [1024]u8 = undefined; + log.debug("variation axes font={s}", .{try result.name(&buf)}); + + const len = axes.getCount(); + for (0..len) |i| { + const dict = axes.getValueAtIndex(macos.foundation.Dictionary, i); + const Key = macos.text.FontVariationAxisKey; + const cf_name = dict.getValue(Key.name.Value(), Key.name.key()).?; + const cf_id = dict.getValue(Key.identifier.Value(), Key.identifier.key()).?; + const cf_min = dict.getValue(Key.minimum_value.Value(), Key.minimum_value.key()).?; + const cf_max = dict.getValue(Key.maximum_value.Value(), Key.maximum_value.key()).?; + const cf_def = dict.getValue(Key.default_value.Value(), Key.default_value.key()).?; + + const namestr = cf_name.cstring(&buf, .utf8) orelse ""; + + var id_raw: c_int = 0; + _ = cf_id.getValue(.int, &id_raw); + const id: font.face.Variation.Id = @bitCast(id_raw); + + var min: f64 = 0; + _ = cf_min.getValue(.double, &min); + + var max: f64 = 0; + _ = cf_max.getValue(.double, &max); + + var def: f64 = 0; + _ = cf_def.getValue(.double, &def); + + log.debug("variation axis: name={s} id={s} min={} max={} def={}", .{ + namestr, + id.str(), + min, + max, + def, + }); + } + } + } + + return result; + } + + pub fn deinit(self: *Face) void { + self.font.release(); + if (comptime harfbuzz_shaper) self.hb_font.destroy(); + if (self.color) |v| v.deinit(); + self.* = undefined; + } + + /// Return a new face that is the same as this but has a transformation + /// matrix applied to italicize it. + pub fn syntheticItalic(self: *const Face, opts: font.face.Options) !Face { + const ct_font = try self.font.copyWithAttributes(0.0, &italic_skew, null); + errdefer ct_font.release(); + return try initFont(ct_font, opts); + } + + /// Return a new face that is the same as this but applies a synthetic + /// bold effect to it. This is useful for fonts that don't have a bold + /// variant. + pub fn syntheticBold(self: *const Face, opts: font.face.Options) !Face { + const ct_font = try self.font.copyWithAttributes(0.0, null, null); + errdefer ct_font.release(); + var face = try initFont(ct_font, opts); + + // To determine our synthetic bold line width we get a multiplier + // from the font size in points. This is a heuristic that is based + // on the fact that a line width of 1 looks good to me at a certain + // point size. We want to scale that up roughly linearly with the + // font size. + const points_f64: f64 = @floatCast(opts.size.points); + const line_width = @max(points_f64 / 14.0, 1); + // log.debug("synthetic bold line width={}", .{line_width}); + face.synthetic_bold = line_width; + + return face; + } + + /// Returns the font name. If allocation is required, buf will be used, + /// but sometimes allocation isn't required and a static string is + /// returned. + pub fn name(self: *const Face, buf: []u8) Allocator.Error![]const u8 { + const family_name = self.font.copyFamilyName(); + if (family_name.cstringPtr(.utf8)) |str| return str; + + // "NULL if the internal storage of theString does not allow + // this to be returned efficiently." In this case, we need + // to allocate. + return family_name.cstring(buf, .utf8) orelse error.OutOfMemory; + } + + /// 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, opts: font.face.Options) !void { + // We just create a copy and replace ourself + const face = try initFontCopy(self.font, opts); + self.deinit(); + self.* = face; + } + + /// Set the variation axes for this font. This will modify this font + /// in-place. + pub fn setVariations( + self: *Face, + vs: []const font.face.Variation, + opts: font.face.Options, + ) !void { + // If we have no variations, we don't need to do anything. + if (vs.len == 0) return; + + // Create a new font descriptor with all the variations set. + var desc = self.font.copyDescriptor(); + defer desc.release(); + for (vs) |v| { + const id = try macos.foundation.Number.create(.int, @ptrCast(&v.id)); + defer id.release(); + const next = try desc.createCopyWithVariation(id, v.value); + desc.release(); + desc = next; + } + + // Put our current size in the opts so that we don't change size. + var new_opts = opts; + new_opts.size = self.size; + + // Initialize a font based on these attributes. + const ct_font = try self.font.copyWithAttributes(0, null, desc); + errdefer ct_font.release(); + const face = try initFont(ct_font, new_opts); + self.deinit(); + self.* = face; + } + + /// Returns true if the face has any glyphs that are colorized. + /// To determine if an individual glyph is colorized you must use + /// isColorGlyph. + pub fn hasColor(self: *const Face) bool { + return self.color != null; + } + + /// Returns true if the given glyph ID is colorized. + pub fn isColorGlyph(self: *const Face, glyph_id: u32) bool { + const c = self.color orelse return false; + return c.isColorGlyph(glyph_id); + } + + /// Returns the glyph index for the given Unicode code point. If this + /// face doesn't support this glyph, null is returned. + pub fn glyphIndex(self: Face, cp: u32) ?u32 { + // Turn UTF-32 into UTF-16 for CT API + var unichars: [2]u16 = undefined; + const pair = macos.foundation.stringGetSurrogatePairForLongCharacter(cp, &unichars); + const len: usize = if (pair) 2 else 1; + + // Get our glyphs + var glyphs = [2]macos.graphics.Glyph{ 0, 0 }; + if (!self.font.getGlyphsForCharacters(unichars[0..len], glyphs[0..len])) + return null; + + // We can have pairs due to chars like emoji but we expect all of them + // to decode down into exactly one glyph ID. + if (pair) assert(glyphs[1] == 0); + + return @intCast(glyphs[0]); + } + + pub fn renderGlyph( + self: Face, + alloc: Allocator, + atlas: *font.Atlas, + glyph_index: u32, + opts: font.face.RenderOptions, + ) !font.Glyph { + var glyphs = [_]macos.graphics.Glyph{@intCast(glyph_index)}; + + // Get the bounding rect for rendering this glyph. + // This is in a coordinate space with (0.0, 0.0) + // in the bottom left and +Y pointing up. + var rect = self.font.getBoundingRectsForGlyphs(.horizontal, &glyphs, null); + + // Determine whether this is a color glyph. + const is_color = self.isColorGlyph(glyph_index); + // And whether it's (probably) a bitmap (sbix). + const sbix = is_color and self.color != null and self.color.?.sbix; + + // If we're rendering a synthetic bold then we will gain 50% of + // the line width on every edge, which means we should increase + // our width and height by the line width and subtract half from + // our origin points. + // + // We don't add extra size if it's a sbix color font though, + // since bitmaps aren't affected by synthetic bold. + if (!sbix) if (self.synthetic_bold) |line_width| { + rect.size.width += line_width; + rect.size.height += line_width; + rect.origin.x -= line_width / 2; + rect.origin.y -= line_width / 2; + }; + + // If our rect is smaller than a quarter pixel in either axis + // then it has no outlines or they're too small to render. + // + // In this case we just return 0-sized glyph struct. + if (rect.size.width < 0.25 or rect.size.height < 0.25) + return font.Glyph{ + .width = 0, + .height = 0, + .offset_x = 0, + .offset_y = 0, + .atlas_x = 0, + .atlas_y = 0, + }; + + const metrics = opts.grid_metrics; + const cell_width: f64 = @floatFromInt(metrics.cell_width); + const cell_height: f64 = @floatFromInt(metrics.cell_height); + + // Next we apply any constraints to get the final size of the glyph. + const constraint = opts.constraint; + + // We need to add the baseline position before passing to the constrain + // function since it operates on cell-relative positions, not baseline. + const cell_baseline: f64 = @floatFromInt(metrics.cell_baseline); + + const glyph_size = constraint.constrain( + .{ + .width = rect.size.width, + .height = rect.size.height, + .x = rect.origin.x, + .y = rect.origin.y + cell_baseline, + }, + metrics, + opts.constraint_width, + ); + + var x = glyph_size.x; + var y = glyph_size.y; + var width = glyph_size.width; + var height = glyph_size.height; + + // We center all glyphs within the pixel-rounded and adjusted + // cell width if it's larger than the face width, so that they + // aren't weirdly off to the left. + // + // We don't do this if the glyph has a stretch constraint, + // since in that case the position was already calculated with the + // new cell width in mind. + if (constraint.size != .stretch) { + // We add half the difference to re-center. + const dx = (cell_width - metrics.face_width) / 2; + x += dx; + if (dx < 0) { + // For negative diff (cell narrower than advance), we remove the + // integer part and only keep the fractional adjustment needed + // for consistent subpixel positioning. + x -= @trunc(dx); + } + } + + // If this is a bitmap glyph, it will always render as full pixels, + // not fractional pixels, so we need to quantize its position and + // size accordingly to align to full pixels so we get good results. + if (sbix) { + width = cell_width - @round(cell_width - width - x) - @round(x); + height = cell_height - @round(cell_height - height - y) - @round(y); + x = @round(x); + y = @round(y); + } + + // We make an assumption that font smoothing ("thicken") + // adds no more than 1 extra pixel to any edge. We don't + // add extra size if it's a sbix color font though, since + // bitmaps aren't affected by smoothing. + const canvas_padding: u32 = if (opts.thicken and !sbix) 1 else 0; + + // Our whole-pixel bearings for the final glyph. + // The fractional portion will be included in the rasterized position. + const px_x = @as(i32, @intFromFloat(@floor(x))) - @as(i32, @intCast(canvas_padding)); + const px_y = @as(i32, @intFromFloat(@floor(y))) - @as(i32, @intCast(canvas_padding)); + + // We keep track of the fractional part of the pixel bearings, which + // we will add as an offset when rasterizing to make sure we get the + // correct sub-pixel position. + const frac_x = x - @floor(x); + const frac_y = y - @floor(y); + + // Add the fractional pixel to the width and height and take + // the ceiling to get a canvas size that will definitely fit + // our drawn glyph, including the fractional offset and font smoothing. + const px_width = @as(u32, @intFromFloat(@ceil(width + frac_x))) + (2 * canvas_padding); + const px_height = @as(u32, @intFromFloat(@ceil(height + frac_y))) + (2 * canvas_padding); + + // Settings that are specific to if we are rendering text or emoji. + const color: struct { + color: bool, + depth: u32, + space: *macos.graphics.ColorSpace, + context_opts: c_uint, + } = if (!is_color) .{ + .color = false, + .depth = 1, + .space = try macos.graphics.ColorSpace.createNamed(.linearGray), + .context_opts = @intFromEnum(macos.graphics.ImageAlphaInfo.only), + } else .{ + .color = true, + .depth = 4, + .space = try macos.graphics.ColorSpace.createNamed(.displayP3), + .context_opts = @intFromEnum(macos.graphics.BitmapInfo.byte_order_32_little) | + @intFromEnum(macos.graphics.ImageAlphaInfo.premultiplied_first), + }; + defer color.space.release(); + + // This is just a safety check. + if (atlas.format.depth() != color.depth) { + log.warn("font atlas color depth doesn't equal font color depth atlas={} font={}", .{ + atlas.format.depth(), + color.depth, + }); + return error.InvalidAtlasFormat; + } + + // Our buffer for rendering. We could cache this but glyph rasterization + // usually stabilizes pretty quickly and is very infrequent so I think + // the allocation overhead is acceptable compared to the cost of + // caching it forever or having to deal with a cache lifetime. + const buf = try alloc.alloc(u8, px_width * px_height * color.depth); + defer alloc.free(buf); + @memset(buf, 0); + + const context = macos.graphics.BitmapContext.context; + const ctx = try macos.graphics.BitmapContext.create( + buf, + px_width, + px_height, + 8, + px_width * color.depth, + color.space, + color.context_opts, + ); + defer context.release(ctx); + + // Perform an initial fill. This ensures that we don't have any + // uninitialized pixels in the bitmap. + if (color.color) + context.setRGBFillColor(ctx, 0, 0, 0, 0) + else + context.setGrayFillColor(ctx, 0, 0); + context.fillRect(ctx, .{ + .origin = .{ .x = 0, .y = 0 }, + .size = .{ + .width = @floatFromInt(px_width), + .height = @floatFromInt(px_height), + }, + }); + + // "Font smoothing" is what we call "thickening", it's an attempt + // to compensate for optical thinning of fonts, but at this point + // it's just something that makes the text look closer to system + // applications if users want that. + context.setAllowsFontSmoothing(ctx, true); + context.setShouldSmoothFonts(ctx, opts.thicken); + + // Subpixel positioning allows glyphs to be placed at non-integer + // coordinates. We need this for our alignment. + context.setAllowsFontSubpixelPositioning(ctx, true); + context.setShouldSubpixelPositionFonts(ctx, true); + + // We don't want subpixel quantization, since we very carefully + // manage the position of our glyphs ourselves, and dont want to + // mess that up. + context.setAllowsFontSubpixelQuantization(ctx, false); + context.setShouldSubpixelQuantizeFonts(ctx, false); + + // Anti-aliasing is self explanatory. + context.setAllowsAntialiasing(ctx, true); + context.setShouldAntialias(ctx, true); + + // Set our color for drawing + if (color.color) { + context.setRGBFillColor(ctx, 1, 1, 1, 1); + context.setRGBStrokeColor(ctx, 1, 1, 1, 1); + } else { + const strength: f64 = @floatFromInt(opts.thicken_strength); + context.setGrayFillColor(ctx, strength / 255.0, 1); + context.setGrayStrokeColor(ctx, strength / 255.0, 1); + } + + // If we are drawing with synthetic bold then use a fill stroke + // which strokes the outlines of the glyph making a more bold look. + if (self.synthetic_bold) |line_width| { + context.setTextDrawingMode(ctx, .fill_stroke); + context.setLineWidth(ctx, line_width); + } + + // Translate our drawing context so that when we draw our + // glyph the bottom/left edge is at the correct sub-pixel + // position. The bottom/left edges are guaranteed to be at + // exactly [0, 0] relative to this because when we call to + // `drawGlyphs`, we pass the negated bearings. + context.translateCTM( + ctx, + frac_x + @as(f64, @floatFromInt(canvas_padding)), + frac_y + @as(f64, @floatFromInt(canvas_padding)), + ); + + // Scale the drawing context so that when we draw + // our glyph it's stretched to the constrained size. + context.scaleCTM( + ctx, + width / rect.size.width, + height / rect.size.height, + ); + + // Draw our glyph. + // + // We offset the position by the negated bearings so that the + // glyph is drawn at exactly [0, 0], which is then offset to + // the appropriate fractional position by the translation we + // did before scaling. + self.font.drawGlyphs(&glyphs, &.{.{ + .x = -rect.origin.x, + .y = -rect.origin.y, + }}, ctx); + + // Write our rasterized glyph to the atlas. + const region = try atlas.reserve(alloc, px_width, px_height); + atlas.set(region, buf); + + // This should be the distance from the bottom of + // the cell to the top of the glyph's bounding box. + const offset_y: i32 = px_y + @as(i32, @intCast(px_height)); + + // This should be the distance from the left of + // the cell to the left of the glyph's bounding box. + const offset_x: i32 = px_x; + + return .{ + .width = px_width, + .height = px_height, + .offset_x = offset_x, + .offset_y = offset_y, + .atlas_x = region.x, + .atlas_y = region.y, + }; + } + + /// Get the `FaceMetrics` for this face. + pub fn getMetrics(self: *Face) font.Metrics.FaceMetrics { + const ct_font = self.font; + + // Read the 'head' table out of the font data. + const head_: ?opentype.Head = head: { + // macOS bitmap-only fonts use a 'bhed' tag rather than 'head', but + // the table format is byte-identical to the 'head' table, so if we + // can't find 'head' we try 'bhed' instead before failing. + // + // ref: https://fontforge.org/docs/techref/bitmaponlysfnt.html + const head_tag = macos.text.FontTableTag.init("head"); + const bhed_tag = macos.text.FontTableTag.init("bhed"); + const data = + ct_font.copyTable(head_tag) orelse + ct_font.copyTable(bhed_tag) orelse + break :head null; + defer data.release(); + const ptr = data.getPointer(); + const len = data.getLength(); + break :head opentype.Head.init(ptr[0..len]) catch |err| { + log.warn("error parsing head table: {}", .{err}); + break :head null; + }; + }; + + // Read the 'post' table out of the font data. + const post_: ?opentype.Post = post: { + const tag = macos.text.FontTableTag.init("post"); + const data = ct_font.copyTable(tag) orelse break :post null; + defer data.release(); + const ptr = data.getPointer(); + const len = data.getLength(); + break :post opentype.Post.init(ptr[0..len]) catch |err| { + log.warn("error parsing post table: {}", .{err}); + break :post null; + }; + }; + + // Read the 'OS/2' table out of the font data if it's available. + const os2_: ?opentype.OS2 = os2: { + const tag = macos.text.FontTableTag.init("OS/2"); + const data = ct_font.copyTable(tag) orelse break :os2 null; + defer data.release(); + const ptr = data.getPointer(); + const len = data.getLength(); + break :os2 opentype.OS2.init(ptr[0..len]) catch |err| { + log.warn("error parsing OS/2 table: {}", .{err}); + break :os2 null; + }; + }; + + // Read the 'hhea' table out of the font data. + const hhea_: ?opentype.Hhea = hhea: { + const tag = macos.text.FontTableTag.init("hhea"); + const data = ct_font.copyTable(tag) orelse break :hhea null; + defer data.release(); + const ptr = data.getPointer(); + const len = data.getLength(); + break :hhea opentype.Hhea.init(ptr[0..len]) catch |err| { + log.warn("error parsing hhea table: {}", .{err}); + break :hhea null; + }; + }; + + const units_per_em: f64 = + if (head_) |head| + @floatFromInt(head.unitsPerEm) + else + @floatFromInt(self.font.getUnitsPerEm()); + const px_per_em: f64 = ct_font.getSize(); + const px_per_unit: f64 = px_per_em / units_per_em; + + const ascent: f64, const descent: f64, const line_gap: f64 = vertical_metrics: { + // If we couldn't get the hhea table, rely on metrics from CoreText. + const hhea = hhea_ orelse break :vertical_metrics .{ + self.font.getAscent(), + -self.font.getDescent(), + self.font.getLeading(), + }; + + const hhea_ascent: f64 = @floatFromInt(hhea.ascender); + const hhea_descent: f64 = @floatFromInt(hhea.descender); + const hhea_line_gap: f64 = @floatFromInt(hhea.lineGap); + + // If our font has no OS/2 table, then we just + // blindly use the metrics from the hhea table. + const os2 = os2_ orelse break :vertical_metrics .{ + hhea_ascent * px_per_unit, + hhea_descent * px_per_unit, + hhea_line_gap * px_per_unit, + }; + + const os2_ascent: f64 = @floatFromInt(os2.sTypoAscender); + const os2_descent: f64 = @floatFromInt(os2.sTypoDescender); + const os2_line_gap: f64 = @floatFromInt(os2.sTypoLineGap); + + // If the font says to use typo metrics, trust it. + if (os2.fsSelection.use_typo_metrics) break :vertical_metrics .{ + os2_ascent * px_per_unit, + os2_descent * px_per_unit, + os2_line_gap * px_per_unit, + }; + + // Otherwise we prefer the height metrics from 'hhea' if they + // are available, or else OS/2 sTypo* metrics, and if all else + // fails then we use OS/2 usWin* metrics. + // + // This is not "standard" behavior, but it's our best bet to + // account for fonts being... just weird. It's pretty much what + // FreeType does to get its generic ascent and descent metrics. + + if (hhea.ascender != 0 or hhea.descender != 0) break :vertical_metrics .{ + hhea_ascent * px_per_unit, + hhea_descent * px_per_unit, + hhea_line_gap * px_per_unit, + }; + + if (os2_ascent != 0 or os2_descent != 0) break :vertical_metrics .{ + os2_ascent * px_per_unit, + os2_descent * px_per_unit, + os2_line_gap * px_per_unit, + }; + + const win_ascent: f64 = @floatFromInt(os2.usWinAscent); + const win_descent: f64 = @floatFromInt(os2.usWinDescent); + break :vertical_metrics .{ + win_ascent * px_per_unit, + // usWinDescent is *positive* -> down unlike sTypoDescender + // and hhea.Descender, so we flip its sign to fix this. + -win_descent * px_per_unit, + 0.0, + }; + }; + + const underline_position, const underline_thickness = ul: { + const post = post_ orelse break :ul .{ null, null }; + + // Some fonts have degenerate 'post' tables where the underline + // thickness (and often position) are 0. We consider them null + // if this is the case and use our own fallbacks when we calculate. + const has_broken_underline = post.underlineThickness == 0; + + // If the underline position isn't 0 then we do use it, + // even if the thickness is't properly specified. + const pos: ?f64 = if (has_broken_underline and post.underlinePosition == 0) + null + else + @as(f64, @floatFromInt(post.underlinePosition)) * px_per_unit; + + const thick: ?f64 = if (has_broken_underline) + null + else + @as(f64, @floatFromInt(post.underlineThickness)) * px_per_unit; + + break :ul .{ pos, thick }; + }; + + // Similar logic to the underline above. + const strikethrough_position, const strikethrough_thickness = st: { + const os2 = os2_ orelse break :st .{ null, null }; + + const has_broken_strikethrough = os2.yStrikeoutSize == 0; + + const pos: ?f64 = if (has_broken_strikethrough and os2.yStrikeoutPosition == 0) + null + else + @as(f64, @floatFromInt(os2.yStrikeoutPosition)) * px_per_unit; + + const thick: ?f64 = if (has_broken_strikethrough) + null + else + @as(f64, @floatFromInt(os2.yStrikeoutSize)) * px_per_unit; + + break :st .{ pos, thick }; + }; + + // We fall back to whatever CoreText does if the + // OS/2 table doesn't specify a cap or ex height. + const cap_height: f64, const ex_height: f64 = heights: { + const os2 = os2_ orelse break :heights .{ + ct_font.getCapHeight(), + ct_font.getXHeight(), + }; + + break :heights .{ + if (os2.sCapHeight) |sCapHeight| + @as(f64, @floatFromInt(sCapHeight)) * px_per_unit + else + ct_font.getCapHeight(), + + if (os2.sxHeight) |sxHeight| + @as(f64, @floatFromInt(sxHeight)) * px_per_unit + else + ct_font.getXHeight(), + }; + }; + + // Cell width is calculated by calculating the widest width of the + // visible ASCII characters. Usually 'M' is widest but we just take + // whatever is widest. + // + // ASCII height is calculated as the height of the overall bounding + // box of the same characters. + const cell_width: f64, const ascii_height: f64 = measurements: { + // Build a comptime array of all the ASCII chars + const unichars = comptime unichars: { + const len = 127 - 32; + var result: [len]u16 = undefined; + var i: u16 = 32; + while (i < 127) : (i += 1) { + result[i - 32] = i; + } + + break :unichars result; + }; + + // Get our glyph IDs for the ASCII chars + var glyphs: [unichars.len]macos.graphics.Glyph = undefined; + _ = ct_font.getGlyphsForCharacters(&unichars, &glyphs); + + // Get all our advances + var advances: [unichars.len]macos.graphics.Size = undefined; + _ = ct_font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances); + + // Find the maximum advance + var max: f64 = 0; + var i: usize = 0; + while (i < advances.len) : (i += 1) { + max = @max(advances[i].width, max); + } + + // Get the overall bounding rect for the glyphs + const rect = ct_font.getBoundingRectsForGlyphs(.horizontal, &glyphs, null); + + break :measurements .{ max, rect.size.height }; + }; + + // Measure "水" (CJK water ideograph, U+6C34) for our ic width. + const ic_width: ?f64 = ic_width: { + const glyph = self.glyphIndex('水') orelse break :ic_width null; + + const advance = ct_font.getAdvancesForGlyphs( + .horizontal, + &.{@intCast(glyph)}, + null, + ); + + const bounds = ct_font.getBoundingRectsForGlyphs( + .horizontal, + &.{@intCast(glyph)}, + null, + ); + + // If the advance of the glyph is less than the width of the actual + // glyph then we just treat it as invalid since it's probably wrong + // and using it for size normalization will instead make the font + // way too big. + // + // This can sometimes happen if there's a CJK font that has been + // patched with the nerd fonts patcher and it butchers the advance + // values so the advance ends up half the width of the actual glyph. + if (bounds.size.width > advance) { + var buf: [1024]u8 = undefined; + const font_name = self.name(&buf) catch "<Error getting font name>"; + log.warn( + "(getMetrics) Width of glyph '水' for font \"{s}\" is greater than its advance ({d} > {d}), discarding ic_width metric.", + .{ + font_name, + bounds.size.width, + advance, + }, + ); + break :ic_width null; + } + + break :ic_width advance; + }; + + return .{ + .px_per_em = px_per_em, + + .cell_width = cell_width, + + .ascent = ascent, + .descent = descent, + .line_gap = line_gap, + + .underline_position = underline_position, + .underline_thickness = underline_thickness, + + .strikethrough_position = strikethrough_position, + .strikethrough_thickness = strikethrough_thickness, + + .cap_height = cap_height, + .ex_height = ex_height, + .ascii_height = ascii_height, + .ic_width = ic_width, + }; + } + + /// Copy the font table data for the given tag. + pub fn copyTable( + self: Face, + alloc: Allocator, + tag: *const [4]u8, + ) Allocator.Error!?[]u8 { + const data = self.font.copyTable(macos.text.FontTableTag.init(tag)) orelse + return null; + defer data.release(); + + const buf = try alloc.alloc(u8, data.getLength()); + errdefer alloc.free(buf); + + const ptr = data.getPointer(); + @memcpy(buf, ptr[0..buf.len]); + + return buf; + } +}; + +/// The state associated with a font face that may have colorized glyphs. +/// This is used to determine if a specific glyph ID is colorized. +const ColorState = struct { + /// True if there is an sbix font table. For now, the mere presence + /// of an sbix font table causes us to assume the glyph is colored. + /// We can improve this later. + sbix: bool, + + /// The SVG font table data (if any), which we can use to determine + /// if a glyph is present in the SVG table. + svg: ?opentype.SVG, + svg_data: ?*macos.foundation.Data, + + pub const Error = error{InvalidSVGTable}; + + pub fn init(f: *macos.text.Font) Error!ColorState { + // sbix is true if the table exists in the font data at all. + // In the future we probably want to actually parse it and + // check for glyphs. + const sbix: bool = sbix: { + const tag = macos.text.FontTableTag.init("sbix"); + const data = f.copyTable(tag) orelse break :sbix false; + data.release(); + break :sbix data.getLength() > 0; + }; + + // Read the SVG table out of the font data. + const svg: ?struct { + svg: opentype.SVG, + data: *macos.foundation.Data, + } = svg: { + const tag = macos.text.FontTableTag.init("SVG "); + const data = f.copyTable(tag) orelse break :svg null; + errdefer data.release(); + const ptr = data.getPointer(); + const len = data.getLength(); + const svg = opentype.SVG.init(ptr[0..len]) catch |err| { + return switch (err) { + error.EndOfStream, + error.SVGVersionNotSupported, + => error.InvalidSVGTable, + }; + }; + + break :svg .{ + .svg = svg, + .data = data, + }; + }; + + return .{ + .sbix = sbix, + .svg = if (svg) |v| v.svg else null, + .svg_data = if (svg) |v| v.data else null, + }; + } + + pub fn deinit(self: *const ColorState) void { + if (self.svg_data) |v| v.release(); + } + + /// Returns true if the given glyph ID is colored. + pub fn isColorGlyph(self: *const ColorState, glyph_id: u32) bool { + // Our font system uses 32-bit glyph IDs for special values but + // actual fonts only contain 16-bit glyph IDs so if we can't cast + // into it it must be false. + const glyph_u16 = std.math.cast(u16, glyph_id) orelse return false; + + // sbix is always true for now + if (self.sbix) return true; + + // if we have svg data, check it + if (self.svg) |svg| { + if (svg.hasGlyph(glyph_u16)) return true; + } + + return false; + } +}; + +test { + const testing = std.testing; + const alloc = testing.allocator; + + var atlas = try font.Atlas.init(alloc, 512, .grayscale); + defer atlas.deinit(alloc); + + const name = try macos.foundation.String.createWithBytes("Monaco", .utf8, false); + defer name.release(); + const desc = try macos.text.FontDescriptor.createWithNameAndSize(name, 12); + defer desc.release(); + const ct_font = try macos.text.Font.createWithFontDescriptor(desc, 12); + defer ct_font.release(); + + var face = try Face.initFontCopy(ct_font, .{ .size = .{ .points = 12 } }); + defer face.deinit(); + + // Generate all visible ASCII + var i: u8 = 32; + while (i < 127) : (i += 1) { + try testing.expect(face.glyphIndex(i) != null); + _ = try face.renderGlyph( + alloc, + &atlas, + face.glyphIndex(i).?, + .{ .grid_metrics = font.Metrics.calc(face.getMetrics()) }, + ); + } +} + +test "name" { + const testing = std.testing; + + const name = try macos.foundation.String.createWithBytes("Menlo", .utf8, false); + defer name.release(); + const desc = try macos.text.FontDescriptor.createWithNameAndSize(name, 12); + defer desc.release(); + const ct_font = try macos.text.Font.createWithFontDescriptor(desc, 12); + defer ct_font.release(); + + var face = try Face.initFontCopy(ct_font, .{ .size = .{ .points = 12 } }); + defer face.deinit(); + + var buf: [1024]u8 = undefined; + const font_name = try face.name(&buf); + try testing.expectEqualStrings(font_name, "Menlo"); +} + +test "emoji" { + const testing = std.testing; + + const name = try macos.foundation.String.createWithBytes("Apple Color Emoji", .utf8, false); + defer name.release(); + const desc = try macos.text.FontDescriptor.createWithNameAndSize(name, 12); + defer desc.release(); + const ct_font = try macos.text.Font.createWithFontDescriptor(desc, 12); + defer ct_font.release(); + + var face = try Face.initFontCopy(ct_font, .{ .size = .{ .points = 18 } }); + defer face.deinit(); + + // Glyph index check + { + const id = face.glyphIndex('🥸').?; + try testing.expect(face.isColorGlyph(id)); + } +} + +test "in-memory" { + const testing = std.testing; + const alloc = testing.allocator; + const testFont = font.embedded.regular; + + var atlas = try font.Atlas.init(alloc, 512, .grayscale); + defer atlas.deinit(alloc); + + var lib = try font.Library.init(alloc); + defer lib.deinit(); + + var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } }); + defer face.deinit(); + + // Generate all visible ASCII + var i: u8 = 32; + while (i < 127) : (i += 1) { + try testing.expect(face.glyphIndex(i) != null); + _ = try face.renderGlyph( + alloc, + &atlas, + face.glyphIndex(i).?, + .{ .grid_metrics = font.Metrics.calc(face.getMetrics()) }, + ); + } +} + +test "variable" { + const testing = std.testing; + const alloc = testing.allocator; + const testFont = font.embedded.variable; + + var atlas = try font.Atlas.init(alloc, 512, .grayscale); + defer atlas.deinit(alloc); + + var lib = try font.Library.init(alloc); + defer lib.deinit(); + + var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } }); + defer face.deinit(); + + // Generate all visible ASCII + var i: u8 = 32; + while (i < 127) : (i += 1) { + try testing.expect(face.glyphIndex(i) != null); + _ = try face.renderGlyph( + alloc, + &atlas, + face.glyphIndex(i).?, + .{ .grid_metrics = font.Metrics.calc(face.getMetrics()) }, + ); + } +} + +test "variable set variation" { + const testing = std.testing; + const alloc = testing.allocator; + const testFont = font.embedded.variable; + + var atlas = try font.Atlas.init(alloc, 512, .grayscale); + defer atlas.deinit(alloc); + + var lib = try font.Library.init(alloc); + defer lib.deinit(); + + var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } }); + defer face.deinit(); + + try face.setVariations(&.{ + .{ .id = font.face.Variation.Id.init("wght"), .value = 400 }, + }, .{ .size = .{ .points = 12 } }); + + // Generate all visible ASCII + var i: u8 = 32; + while (i < 127) : (i += 1) { + try testing.expect(face.glyphIndex(i) != null); + _ = try face.renderGlyph( + alloc, + &atlas, + face.glyphIndex(i).?, + .{ .grid_metrics = font.Metrics.calc(face.getMetrics()) }, + ); + } +} + +test "svg font table" { + const testing = std.testing; + const alloc = testing.allocator; + const testFont = font.embedded.julia_mono; + + var lib = try font.Library.init(alloc); + defer lib.deinit(); + + var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } }); + defer face.deinit(); + + const table = (try face.copyTable(alloc, "SVG ")).?; + defer alloc.free(table); + + try testing.expect(table.len > 0); +} + +test "glyphIndex colored vs text" { + const testing = std.testing; + const alloc = testing.allocator; + const testFont = font.embedded.julia_mono; + + var lib = try font.Library.init(alloc); + defer lib.deinit(); + + var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } }); + defer face.deinit(); + + { + const glyph = face.glyphIndex('A').?; + try testing.expectEqual(4, glyph); + try testing.expect(!face.isColorGlyph(glyph)); + } + + { + const glyph = face.glyphIndex(0xE800).?; + try testing.expectEqual(11482, glyph); + try testing.expect(face.isColorGlyph(glyph)); + } +} diff --git a/vendor/libghostty-vt/src/font/face/freetype.zig b/vendor/libghostty-vt/src/font/face/freetype.zig new file mode 100644 index 00000000..528f72d5 --- /dev/null +++ b/vendor/libghostty-vt/src/font/face/freetype.zig @@ -0,0 +1,1439 @@ +//! Face represents a single font face. A single font face has a single set +//! of properties associated with it such as style, weight, etc. +//! +//! A Face isn't typically meant to be used directly. It is usually used +//! via a Family in order to store it in an Atlas. + +const std = @import("std"); +const builtin = @import("builtin"); +const freetype = @import("freetype"); +const harfbuzz = @import("harfbuzz"); +const stb = @import("../../stb/main.zig"); +const assert = @import("../../quirks.zig").inlineAssert; +const testing = std.testing; +const Allocator = std.mem.Allocator; +const font = @import("../main.zig"); +const Glyph = font.Glyph; +const Library = font.Library; +const opentype = @import("../opentype.zig"); +const quirks = @import("../../quirks.zig"); +const config = @import("../../config.zig"); + +const F26Dot6 = opentype.sfnt.F26Dot6; + +const log = std.log.scoped(.font_face); + +pub const Face = struct { + comptime { + // If we have the freetype backend, we should have load flags. + assert(font.face.FreetypeLoadFlags != void); + } + + /// Our Library + lib: Library, + + /// Our font face. + face: freetype.Face, + + /// This mutex MUST be held while doing anything with the + /// glyph slot on the freetype face, because this struct + /// may be shared across multiple surfaces. + /// + /// This means that anywhere where `self.face.loadGlyph` + /// is called, this mutex must be held. + ft_mutex: *std.Thread.Mutex, + + /// Harfbuzz font corresponding to this face. + hb_font: harfbuzz.Font, + + /// Freetype load flags for this font face. + load_flags: font.face.FreetypeLoadFlags, + + /// Set quirks.disableDefaultFontFeatures + quirks_disable_default_font_features: bool = false, + + /// Set to true to apply a synthetic italic to the face. + synthetic: packed struct { + italic: bool = false, + bold: bool = false, + } = .{}, + + /// The current size this font is set to. + size: font.face.DesiredSize, + + /// Initialize a new font face with the given source in-memory. + pub fn initFile( + lib: Library, + path: [:0]const u8, + index: i32, + opts: font.face.Options, + ) !Face { + lib.mutex.lock(); + defer lib.mutex.unlock(); + const face = try lib.lib.initFace(path, index); + errdefer face.deinit(); + return try initFace(lib, face, opts); + } + + /// Initialize a new font face with the given source in-memory. + pub fn init( + lib: Library, + source: [:0]const u8, + opts: font.face.Options, + ) !Face { + lib.mutex.lock(); + defer lib.mutex.unlock(); + const face = try lib.lib.initMemoryFace(source, 0); + errdefer face.deinit(); + return try initFace(lib, face, opts); + } + + fn initFace( + lib: Library, + face: freetype.Face, + opts: font.face.Options, + ) !Face { + try face.selectCharmap(.unicode); + try setSize_(face, opts.size); + + var hb_font = try harfbuzz.freetype.createFont(face.handle); + errdefer hb_font.destroy(); + + const ft_mutex = try lib.alloc.create(std.Thread.Mutex); + errdefer lib.alloc.destroy(ft_mutex); + ft_mutex.* = .{}; + + var result: Face = .{ + .lib = lib, + .face = face, + .hb_font = hb_font, + .ft_mutex = ft_mutex, + .load_flags = opts.freetype_load_flags, + .size = opts.size, + }; + result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result); + + // In debug mode, we output information about available variation axes, + // if they exist. + if (comptime builtin.mode == .Debug) mm: { + if (!face.hasMultipleMasters()) break :mm; + var buf: [1024]u8 = undefined; + log.debug("variation axes font={s}", .{try result.name(&buf)}); + + const mm = try face.getMMVar(); + defer lib.lib.doneMMVar(mm); + for (0..mm.num_axis) |i| { + const axis = mm.axis[i]; + const id_raw = std.math.cast(c_int, axis.tag) orelse continue; + const id: font.face.Variation.Id = @bitCast(id_raw); + log.debug("variation axis: name={s} id={s} min={} max={} def={}", .{ + std.mem.sliceTo(axis.name, 0), + id.str(), + axis.minimum >> 16, + axis.maximum >> 16, + axis.def >> 16, + }); + } + } + + return result; + } + + pub fn deinit(self: *Face) void { + self.lib.alloc.destroy(self.ft_mutex); + { + self.lib.mutex.lock(); + defer self.lib.mutex.unlock(); + + self.face.deinit(); + } + self.hb_font.destroy(); + self.* = undefined; + } + + /// Returns the font name. If allocation is required, buf will be used, + /// but sometimes allocation isn't required and a static string is + /// returned. + pub fn name(self: *const Face, buf: []u8) Allocator.Error![]const u8 { + const count = self.face.getSfntNameCount(); + + // We look for the font family entry. + for (0..count) |i| { + const entry = self.face.getSfntName(i) catch continue; + if (entry.name_id == freetype.c.TT_NAME_ID_FONT_FAMILY) { + const string = entry.string[0..entry.string_len]; + // There are other encodings that are something other than UTF-8 + // but this is one we've seen "in the wild" so far. + if (entry.platform_id == freetype.c.TT_PLATFORM_MICROSOFT and entry.encoding_id == freetype.c.TT_MS_ID_UNICODE_CS) skip: { + if (string.len % 2 != 0) break :skip; + if (string.len > 1024) break :skip; + var tmp: [512]u16 = undefined; + const max = string.len / 2; + for (@as([]const u16, @ptrCast(@alignCast(string))), 0..) |c, j| tmp[j] = @byteSwap(c); + const len = std.unicode.utf16LeToUtf8(buf, tmp[0..max]) catch return string; + return buf[0..len]; + } + return string; + } + } + + return ""; + } + + test "face name" { + const embedded = @import("../embedded.zig"); + + var lib: Library = try .init(testing.allocator); + defer lib.deinit(); + + { + var face: Face = try .init(lib, embedded.variable, .{ .size = .{ .points = 14 } }); + defer face.deinit(); + + var buf: [1024]u8 = undefined; + const actual = try face.name(&buf); + + try testing.expectEqualStrings("JetBrains Mono", actual); + } + + { + var face: Face = try .init(lib, embedded.inconsolata, .{ .size = .{ .points = 14 } }); + defer face.deinit(); + + var buf: [1024]u8 = undefined; + const actual = try face.name(&buf); + + try testing.expectEqualStrings("Inconsolata", actual); + } + } + + /// Return a new face that is the same as this but also has synthetic + /// bold applied. + pub fn syntheticBold(self: *const Face, opts: font.face.Options) !Face { + // Increase face ref count + self.face.ref(); + errdefer self.face.deinit(); + + var f = try initFace(self.lib, self.face, opts); + errdefer f.deinit(); + f.synthetic = self.synthetic; + f.synthetic.bold = true; + + return f; + } + + /// Return a new face that is the same as this but has a transformation + /// matrix applied to italicize it. + pub fn syntheticItalic(self: *const Face, opts: font.face.Options) !Face { + // Increase face ref count + self.face.ref(); + errdefer self.face.deinit(); + + var f = try initFace(self.lib, self.face, opts); + errdefer f.deinit(); + f.synthetic = self.synthetic; + f.synthetic.italic = true; + + return f; + } + + /// 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, opts: font.face.Options) !void { + try setSize_(self.face, opts.size); + self.size = opts.size; + } + + fn setSize_(face: freetype.Face, size: font.face.DesiredSize) !void { + // If we have fixed sizes, we just have to try to pick the one closest + // to what the user requested. Otherwise, we can choose an arbitrary + // pixel size. + if (face.isScalable()) { + const size_26dot6: i32 = @intFromFloat(@round(size.points * 64)); + try face.setCharSize(0, size_26dot6, size.xdpi, size.ydpi); + } else try selectSizeNearest(face, @intFromFloat(@round(size.pixels()))); + } + + /// Selects the fixed size in the loaded face that is closest to the + /// requested pixel size. + fn selectSizeNearest(face: freetype.Face, size: u32) !void { + var i: i32 = 0; + var best_i: i32 = 0; + var best_diff: i32 = 0; + while (i < face.handle.*.num_fixed_sizes) : (i += 1) { + const width = face.handle.*.available_sizes[@intCast(i)].width; + const diff = @as(i32, @intCast(size)) - @as(i32, @intCast(width)); + if (i == 0 or diff < best_diff) { + best_diff = diff; + best_i = i; + } + } + + try face.selectSize(best_i); + } + + /// Set the variation axes for this font. This will modify this font + /// in-place. + pub fn setVariations( + self: *Face, + vs: []const font.face.Variation, + opts: font.face.Options, + ) !void { + _ = opts; + + // If this font doesn't support variations, we can't do anything. + if (!self.face.hasMultipleMasters() or vs.len == 0) return; + + // Freetype requires that we send ALL coordinates in at once so the + // first thing we have to do is get all the vars and put them into + // an array. + const mm = try self.face.getMMVar(); + defer self.lib.lib.doneMMVar(mm); + + // To avoid allocations, we cap the number of variation axes we can + // support. This is arbitrary but Firefox caps this at 16 so I + // feel like that's probably safe... and we do double cause its + // cheap. + var coords_buf: [32]freetype.c.FT_Fixed = undefined; + var coords = coords_buf[0..@min(coords_buf.len, mm.num_axis)]; + try self.face.getVarDesignCoordinates(coords); + + // Now we go through each axis and see if its set. This is slow + // but there usually aren't many axes and usually not many set + // variations, either. + for (0..mm.num_axis) |i| { + const axis = mm.axis[i]; + const id = std.math.cast(u32, axis.tag) orelse continue; + for (vs) |v| { + if (id == @as(u32, @bitCast(v.id))) { + coords[i] = @intFromFloat(v.value * 65536); + break; + } + } + } + + // Set them! + try self.face.setVarDesignCoordinates(coords); + } + + /// Returns the glyph index for the given Unicode code point. If this + /// face doesn't support this glyph, null is returned. + pub fn glyphIndex(self: Face, cp: u32) ?u32 { + return self.face.getCharIndex(cp); + } + + /// Returns true if this font is colored. This can be used by callers to + /// determine what kind of atlas to pass in. + pub fn hasColor(self: Face) bool { + return self.face.hasColor(); + } + + /// Returns true if the given glyph ID is colorized. + pub fn isColorGlyph(self: *const Face, glyph_id: u32) bool { + self.ft_mutex.lock(); + defer self.ft_mutex.unlock(); + + // Load the glyph and see what pixel mode it renders with. + // All modes other than BGRA are non-color. + // If the glyph fails to load, just return false. + self.face.loadGlyph(glyph_id, .{ + .render = true, + .color = self.face.hasColor(), + // NO_SVG set to true because we don't currently support rendering + // SVG glyphs under FreeType, since that requires bundling another + // dependency to handle rendering the SVG. + .no_svg = true, + }) catch return false; + + const glyph = self.face.handle.*.glyph; + + return glyph.*.bitmap.pixel_mode == freetype.c.FT_PIXEL_MODE_BGRA; + } + + /// Set the load flags to use when loading a glyph for measurement or + /// rendering. + fn glyphLoadFlags(self: Face, constrained: bool) freetype.LoadFlags { + // Hinting should only be enabled if the configured load flags specify + // it and the provided constraint doesn't actually do anything, since + // if it does, then it'll mess up the hinting anyway when it moves or + // resizes the glyph. + const do_hinting = self.load_flags.hinting and !constrained; + + return .{ + // If our glyph has color, we want to render the color + .color = self.face.hasColor(), + + // We don't render, because we'll invoke the render + // manually after applying constraints further down. + .render = false, + + // use options from config + .no_hinting = !do_hinting, + .force_autohint = self.load_flags.@"force-autohint", + .no_autohint = !self.load_flags.autohint, + + // If we're gonna be rendering this glyph in monochrome, + // then we should use the monochrome hinter as well, or + // else it won't look very good at all. + // + // Otherwise if the user asked for light hinting we + // use that, otherwise we just use the normal target. + .target = if (self.load_flags.monochrome) + .mono + else if (self.load_flags.light) + .light + else + .normal, + + // NO_SVG set to true because we don't currently support rendering + // SVG glyphs under FreeType, since that requires bundling another + // dependency to handle rendering the SVG. + .no_svg = true, + }; + } + + /// Get a rect that represents the position and size of the loaded glyph. + fn getGlyphSize(glyph: freetype.c.FT_GlyphSlot) font.face.GlyphSize { + // If we're dealing with an outline glyph then we get the + // outline's bounding box instead of using the built-in + // metrics, since that's more precise and allows better + // cell-fitting. + if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_OUTLINE) { + // Get the glyph's bounding box before we transform it at all. + // We use this rather than the metrics, since it's more precise. + var bbox: freetype.c.FT_BBox = undefined; + _ = freetype.c.FT_Outline_Get_BBox(&glyph.*.outline, &bbox); + + return .{ + .x = f26dot6ToF64(bbox.xMin), + .y = f26dot6ToF64(bbox.yMin), + .width = f26dot6ToF64(bbox.xMax - bbox.xMin), + .height = f26dot6ToF64(bbox.yMax - bbox.yMin), + }; + } + + return .{ + .x = f26dot6ToF64(glyph.*.metrics.horiBearingX), + .y = f26dot6ToF64(glyph.*.metrics.horiBearingY - glyph.*.metrics.height), + .width = f26dot6ToF64(glyph.*.metrics.width), + .height = f26dot6ToF64(glyph.*.metrics.height), + }; + } + + /// 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, + opts: font.face.RenderOptions, + ) !Glyph { + self.ft_mutex.lock(); + defer self.ft_mutex.unlock(); + + // Load the glyph. + try self.face.loadGlyph(glyph_index, self.glyphLoadFlags(opts.constraint.doesAnything())); + const glyph = self.face.handle.*.glyph; + + // For synthetic bold, we embolden the glyph. + if (self.synthetic.bold) { + // We need to scale the embolden amount based on the font size. + // This is a heuristic I found worked well across a variety of + // founts: 1 pixel per 64 units of height. + const font_height: f64 = @floatFromInt(self.face.handle.*.size.*.metrics.height); + const ratio: f64 = 64.0 / 2048.0; + const amount = @ceil(font_height * ratio); + _ = freetype.c.FT_Outline_Embolden(&glyph.*.outline, @intFromFloat(amount)); + } + + // We get a rect that represents the position + // and size of the glyph before constraints. + const rect = getGlyphSize(glyph); + + // If our glyph is smaller than a quarter pixel in either axis + // then it has no outlines or they're too small to render. + // + // In this case we just return 0-sized glyph struct. + if (rect.width < 0.25 or rect.height < 0.25) + return font.Glyph{ + .width = 0, + .height = 0, + .offset_x = 0, + .offset_y = 0, + .atlas_x = 0, + .atlas_y = 0, + }; + + const metrics = opts.grid_metrics; + const cell_width: f64 = @floatFromInt(metrics.cell_width); + const cell_height: f64 = @floatFromInt(metrics.cell_height); + + // Next we apply any constraints to get the final size of the glyph. + const constraint = opts.constraint; + + // We need to add the baseline position before passing to the constrain + // function since it operates on cell-relative positions, not baseline. + const cell_baseline: f64 = @floatFromInt(metrics.cell_baseline); + + const glyph_size = constraint.constrain( + .{ + .width = rect.width, + .height = rect.height, + .x = rect.x, + .y = rect.y + cell_baseline, + }, + metrics, + opts.constraint_width, + ); + + var width = glyph_size.width; + var height = glyph_size.height; + var x = glyph_size.x; + var y = glyph_size.y; + + // We center all glyphs within the pixel-rounded and adjusted + // cell width if it's larger than the face width, so that they + // aren't weirdly off to the left. + // + // We don't do this if the glyph has a stretch constraint, + // since in that case the position was already calculated with the + // new cell width in mind. + if ((constraint.size != .stretch) and (metrics.face_width < cell_width)) { + // We add half the difference to re-center. + // + // NOTE: We round this to a whole-pixel amount because under + // FreeType, the outlines will be hinted, which isn't + // the case under CoreText. If we move the outlines by + // a non-whole-pixel amount, it completely ruins the + // hinting. + x += @round((cell_width - metrics.face_width) / 2); + } + + // If this is a bitmap glyph, it will always render as full pixels, + // not fractional pixels, so we need to quantize its position and + // size accordingly to align to full pixels so we get good results. + if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_BITMAP) { + width = cell_width - @round(cell_width - width - x) - @round(x); + height = cell_height - @round(cell_height - height - y) - @round(y); + x = @round(x); + y = @round(y); + } + + // Now we can render the glyph. + var bitmap: freetype.c.FT_Bitmap = undefined; + _ = freetype.c.FT_Bitmap_Init(&bitmap); + defer _ = freetype.c.FT_Bitmap_Done(self.lib.lib.handle, &bitmap); + switch (glyph.*.format) { + freetype.c.FT_GLYPH_FORMAT_OUTLINE => { + // Manually adjust the glyph outline with this transform. + // + // This offers better precision than using the freetype transform + // matrix, since that has 16.16 coefficients, and also I was having + // weird issues that I can only assume where due to freetype doing + // some bad caching or something when I did this using the matrix. + const scale_x = width / rect.width; + const scale_y = height / rect.height; + const skew: f64 = + if (self.synthetic.italic) + // We skew by 12 degrees to synthesize italics. + @tan(std.math.degreesToRadians(12)) + else + 0.0; + + const outline = &glyph.*.outline; + for (outline.points[0..@intCast(outline.n_points)]) |*p| { + // Convert to f64 for processing + var px = f26dot6ToF64(p.x); + var py = f26dot6ToF64(p.y); + + // Subtract original bearings + px -= rect.x; + py -= rect.y; + + // Scale + px *= scale_x; + py *= scale_y; + + // Add new bearings + px += x; + py += y - cell_baseline; + + // Skew + px += py * skew; + + // Convert back and store + p.x = @as(i32, @bitCast(F26Dot6.from(px))); + p.y = @as(i32, @bitCast(F26Dot6.from(py))); + } + + try self.face.renderGlyph( + if (self.load_flags.monochrome) + .mono + else + .normal, + ); + + // Copy the glyph's bitmap, making sure + // that it's 8bpp and densely packed. + if (freetype.c.FT_Bitmap_Convert( + self.lib.lib.handle, + &glyph.*.bitmap, + &bitmap, + 1, + ) != 0) { + return error.BitmapHandlingError; + } + }, + + freetype.c.FT_GLYPH_FORMAT_BITMAP => { + // If our glyph has a non-color bitmap, we need + // to convert it to dense 8bpp so that the scale + // operation works correctly. + switch (glyph.*.bitmap.pixel_mode) { + freetype.c.FT_PIXEL_MODE_BGRA, + freetype.c.FT_PIXEL_MODE_GRAY, + => {}, + else => { + // Make sure the slot owns its bitmap, + // since we'll be modifying it here. + if (freetype.c.FT_GlyphSlot_Own_Bitmap(glyph) != 0) { + return error.BitmapHandlingError; + } + + var converted: freetype.c.FT_Bitmap = undefined; + freetype.c.FT_Bitmap_Init(&converted); + if (freetype.c.FT_Bitmap_Convert( + self.lib.lib.handle, + &glyph.*.bitmap, + &converted, + 1, + ) != 0) { + return error.BitmapHandlingError; + } + // Free the existing glyph bitmap and + // replace it with the converted one. + _ = freetype.c.FT_Bitmap_Done( + self.lib.lib.handle, + &glyph.*.bitmap, + ); + glyph.*.bitmap = converted; + }, + } + + const glyph_bitmap = glyph.*.bitmap; + + // Round our target width and height + // as the size for our scaled bitmap. + const w: u32 = @intFromFloat(@round(width)); + const h: u32 = @intFromFloat(@round(height)); + const pitch = w * atlas.format.depth(); + + // Allocate a buffer for our scaled bitmap. + // + // We'll copy this to the original bitmap once we're + // done so we can free it at the end of this scope. + const buf = try alloc.alloc(u8, pitch * h); + defer alloc.free(buf); + + // Resize + if (stb.stbir_resize_uint8( + glyph_bitmap.buffer, + @intCast(glyph_bitmap.width), + @intCast(glyph_bitmap.rows), + glyph_bitmap.pitch, + buf.ptr, + @intCast(w), + @intCast(h), + @intCast(pitch), + atlas.format.depth(), + ) == 0) { + // This should never fail because this is a + // fairly straightforward in-memory operation... + return error.GlyphResizeFailed; + } + + const scaled_bitmap: freetype.c.FT_Bitmap = .{ + .buffer = buf.ptr, + .width = @intCast(w), + .rows = @intCast(h), + .pitch = @intCast(pitch), + .pixel_mode = glyph_bitmap.pixel_mode, + .num_grays = glyph_bitmap.num_grays, + }; + + // Replace the bitmap's buffer and size info. + if (freetype.c.FT_Bitmap_Copy( + self.lib.lib.handle, + &scaled_bitmap, + &bitmap, + ) != 0) { + return error.BitmapHandlingError; + } + + // Update the bearings to account for the new positioning. + glyph.*.bitmap_top = @intFromFloat(@floor(y - cell_baseline + height)); + glyph.*.bitmap_left = @intFromFloat(@floor(x)); + }, + + else => |f| { + // Glyph formats are tags, so we can + // output a semi-readable error here. + // Use @bitCast to u32 because MSVC translates C enums + // as signed int, while GCC/Clang uses unsigned int. + const tag: u32 = @bitCast(f); + log.err( + "Can't render glyph with unsupported glyph format \"{s}\"", + .{[4]u8{ + @truncate(tag >> 24), + @truncate(tag >> 16), + @truncate(tag >> 8), + @truncate(tag >> 0), + }}, + ); + return error.UnsupportedGlyphFormat; + }, + } + + // If this is a color glyph but we're trying to render it to the + // grayscale atlas, or vice versa, then we throw and error. Maybe + // in the future we could convert, but for now it should be fine. + switch (bitmap.pixel_mode) { + freetype.c.FT_PIXEL_MODE_GRAY => if (atlas.format != .grayscale) { + return error.WrongAtlas; + }, + freetype.c.FT_PIXEL_MODE_BGRA => if (atlas.format != .bgra) { + return error.WrongAtlas; + }, + else => { + log.warn("glyph={} pixel mode={}", .{ glyph_index, bitmap.pixel_mode }); + @panic("unsupported pixel mode"); + }, + } + + // Our whole-pixel bearings for the final glyph. + // The fractional portion will be included in the rasterized position. + // + // For the Y position, FreeType's `bitmap_top` is the distance from the + // baseline to the top of the glyph, but we need the distance from the + // bottom of the cell to the bottom of the glyph, so first we add the + // baseline to get the distance from the bottom of the cell to the top + // of the glyph, then we subtract the height of the glyph to get the + // bottom. + const px_x: i32 = glyph.*.bitmap_left; + const px_y: i32 = glyph.*.bitmap_top + + @as(i32, @intCast(metrics.cell_baseline)) - + @as(i32, @intCast(bitmap.rows)); + + const px_width = bitmap.width; + const px_height = bitmap.rows; + const len: usize = @intCast( + @as(c_uint, @intCast(@abs(bitmap.pitch))) * bitmap.rows, + ); + + // If our bitmap is grayscale, make sure to multiply all pixel + // values by the right factor to bring `num_grays` up to 256. + // + // This is necessary because FT_Bitmap_Convert doesn't do this, + // it just sets num_grays to the correct number and uses the + // original smaller pixel values. + if (bitmap.pixel_mode == freetype.c.FT_PIXEL_MODE_GRAY and + bitmap.num_grays < 256) + { + const factor: u8 = @intCast(255 / (bitmap.num_grays - 1)); + for (bitmap.buffer[0..len]) |*p| { + p.* *= factor; + } + bitmap.num_grays = 256; + } + + // Must have non-empty bitmap because we return earlier if zero. + // We assume the rest of this that it is non-zero so this is important. + assert(px_width > 0 and px_height > 0); + + // If this doesn't match then something is wrong. + assert(px_width * atlas.format.depth() == bitmap.pitch); + + // Allocate our texture atlas region and copy our bitmap in to it. + const region = try atlas.reserve(alloc, px_width, px_height); + atlas.set(region, bitmap.buffer[0..len]); + + // This should be the distance from the bottom of + // the cell to the top of the glyph's bounding box. + const offset_y: i32 = px_y + @as(i32, @intCast(px_height)); + + // This should be the distance from the left of + // the cell to the left of the glyph's bounding box. + const offset_x: i32 = px_x; + + return Glyph{ + .width = px_width, + .height = px_height, + .offset_x = offset_x, + .offset_y = offset_y, + .atlas_x = region.x, + .atlas_y = region.y, + }; + } + + /// Convert 16.6 pixel format to pixels based on the scale factor of the + /// current font size. + fn unitsToPxY(self: Face, units: i32) i32 { + return @intCast(freetype.mulFix( + units, + @intCast(self.face.handle.*.size.*.metrics.y_scale), + ) >> 6); + } + + /// Convert 26.6 pixel format to f32 + fn f26dot6ToFloat(v: freetype.c.FT_F26Dot6) f32 { + return @floatFromInt(v >> 6); + } + + fn f26dot6ToF64(v: freetype.c.FT_F26Dot6) f64 { + return @as(F26Dot6, @bitCast(@as(i32, @intCast(v)))).to(f64); + } + + /// Get the `FaceMetrics` for this face. + pub fn getMetrics(self: *Face) font.Metrics.FaceMetrics { + const face = self.face; + + const size_metrics = face.handle.*.size.*.metrics; + + // This code relies on this assumption, and it should always be + // true since we don't do any non-uniform scaling on the font ever. + assert(size_metrics.x_ppem == size_metrics.y_ppem); + + // Read the 'head' table out of the font data. + const head_ = face.getSfntTable(.head); + + // Read the 'post' table out of the font data. + const post_ = face.getSfntTable(.post); + + // Read the 'OS/2' table out of the font data. + const os2_: ?*freetype.c.TT_OS2 = os2: { + const os2 = face.getSfntTable(.os2) orelse break :os2 null; + if (os2.version == 0xFFFF) break :os2 null; + break :os2 os2; + }; + + // Read the 'hhea' table out of the font data. + const hhea_ = face.getSfntTable(.hhea); + + // Whether the font is in a scalable format. We need to know this + // because many of the metrics provided by FreeType are invalid for + // non-scalable fonts. + const is_scalable = face.handle.*.face_flags & freetype.c.FT_FACE_FLAG_SCALABLE != 0; + + // We get the UPM from the head table. + // + // If we have no head, but it is a scalable face, take the UPM from + // FreeType's units_per_EM, otherwise we'll assume that UPM == PPEM. + const units_per_em: freetype.c.FT_UShort = + if (head_) |head| + head.Units_Per_EM + else if (is_scalable) + face.handle.*.units_per_EM + else + size_metrics.y_ppem; + const px_per_em: f64 = @floatFromInt(size_metrics.y_ppem); + const px_per_unit = px_per_em / @as(f64, @floatFromInt(units_per_em)); + + const ascent: f64, const descent: f64, const line_gap: f64 = vertical_metrics: { + const hhea = hhea_ orelse { + // If we couldn't get the hhea table, rely on metrics from FreeType. + const ascender = f26dot6ToF64(size_metrics.ascender); + const descender = f26dot6ToF64(size_metrics.descender); + const height = f26dot6ToF64(size_metrics.height); + break :vertical_metrics .{ + ascender, + descender, + // We compute the line gap by adding the (negative) descender + // and subtracting the (positive) ascender from the line height + // to get the remaining gap size. + // + // NOTE: This might always be 0... but it doesn't hurt to do. + height + descender - ascender, + }; + }; + + const hhea_ascent: f64 = @floatFromInt(hhea.Ascender); + const hhea_descent: f64 = @floatFromInt(hhea.Descender); + const hhea_line_gap: f64 = @floatFromInt(hhea.Line_Gap); + + // If our font has no OS/2 table, then we just + // blindly use the metrics from the hhea table. + const os2 = os2_ orelse break :vertical_metrics .{ + hhea_ascent * px_per_unit, + hhea_descent * px_per_unit, + hhea_line_gap * px_per_unit, + }; + + const os2_ascent: f64 = @floatFromInt(os2.sTypoAscender); + const os2_descent: f64 = @floatFromInt(os2.sTypoDescender); + const os2_line_gap: f64 = @floatFromInt(os2.sTypoLineGap); + + // If the font says to use typo metrics, trust it. + // (The USE_TYPO_METRICS bit is bit 7) + if (os2.fsSelection & (1 << 7) != 0) { + break :vertical_metrics .{ + os2_ascent * px_per_unit, + os2_descent * px_per_unit, + os2_line_gap * px_per_unit, + }; + } + + // Otherwise we prefer the height metrics from 'hhea' if they + // are available, or else OS/2 sTypo* metrics, and if all else + // fails then we use OS/2 usWin* metrics. + // + // This is not "standard" behavior, but it's our best bet to + // account for fonts being... just weird. It's pretty much what + // FreeType does to get its generic ascent and descent metrics. + + if (hhea.Ascender != 0 or hhea.Descender != 0) { + break :vertical_metrics .{ + hhea_ascent * px_per_unit, + hhea_descent * px_per_unit, + hhea_line_gap * px_per_unit, + }; + } + + if (os2_ascent != 0 or os2_descent != 0) { + break :vertical_metrics .{ + os2_ascent * px_per_unit, + os2_descent * px_per_unit, + os2_line_gap * px_per_unit, + }; + } + + const win_ascent: f64 = @floatFromInt(os2.usWinAscent); + const win_descent: f64 = @floatFromInt(os2.usWinDescent); + break :vertical_metrics .{ + win_ascent * px_per_unit, + // usWinDescent is *positive* -> down unlike sTypoDescender + // and hhea.Descender, so we flip its sign to fix this. + -win_descent * px_per_unit, + 0.0, + }; + }; + + const underline_position: ?f64, const underline_thickness: ?f64 = ul: { + const post = post_ orelse break :ul .{ null, null }; + + // Some fonts have degenerate 'post' tables where the underline + // thickness (and often position) are 0. We consider them null + // if this is the case and use our own fallbacks when we calculate. + const has_broken_underline = post.underlineThickness == 0; + + // If the underline position isn't 0 then we do use it, + // even if the thickness is't properly specified. + const pos: ?f64 = if (has_broken_underline and post.underlinePosition == 0) + null + else + @as(f64, @floatFromInt(post.underlinePosition)) * px_per_unit; + + const thick: ?f64 = if (has_broken_underline) + null + else + @as(f64, @floatFromInt(post.underlineThickness)) * px_per_unit; + + break :ul .{ pos, thick }; + }; + + // Similar logic to the underline above. + const strikethrough_position, const strikethrough_thickness = st: { + const os2 = os2_ orelse break :st .{ null, null }; + + const has_broken_strikethrough = os2.yStrikeoutSize == 0; + + const pos: ?f64 = if (has_broken_strikethrough and os2.yStrikeoutPosition == 0) + null + else + @as(f64, @floatFromInt(os2.yStrikeoutPosition)) * px_per_unit; + + const thick: ?f64 = if (has_broken_strikethrough) + null + else + @as(f64, @floatFromInt(os2.yStrikeoutSize)) * px_per_unit; + + break :st .{ pos, thick }; + }; + + // Cell width is calculated by calculating the widest width of the + // visible ASCII characters. Usually 'M' is widest but we just take + // whatever is widest. + // + // ASCII height is calculated as the height of the overall bounding + // box of the same characters. + // + // If we fail to load any visible ASCII we just use max_advance from + // the metrics provided by FreeType, and set ascii_height to null as + // it's optional. + const cell_width: f64, const ascii_height: ?f64 = measurements: { + self.ft_mutex.lock(); + defer self.ft_mutex.unlock(); + + var max: f64 = 0.0; + var top: f64 = 0.0; + var bottom: f64 = 0.0; + var c: u8 = ' '; + while (c < 127) : (c += 1) { + if (face.getCharIndex(c)) |glyph_index| { + if (face.loadGlyph(glyph_index, self.glyphLoadFlags(false))) { + const glyph = face.handle.*.glyph; + max = @max( + f26dot6ToF64(glyph.*.advance.x), + max, + ); + const rect = getGlyphSize(glyph); + top = @max(rect.y + rect.height, top); + bottom = @min(rect.y, bottom); + } else |_| {} + } + } + + // If we couldn't get valid measurements, just use + // FreeType's max_advance and null, respectively. + if (max == 0.0) { + max = f26dot6ToF64(size_metrics.max_advance); + } + const rect_height: ?f64 = rect_height: { + const estimate = top - bottom; + if (estimate <= 0.0) { + break :rect_height null; + } + break :rect_height estimate; + }; + + break :measurements .{ max, rect_height }; + }; + + // We use the cap and ex heights specified by the font if they're + // available, otherwise we try to measure the `H` and `x` glyphs. + const cap_height: ?f64, const ex_height: ?f64 = heights: { + if (os2_) |os2| { + // The OS/2 table does not include these metrics in version 1. + if (os2.version >= 2) { + break :heights .{ + @as(f64, @floatFromInt(os2.sCapHeight)) * px_per_unit, + @as(f64, @floatFromInt(os2.sxHeight)) * px_per_unit, + }; + } + } + + break :heights .{ + cap: { + self.ft_mutex.lock(); + defer self.ft_mutex.unlock(); + if (face.getCharIndex('H')) |glyph_index| { + if (face.loadGlyph(glyph_index, self.glyphLoadFlags(false))) { + break :cap getGlyphSize(face.handle.*.glyph).height; + } else |_| {} + } + break :cap null; + }, + ex: { + self.ft_mutex.lock(); + defer self.ft_mutex.unlock(); + if (face.getCharIndex('x')) |glyph_index| { + if (face.loadGlyph(glyph_index, self.glyphLoadFlags(false))) { + break :ex getGlyphSize(face.handle.*.glyph).height; + } else |_| {} + } + break :ex null; + }, + }; + }; + + // Measure "水" (CJK water ideograph, U+6C34) for our ic width. + const ic_width: ?f64 = ic_width: { + self.ft_mutex.lock(); + defer self.ft_mutex.unlock(); + + const glyph = face.getCharIndex('水') orelse break :ic_width null; + + face.loadGlyph(glyph, self.glyphLoadFlags(false)) catch break :ic_width null; + + const ft_glyph = face.handle.*.glyph; + + // If the advance of the glyph is less than the width of the actual + // glyph then we just treat it as invalid since it's probably wrong + // and using it for size normalization will instead make the font + // way too big. + // + // This can sometimes happen if there's a CJK font that has been + // patched with the nerd fonts patcher and it butchers the advance + // values so the advance ends up half the width of the actual glyph. + const ft_glyph_width = getGlyphSize(ft_glyph).width; + const advance = f26dot6ToF64(ft_glyph.*.advance.x); + if (ft_glyph_width > advance) { + var buf: [1024]u8 = undefined; + const font_name = self.name(&buf) catch "<Error getting font name>"; + log.warn( + "(getMetrics) Width of glyph '水' for font \"{s}\" is greater than its advance ({d} > {d}), discarding ic_width metric.", + .{ font_name, ft_glyph_width, advance }, + ); + break :ic_width null; + } + + break :ic_width advance; + }; + + return .{ + .px_per_em = px_per_em, + + .cell_width = cell_width, + + .ascent = ascent, + .descent = descent, + .line_gap = line_gap, + + .underline_position = underline_position, + .underline_thickness = underline_thickness, + + .strikethrough_position = strikethrough_position, + .strikethrough_thickness = strikethrough_thickness, + + .cap_height = cap_height, + .ex_height = ex_height, + .ascii_height = ascii_height, + .ic_width = ic_width, + }; + } + + /// Copy the font table data for the given tag. + pub fn copyTable(self: Face, alloc: Allocator, tag: *const [4]u8) !?[]u8 { + return try self.face.loadSfntTable(alloc, freetype.Tag.init(tag)); + } +}; + +test { + const testFont = font.embedded.inconsolata; + const alloc = testing.allocator; + + var lib = try Library.init(alloc); + defer lib.deinit(); + + var atlas = try font.Atlas.init(alloc, 512, .grayscale); + defer atlas.deinit(alloc); + + var ft_font = try Face.init( + lib, + testFont, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ); + defer ft_font.deinit(); + + // Generate all visible ASCII + var i: u8 = 32; + while (i < 127) : (i += 1) { + _ = try ft_font.renderGlyph( + alloc, + &atlas, + ft_font.glyphIndex(i).?, + .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) }, + ); + } + + // Test resizing + { + const g1 = try ft_font.renderGlyph( + alloc, + &atlas, + ft_font.glyphIndex('A').?, + .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) }, + ); + try testing.expectEqual(@as(u32, 11), g1.height); + + try ft_font.setSize(.{ .size = .{ .points = 24, .xdpi = 96, .ydpi = 96 } }); + const g2 = try ft_font.renderGlyph( + alloc, + &atlas, + ft_font.glyphIndex('A').?, + .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) }, + ); + try testing.expectEqual(@as(u32, 21), g2.height); + } +} + +test "color emoji" { + const alloc = testing.allocator; + const testFont = font.embedded.emoji; + + var lib = try Library.init(alloc); + defer lib.deinit(); + + var atlas = try font.Atlas.init(alloc, 512, .bgra); + defer atlas.deinit(alloc); + + var ft_font = try Face.init( + lib, + testFont, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ); + defer ft_font.deinit(); + + _ = try ft_font.renderGlyph( + alloc, + &atlas, + ft_font.glyphIndex('🥸').?, + .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) }, + ); + + // Make sure this glyph has color + { + try testing.expect(ft_font.hasColor()); + const glyph_id = ft_font.glyphIndex('🥸').?; + try testing.expect(ft_font.isColorGlyph(glyph_id)); + } +} + +test "mono to bgra" { + const alloc = testing.allocator; + const testFont = font.embedded.emoji; + + var lib = try Library.init(alloc); + defer lib.deinit(); + + var atlas = try font.Atlas.init(alloc, 512, .bgra); + defer atlas.deinit(alloc); + + var ft_font = try Face.init(lib, testFont, .{ .size = .{ .points = 12, .xdpi = 72, .ydpi = 72 } }); + defer ft_font.deinit(); + + // glyph 3 is mono in Noto + _ = try ft_font.renderGlyph( + alloc, + &atlas, + 3, + .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) }, + ); +} + +test "svg font table" { + const alloc = testing.allocator; + const testFont = font.embedded.julia_mono; + + var lib = try font.Library.init(alloc); + defer lib.deinit(); + + var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12, .xdpi = 72, .ydpi = 72 } }); + defer face.deinit(); + + const table = (try face.copyTable(alloc, "SVG ")).?; + defer alloc.free(table); + + try testing.expectEqual(430, table.len); +} + +const terminus_i = + \\........ + \\........ + \\...#.... + \\...#.... + \\........ + \\..##.... + \\...#.... + \\...#.... + \\...#.... + \\...#.... + \\...#.... + \\..###... + \\........ + \\........ + \\........ + \\........ +; +// Including the newline +const terminus_i_pitch = 9; + +test "bitmap glyph" { + const alloc = testing.allocator; + const testFont = font.embedded.terminus_ttf; + + var lib = try Library.init(alloc); + defer lib.deinit(); + + var atlas = try font.Atlas.init(alloc, 512, .grayscale); + defer atlas.deinit(alloc); + + // Any glyph at 12pt @ 96 DPI is a bitmap + var ft_font = try Face.init(lib, testFont, .{ .size = .{ + .points = 12, + .xdpi = 96, + .ydpi = 96, + } }); + defer ft_font.deinit(); + + // glyph 77 = 'i' + const glyph = try ft_font.renderGlyph( + alloc, + &atlas, + 77, + .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) }, + ); + + // should render crisp + try testing.expectEqual(8, glyph.width); + try testing.expectEqual(16, glyph.height); + for (0..glyph.height) |y| { + for (0..glyph.width) |x| { + const pixel = terminus_i[y * terminus_i_pitch + x]; + try testing.expectEqual( + @as(u8, if (pixel == '#') 255 else 0), + atlas.data[(glyph.atlas_y + y) * atlas.size + (glyph.atlas_x + x)], + ); + } + } +} + +// Expected pixel pattern for Spleen 8x16 'A' (glyph index from char 'A') +// Derived from BDF BITMAP data: 00,00,7C,C6,C6,C6,FE,C6,C6,C6,C6,C6,00,00,00,00 +const spleen_A = + \\........ + \\........ + \\.#####.. + \\##...##. + \\##...##. + \\##...##. + \\#######. + \\##...##. + \\##...##. + \\##...##. + \\##...##. + \\##...##. + \\........ + \\........ + \\........ + \\........ +; +// Including the newline +const spleen_A_pitch = 9; +// Test parameters for bitmap font tests +const spleen_test_point_size = 12; +const spleen_test_dpi = 96; + +test "bitmap glyph BDF" { + const alloc = testing.allocator; + const testFont = font.embedded.spleen_bdf; + + var lib = try Library.init(alloc); + defer lib.deinit(); + + var atlas = try font.Atlas.init(alloc, 512, .grayscale); + defer atlas.deinit(alloc); + + // Spleen 8x16 is a pure bitmap font at 16px height + var ft_font = try Face.init(lib, testFont, .{ .size = .{ + .points = spleen_test_point_size, + .xdpi = spleen_test_dpi, + .ydpi = spleen_test_dpi, + } }); + defer ft_font.deinit(); + + // Get glyph index for 'A' + const glyph_index = ft_font.glyphIndex('A') orelse return error.GlyphNotFound; + + const glyph = try ft_font.renderGlyph( + alloc, + &atlas, + glyph_index, + .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) }, + ); + + // Verify dimensions match Spleen 8x16 + try testing.expectEqual(8, glyph.width); + try testing.expectEqual(16, glyph.height); + + // Verify pixel-perfect rendering + for (0..glyph.height) |y| { + for (0..glyph.width) |x| { + const pixel = spleen_A[y * spleen_A_pitch + x]; + try testing.expectEqual( + @as(u8, if (pixel == '#') 255 else 0), + atlas.data[(glyph.atlas_y + y) * atlas.size + (glyph.atlas_x + x)], + ); + } + } +} + +test "bitmap glyph PCF" { + const alloc = testing.allocator; + const testFont = font.embedded.spleen_pcf; + + var lib = try Library.init(alloc); + defer lib.deinit(); + + var atlas = try font.Atlas.init(alloc, 512, .grayscale); + defer atlas.deinit(alloc); + + var ft_font = try Face.init(lib, testFont, .{ .size = .{ + .points = spleen_test_point_size, + .xdpi = spleen_test_dpi, + .ydpi = spleen_test_dpi, + } }); + defer ft_font.deinit(); + + const glyph_index = ft_font.glyphIndex('A') orelse return error.GlyphNotFound; + + const glyph = try ft_font.renderGlyph( + alloc, + &atlas, + glyph_index, + .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) }, + ); + + try testing.expectEqual(8, glyph.width); + try testing.expectEqual(16, glyph.height); + + for (0..glyph.height) |y| { + for (0..glyph.width) |x| { + const pixel = spleen_A[y * spleen_A_pitch + x]; + try testing.expectEqual( + @as(u8, if (pixel == '#') 255 else 0), + atlas.data[(glyph.atlas_y + y) * atlas.size + (glyph.atlas_x + x)], + ); + } + } +} + +test "bitmap glyph OTB" { + const alloc = testing.allocator; + const testFont = font.embedded.spleen_otb; + + var lib = try Library.init(alloc); + defer lib.deinit(); + + var atlas = try font.Atlas.init(alloc, 512, .grayscale); + defer atlas.deinit(alloc); + + var ft_font = try Face.init(lib, testFont, .{ .size = .{ + .points = spleen_test_point_size, + .xdpi = spleen_test_dpi, + .ydpi = spleen_test_dpi, + } }); + defer ft_font.deinit(); + + const glyph_index = ft_font.glyphIndex('A') orelse return error.GlyphNotFound; + + const glyph = try ft_font.renderGlyph( + alloc, + &atlas, + glyph_index, + .{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) }, + ); + + try testing.expectEqual(8, glyph.width); + try testing.expectEqual(16, glyph.height); + + for (0..glyph.height) |y| { + for (0..glyph.width) |x| { + const pixel = spleen_A[y * spleen_A_pitch + x]; + try testing.expectEqual( + @as(u8, if (pixel == '#') 255 else 0), + atlas.data[(glyph.atlas_y + y) * atlas.size + (glyph.atlas_x + x)], + ); + } + } +} diff --git a/vendor/libghostty-vt/src/font/face/web_canvas.zig b/vendor/libghostty-vt/src/font/face/web_canvas.zig new file mode 100644 index 00000000..b4f9f5d5 --- /dev/null +++ b/vendor/libghostty-vt/src/font/face/web_canvas.zig @@ -0,0 +1,561 @@ +const std = @import("std"); +const assert = @import("../../quirks.zig").inlineAssert; +const testing = std.testing; +const Allocator = std.mem.Allocator; +const js = @import("zig-js"); +const font = @import("../main.zig"); + +const log = std.log.scoped(.font_face); + +pub const Face = struct { + /// See graphemes field for more details. + const grapheme_start: u32 = 0x10FFFF + 1; + const grapheme_end: u32 = std.math.maxInt(u32); + + /// 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. + presentation: font.Presentation, + + /// Metrics for this font face. These are useful for renderers. + metrics: font.Metrics, + + /// The canvas element that we will reuse to render glyphs + canvas: js.Object, + + /// The map to store multi-codepoint grapheme clusters that are rendered. + /// We use 1 above the maximum unicode codepoint up to the max 32-bit + /// unsigned integer to store the "glyph index" for graphemes. + grapheme_to_glyph: std.StringHashMapUnmanaged(u32) = .{}, + glyph_to_grapheme: std.AutoHashMapUnmanaged(u32, []u8) = .{}, + grapheme_next: u32 = grapheme_start, + + /// 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. + /// + /// 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); + 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, + .presentation = presentation, + + .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.grapheme_to_glyph.deinit(self.alloc); + { + var it = self.glyph_to_grapheme.valueIterator(); + while (it.next()) |value| self.alloc.free(value.*); + self.glyph_to_grapheme.deinit(self.alloc); + } + 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 { + // If this is a multi-codepoint grapheme then we only check if + // we actually know about it. + if (cp >= grapheme_start) { + if (!self.glyph_to_grapheme.contains(cp)) return null; + } + + // Render the glyph to determine if it is colored or not. We + // have to do this because the browser will always try to render + // whatever we give it and we have no API to determine color. + // + // We don't want to say yes to the wrong presentation because + // it will go into the wrong Atlas. + const p: font.Presentation = if (cp <= 255) .text else p: { + break :p self.glyphPresentation(cp) catch { + // In this case, we assume we are unable to render + // this glyph and therefore just say we don't support it. + return null; + }; + }; + if (p != self.presentation) return null; + + return cp; + } + + /// This determines the presentation of the glyph by literally + /// inspecting the image data to look for any color. This isn't + /// super performant but we don't have a better choice given the + /// canvas APIs. + fn glyphPresentation( + self: Face, + cp: u32, + ) !font.Presentation { + // Render the glyph + var render = try self.renderGlyphInternal(self.alloc, cp); + defer render.deinit(); + + // Inspect the image data for any non-zeros in the RGB value. + // NOTE(perf): this is an easy candidate for SIMD. + var i: usize = 0; + while (i < render.bitmap.len) : (i += 4) { + if (render.bitmap[i] > 0 or + render.bitmap[i + 1] > 0 or + render.bitmap[i + 2] > 0) return .emoji; + } + + return .text; + } + + /// Returns the glyph index for the given grapheme cluster. The same + /// cluster will always map to the same glyph index. This does not render + /// the grapheme at this time, only reserves the index. + pub fn graphemeGlyphIndex(self: *Face, cluster: []const u8) error{OutOfMemory}!u32 { + // If we already have this stored then return it + const gop = try self.grapheme_to_glyph.getOrPut(self.alloc, cluster); + if (gop.found_existing) return gop.value_ptr.*; + errdefer _ = self.grapheme_to_glyph.remove(cluster); + + // We don't have it stored. Ensure we have space to store. The + // next will be "0" if we're out of space due to unsigned int wrapping. + if (self.grapheme_next == 0) return error.OutOfMemory; + + // Copy the cluster for our reverse mapping + const copy = try self.alloc.dupe(u8, cluster); + errdefer self.alloc.free(copy); + + // Grow space for the reverse mapping + try self.glyph_to_grapheme.ensureUnusedCapacity(self.alloc, 1); + + // Store it + gop.value_ptr.* = self.grapheme_next; + self.glyph_to_grapheme.putAssumeCapacity(self.grapheme_next, copy); + + self.grapheme_next +%= 1; + return gop.value_ptr.*; + } + + /// 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, + opts: font.face.RenderOptions, + ) !font.Glyph { + _ = opts; + + var render = try self.renderGlyphInternal(alloc, glyph_index); + defer render.deinit(); + + // Convert the format of the bitmap if necessary + const bitmap_formatted: []u8 = switch (atlas.format) { + // Bitmap is already in RGBA + .rgba => render.bitmap, + + // Convert down to A8 + .grayscale => a8: { + assert(@mod(render.bitmap.len, 4) == 0); + var bitmap_a8 = try alloc.alloc(u8, render.bitmap.len / 4); + errdefer alloc.free(bitmap_a8); + var i: usize = 0; + while (i < bitmap_a8.len) : (i += 1) { + bitmap_a8[i] = render.bitmap[(i * 4) + 3]; + } + + break :a8 bitmap_a8; + }, + + else => return error.UnsupportedAtlasFormat, + }; + defer if (bitmap_formatted.ptr != render.bitmap.ptr) { + alloc.free(bitmap_formatted); + }; + + // Put it in our atlas + const region = try atlas.reserve(alloc, render.width, render.height); + if (region.width > 0 and region.height > 0) { + atlas.set(region, bitmap_formatted); + } + + return font.Glyph{ + .width = render.width, + .height = render.height, + // TODO: this can't be right + .offset_x = 0, + .offset_y = 0, + .atlas_x = region.x, + .atlas_y = region.y, + }; + } + + /// 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; + + const result = font.Metrics{ + .cell_width = @intFromFloat(cell_width), + .cell_height = @intFromFloat(cell_height), + .cell_baseline = @intFromFloat(cell_baseline), + .underline_position = @intFromFloat(underline_position), + .underline_thickness = @intFromFloat(underline_thickness), + .strikethrough_position = @intFromFloat(underline_position), + .strikethrough_thickness = @intFromFloat(underline_thickness), + }; + + self.metrics = result; + 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 + const 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; + } + + /// An internal (web-canvas-only) format for rendered glyphs + /// since we do render passes in multiple different situations. + const RenderedGlyph = struct { + alloc: Allocator, + metrics: js.Object, + width: u32, + height: u32, + bitmap: []u8, + + pub fn deinit(self: *RenderedGlyph) void { + self.metrics.deinit(); + self.alloc.free(self.bitmap); + self.* = undefined; + } + }; + + /// Shared logic for rendering a glyph. + fn renderGlyphInternal( + self: Face, + alloc: Allocator, + glyph_index: u32, + ) !RenderedGlyph { + // Encode our glyph to UTF-8 so we can build a JS string out of it. + var utf8: [4]u8 = undefined; + const glyph_str = glyph_str: { + // If we are a normal glyph then we are a single codepoint and + // we just UTF8 encode it as-is. + if (glyph_index < grapheme_start) { + const utf8_len = try std.unicode.utf8Encode(@intCast(glyph_index), &utf8); + break :glyph_str js.string(utf8[0..utf8_len]); + } + + // We are a multi-codepoint glyph so we have to read the glyph + // from the map and it is already utf8 encoded. + const slice = self.glyph_to_grapheme.get(glyph_index) orelse + return error.UnknownGraphemeCluster; + break :glyph_str js.string(slice); + }; + + // 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}); + errdefer metrics.deinit(); + const width: u32 = @as(u32, @intFromFloat(@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; + + const left = try metrics.get(f32, "actualBoundingBoxLeft"); + const asc = try metrics.get(f32, "actualBoundingBoxAscent"); + const desc = try metrics.get(f32, "actualBoundingBoxDescent"); + + // 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) @as(u32, @intFromFloat(@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 + // 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(); + + // 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", .{ + 0, + 0, + width, + height, + }); + + // Draw glyph + try ctx.set("fillStyle", js.string("black")); + try ctx.call(void, "fillText", .{ + glyph_str, + left + 1, + if (!broken_bbox) asc + 1 else @as(f32, @floatFromInt(height)), + }); + + // 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"); + const bitmap = try alloc.alloc(u8, @intCast(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; + }; + errdefer alloc.free(bitmap); + + return RenderedGlyph{ + .alloc = alloc, + .metrics = metrics, + .width = width, + .height = height, + .bitmap = bitmap, + }; + } +}; + +/// 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, p: u16) ?*Face { + return face_new_(ptr, len, pts, p) catch null; + } + + fn face_new_(ptr: [*]const u8, len: usize, pts: f32, presentation: u16) !*Face { + var face = try Face.initNamed( + alloc, + ptr[0..len], + .{ .points = pts }, + @enumFromInt(presentation), + ); + errdefer face.deinit(); + + const 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, .{}); + + const result = try alloc.create(font.Glyph); + errdefer alloc.destroy(result); + _ = try wasm.toHostOwned(result); + result.* = glyph; + return result; + } +}; |
