From 1ee5b7f91c7e8d715110fe928a74e2ad60b19fce Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 27 Aug 2023 19:58:25 -0700 Subject: [PATCH] font: freetype supports font variation settings --- pkg/freetype/Library.zig | 5 +++ pkg/freetype/face.zig | 33 +++++++++++++++ pkg/freetype/freetype-zig.h | 1 + src/font/DeferredFace.zig | 8 +++- src/font/discovery.zig | 3 ++ src/font/face/freetype.zig | 80 +++++++++++++++++++++++++++++++++++-- 6 files changed, 125 insertions(+), 5 deletions(-) diff --git a/pkg/freetype/Library.zig b/pkg/freetype/Library.zig index ab3be153a..f5dc606ef 100644 --- a/pkg/freetype/Library.zig +++ b/pkg/freetype/Library.zig @@ -57,6 +57,11 @@ pub fn initMemoryFace(self: Library, data: []const u8, index: i32) Error!Face { return face; } +/// Call when you're done with a loaded MM var. +pub fn doneMMVar(self: Library, mm: *c.FT_MM_Var) void { + _ = c.FT_Done_MM_Var(self.handle, mm); +} + pub const Version = struct { major: i32, minor: i32, diff --git a/pkg/freetype/face.zig b/pkg/freetype/face.zig index fd7514d8f..2bd4a33c7 100644 --- a/pkg/freetype/face.zig +++ b/pkg/freetype/face.zig @@ -24,6 +24,12 @@ pub const Face = struct { return c.FT_HAS_COLOR(self.handle); } + /// A macro that returns true whenever a face object contains some + /// multiple masters. + pub fn hasMultipleMasters(self: Face) bool { + return c.FT_HAS_MULTIPLE_MASTERS(self.handle); + } + /// A macro that returns true whenever a face object contains a scalable /// font face (true for TrueType, Type 1, Type 42, CID, OpenType/CFF, /// and PFR font formats). @@ -95,6 +101,33 @@ pub const Face = struct { const res = c.FT_Get_Sfnt_Name(self.handle, @intCast(i), &name); return if (intToError(res)) |_| name else |err| err; } + + /// Retrieve the font variation descriptor for a font. + pub fn getMMVar(self: Face) Error!*c.FT_MM_Var { + var result: *c.FT_MM_Var = undefined; + const res = c.FT_Get_MM_Var(self.handle, @ptrCast(&result)); + return if (intToError(res)) |_| result else |err| err; + } + + /// Get the design coordinates of the currently selected interpolated font. + pub fn getVarDesignCoordinates(self: Face, coords: []c.FT_Fixed) Error!void { + const res = c.FT_Get_Var_Design_Coordinates( + self.handle, + @intCast(coords.len), + coords.ptr, + ); + return intToError(res); + } + + /// Choose an interpolated font design through design coordinates. + pub fn setVarDesignCoordinates(self: Face, coords: []c.FT_Fixed) Error!void { + const res = c.FT_Set_Var_Design_Coordinates( + self.handle, + @intCast(coords.len), + coords.ptr, + ); + return intToError(res); + } }; /// An enumeration to specify indices of SFNT tables loaded and parsed by diff --git a/pkg/freetype/freetype-zig.h b/pkg/freetype/freetype-zig.h index ddb242be8..e430aef52 100644 --- a/pkg/freetype/freetype-zig.h +++ b/pkg/freetype/freetype-zig.h @@ -1,5 +1,6 @@ #include #include FT_FREETYPE_H #include FT_TRUETYPE_TABLES_H +#include #include #include diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index 6d0d6fe31..acf5ba93f 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -54,6 +54,9 @@ pub const Fontconfig = struct { charset: *const fontconfig.CharSet, langset: *const fontconfig.LangSet, + /// Variations to apply to this font. + variations: []const font.face.Variation, + pub fn deinit(self: *Fontconfig) void { self.pattern.destroy(); self.* = undefined; @@ -154,7 +157,10 @@ fn loadFontconfig( const filename = (try fc.pattern.get(.file, 0)).string; const face_index = (try fc.pattern.get(.index, 0)).integer; - return try Face.initFile(lib, filename, face_index, size); + var face = try Face.initFile(lib, filename, face_index, size); + errdefer face.deinit(); + try face.setVariations(fc.variations); + return face; } fn loadCoreText( diff --git a/src/font/discovery.zig b/src/font/discovery.zig index fb3c8322e..d1dc6a601 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -205,6 +205,7 @@ pub const Fontconfig = struct { .pattern = pat, .set = res.fs, .fonts = res.fs.fonts(), + .variations = desc.variations, .i = 0, }; } @@ -214,6 +215,7 @@ pub const Fontconfig = struct { pattern: *fontconfig.Pattern, set: *fontconfig.FontSet, fonts: []*fontconfig.Pattern, + variations: []const Variation, i: usize, pub fn deinit(self: *DiscoverIterator) void { @@ -241,6 +243,7 @@ pub const Fontconfig = struct { .pattern = font_pattern, .charset = (try font_pattern.get(.charset, 0)).char_set, .langset = (try font_pattern.get(.lang, 0)).lang_set, + .variations = self.variations, }, }; } diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 7b104eb01..9ae861c7d 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -23,6 +23,9 @@ const quirks = @import("../../quirks.zig"); const log = std.log.scoped(.font_face); pub const Face = struct { + /// Our freetype library + lib: freetype.Library, + /// Our font face. face: freetype.Face, @@ -43,30 +46,55 @@ pub const Face = struct { pub fn initFile(lib: Library, path: [:0]const u8, index: i32, size: font.face.DesiredSize) !Face { const face = try lib.lib.initFace(path, index); errdefer face.deinit(); - return try initFace(face, size); + return try initFace(lib, face, size); } /// Initialize a new font face with the given source in-memory. pub fn init(lib: Library, source: [:0]const u8, size: font.face.DesiredSize) !Face { const face = try lib.lib.initMemoryFace(source, 0); errdefer face.deinit(); - return try initFace(face, size); + return try initFace(lib, face, size); } - fn initFace(face: freetype.Face, size: font.face.DesiredSize) !Face { + fn initFace(lib: Library, face: freetype.Face, size: font.face.DesiredSize) !Face { try face.selectCharmap(.unicode); try setSize_(face, size); - const hb_font = try harfbuzz.freetype.createFont(face.handle); + var hb_font = try harfbuzz.freetype.createFont(face.handle); errdefer hb_font.destroy(); var result: Face = .{ + .lib = lib.lib, .face = face, .hb_font = hb_font, .presentation = if (face.hasColor()) .emoji else .text, .metrics = calcMetrics(face), }; 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; } @@ -132,6 +160,50 @@ pub const Face = struct { 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, + ) !void { + // 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.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); + + // We need to recalculate font metrics which may have changed. + self.metrics = calcMetrics(self.face); + } + /// 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 {