aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/font/face
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/font/face
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (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.zig1162
-rw-r--r--vendor/libghostty-vt/src/font/face/freetype.zig1439
-rw-r--r--vendor/libghostty-vt/src/font/face/web_canvas.zig561
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;
+ }
+};