Merge pull request #345 from mitchellh/variable-fonts

Variable font support
This commit is contained in:
Mitchell Hashimoto
2023-08-28 07:35:19 -07:00
committed by GitHub
15 changed files with 500 additions and 9 deletions

View File

@ -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,

View File

@ -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

View File

@ -1,5 +1,6 @@
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_TRUETYPE_TABLES_H
#include <freetype/ftmm.h>
#include <freetype/ftsnames.h>
#include <freetype/ttnameid.h>

View File

@ -24,7 +24,7 @@ pub const Array = opaque {
/// constness so that further API calls work correctly. The Foundation
/// API doesn't properly mark things const/non-const.
pub fn getValueAtIndex(self: *Array, comptime T: type, idx: usize) *T {
return @ptrCast(CFArrayGetValueAtIndex(self, idx));
return @ptrCast(@alignCast(CFArrayGetValueAtIndex(self, idx)));
}
pub extern "c" fn CFArrayCreate(

View File

@ -39,6 +39,14 @@ pub const Font = opaque {
c.CFRelease(self);
}
pub fn retain(self: *Font) void {
_ = c.CFRetain(self);
}
pub fn copyDescriptor(self: *Font) *text.FontDescriptor {
return @ptrCast(@constCast(c.CTFontCopyFontDescriptor(@ptrCast(self))));
}
pub fn getGlyphsForCharacters(self: *Font, chars: []const u16, glyphs: []graphics.Glyph) bool {
assert(chars.len == glyphs.len);
return c.CTFontGetGlyphsForCharacters(

View File

@ -31,6 +31,21 @@ pub const FontDescriptor = opaque {
) orelse Allocator.Error.OutOfMemory;
}
pub fn createCopyWithVariation(
original: *FontDescriptor,
id: *foundation.Number,
value: f64,
) Allocator.Error!*FontDescriptor {
return @as(
?*FontDescriptor,
@ptrCast(@constCast(c.CTFontDescriptorCreateCopyWithVariation(
@ptrCast(original),
@ptrCast(id),
value,
))),
) orelse Allocator.Error.OutOfMemory;
}
pub fn release(self: *FontDescriptor) void {
c.CFRelease(self);
}
@ -75,6 +90,9 @@ pub const FontAttribute = enum {
downloadable,
downloaded,
// https://developer.apple.com/documentation/coretext/core_text_constants?language=objc
variation_axes,
pub fn key(self: FontAttribute) *foundation.String {
return @as(*foundation.String, @ptrFromInt(@intFromPtr(switch (self) {
.url => c.kCTFontURLAttribute,
@ -101,6 +119,7 @@ pub const FontAttribute = enum {
.enabled => c.kCTFontEnabledAttribute,
.downloadable => c.kCTFontDownloadableAttribute,
.downloaded => c.kCTFontDownloadedAttribute,
.variation_axes => c.kCTFontVariationAxesAttribute,
})));
}
@ -130,6 +149,7 @@ pub const FontAttribute = enum {
.enabled => *foundation.Number,
.downloadable => *anyopaque, // CFBoolean
.downloaded => *anyopaque, // CFBoolean
.variation_axes => ?*foundation.Array,
};
}
};
@ -159,6 +179,38 @@ pub const FontTraitKey = enum {
}
};
// https://developer.apple.com/documentation/coretext/ctfont/font_variation_axis_dictionary_keys?language=objc
pub const FontVariationAxisKey = enum {
identifier,
minimum_value,
maximum_value,
default_value,
name,
hidden,
pub fn key(self: FontVariationAxisKey) *foundation.String {
return @as(*foundation.String, @ptrFromInt(@intFromPtr(switch (self) {
.identifier => c.kCTFontVariationAxisIdentifierKey,
.minimum_value => c.kCTFontVariationAxisMinimumValueKey,
.maximum_value => c.kCTFontVariationAxisMaximumValueKey,
.default_value => c.kCTFontVariationAxisDefaultValueKey,
.name => c.kCTFontVariationAxisNameKey,
.hidden => c.kCTFontVariationAxisHiddenKey,
})));
}
pub fn Value(comptime self: FontVariationAxisKey) type {
return switch (self) {
.identifier => foundation.Number,
.minimum_value => foundation.Number,
.maximum_value => foundation.Number,
.default_value => foundation.Number,
.name => foundation.String,
.hidden => foundation.Number,
};
}
};
pub const FontSymbolicTraits = packed struct(u32) {
italic: bool = false,
bold: bool = false,

View File

@ -229,6 +229,7 @@ pub fn init(
var disco_it = try disco.discover(.{
.family = family,
.size = font_size.points,
.variations = config.@"font-variation".list.items,
});
defer disco_it.deinit();
if (try disco_it.next()) |face| {
@ -241,6 +242,7 @@ pub fn init(
.family = family,
.size = font_size.points,
.bold = true,
.variations = config.@"font-variation-bold".list.items,
});
defer disco_it.deinit();
if (try disco_it.next()) |face| {
@ -253,6 +255,7 @@ pub fn init(
.family = family,
.size = font_size.points,
.italic = true,
.variations = config.@"font-variation-italic".list.items,
});
defer disco_it.deinit();
if (try disco_it.next()) |face| {
@ -266,6 +269,7 @@ pub fn init(
.size = font_size.points,
.bold = true,
.italic = true,
.variations = config.@"font-variation-bold-italic".list.items,
});
defer disco_it.deinit();
if (try disco_it.next()) |face| {

View File

@ -3,6 +3,7 @@ const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const fontpkg = @import("font/main.zig");
const inputpkg = @import("input.zig");
const terminal = @import("terminal/main.zig");
const internal_os = @import("os/main.zig");
@ -43,6 +44,30 @@ pub const Config = struct {
else => 12,
},
/// A repeatable configuration to set one or more font variations values
/// for a variable font. A variable font is a single font, usually
/// with a filename ending in "-VF.ttf" or "-VF.otf" that contains
/// one or more configurable axes for things such as weight, slant,
/// etc. Not all fonts support variations; only fonts that explicitly
/// state they are variable fonts will work.
///
/// The format of this is "id=value" where "id" is the axis identifier.
/// An axis identifier is always a 4 character string, such as "wght".
/// To get the list of supported axes, look at your font documentation
/// or use a font inspection tool.
///
/// Invalid ids and values are usually ignored. For example, if a font
/// only supports weights from 100 to 700, setting "wght=800" will
/// do nothing (it will not be clamped to 700). You must consult your
/// font's documentation to see what values are supported.
///
/// Common axes are: "wght" (weight), "slnt" (slant), "ital" (italic),
/// "opsz" (optical size), "wdth" (width), "GRAD" (gradient), etc.
@"font-variation": RepeatableFontVariation = .{},
@"font-variation-bold": RepeatableFontVariation = .{},
@"font-variation-italic": RepeatableFontVariation = .{},
@"font-variation-bold-italic": RepeatableFontVariation = .{},
/// Draw fonts with a thicker stroke, if supported. This is only supported
/// currently on macOS.
@"font-thicken": bool = false,
@ -1217,6 +1242,97 @@ pub const RepeatableString = struct {
}
};
/// FontVariation is a repeatable configuration value that sets a single
/// font variation value. Font variations are configurations for what
/// are often called "variable fonts." The font files usually end in
/// "-VF.ttf."
///
/// The value for this is in the format of `id=value` where `id` is the
/// 4-character font variation axis identifier and `value` is the
/// floating point value for that axis. For more details on font variations
/// see the MDN font-variation-settings documentation since this copies that
/// behavior almost exactly:
///
/// https://developer.mozilla.org/en-US/docs/Web/CSS/font-variation-settings
pub const RepeatableFontVariation = struct {
const Self = @This();
// Allocator for the list is the arena for the parent config.
list: std.ArrayListUnmanaged(fontpkg.face.Variation) = .{},
pub fn parseCLI(self: *Self, alloc: Allocator, input_: ?[]const u8) !void {
const input = input_ orelse return error.ValueRequired;
const eql_idx = std.mem.indexOf(u8, input, "=") orelse return error.InvalidFormat;
const whitespace = " \t";
const key = std.mem.trim(u8, input[0..eql_idx], whitespace);
const value = std.mem.trim(u8, input[eql_idx + 1 ..], whitespace);
if (key.len != 4) return error.InvalidFormat;
try self.list.append(alloc, .{
.id = fontpkg.face.Variation.Id.init(@ptrCast(key.ptr)),
.value = std.fmt.parseFloat(f64, value) catch return error.InvalidFormat,
});
}
/// Deep copy of the struct. Required by Config.
pub fn clone(self: *const Self, alloc: Allocator) !Self {
return .{
.list = try self.list.clone(alloc),
};
}
/// Compare if two of our value are requal. Required by Config.
pub fn equal(self: Self, other: Self) bool {
const itemsA = self.list.items;
const itemsB = other.list.items;
if (itemsA.len != itemsB.len) return false;
for (itemsA, itemsB) |a, b| {
if (!std.meta.eql(a, b)) return false;
} else return true;
}
test "parseCLI" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var list: Self = .{};
try list.parseCLI(alloc, "wght=200");
try list.parseCLI(alloc, "slnt=-15");
try testing.expectEqual(@as(usize, 2), list.list.items.len);
try testing.expectEqual(fontpkg.face.Variation{
.id = fontpkg.face.Variation.Id.init("wght"),
.value = 200,
}, list.list.items[0]);
try testing.expectEqual(fontpkg.face.Variation{
.id = fontpkg.face.Variation.Id.init("slnt"),
.value = -15,
}, list.list.items[1]);
}
test "parseCLI with whitespace" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var list: Self = .{};
try list.parseCLI(alloc, "wght =200");
try list.parseCLI(alloc, "slnt= -15");
try testing.expectEqual(@as(usize, 2), list.list.items.len);
try testing.expectEqual(fontpkg.face.Variation{
.id = fontpkg.face.Variation.Id.init("wght"),
.value = 200,
}, list.list.items[0]);
try testing.expectEqual(fontpkg.face.Variation{
.id = fontpkg.face.Variation.Id.init("slnt"),
.value = -15,
}, list.list.items[1]);
}
};
/// Stores a set of keybinds.
pub const Keybinds = struct {
set: inputpkg.Binding.Set = .{},

View File

@ -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(

View File

@ -5,6 +5,7 @@ const fontconfig = @import("fontconfig");
const macos = @import("macos");
const options = @import("main.zig").options;
const DeferredFace = @import("main.zig").DeferredFace;
const Variation = @import("main.zig").face.Variation;
const log = std.log.scoped(.discovery);
@ -43,6 +44,11 @@ pub const Descriptor = struct {
bold: bool = false,
italic: bool = false,
/// Variation axes to apply to the font. This also impacts searching
/// for fonts since fonts with the ability to set these variations
/// will be preferred, but not guaranteed.
variations: []const Variation = &.{},
/// Convert to Fontconfig pattern to use for lookup. The pattern does
/// not have defaults filled/substituted (Fontconfig thing) so callers
/// must still do this.
@ -149,7 +155,21 @@ pub const Descriptor = struct {
);
}
return try macos.text.FontDescriptor.createWithAttributes(@ptrCast(attrs));
// Build our descriptor from attrs
var desc = try macos.text.FontDescriptor.createWithAttributes(@ptrCast(attrs));
errdefer desc.release();
// Variations are built by copying the descriptor. I don't know a way
// to set it on attrs directly.
for (self.variations) |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;
}
return desc;
}
};
@ -185,6 +205,7 @@ pub const Fontconfig = struct {
.pattern = pat,
.set = res.fs,
.fonts = res.fs.fonts(),
.variations = desc.variations,
.i = 0,
};
}
@ -194,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 {
@ -221,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,
},
};
}

View File

@ -1,3 +1,4 @@
const std = @import("std");
const builtin = @import("builtin");
const options = @import("main.zig").options;
const freetype = @import("face/freetype.zig");
@ -36,6 +37,31 @@ pub const DesiredSize = struct {
}
};
/// A font variation setting. The best documentation for this I know of
/// is actually the CSS font-variation-settings property on MDN:
/// https://developer.mozilla.org/en-US/docs/Web/CSS/font-variation-settings
pub const Variation = struct {
id: Id,
value: f64,
pub const Id = packed struct(u32) {
d: u8,
c: u8,
b: u8,
a: u8,
pub fn init(v: *const [4]u8) Id {
return .{ .a = v[0], .b = v[1], .c = v[2], .d = v[3] };
}
/// Converts the ID to a string. The return value is only valid
/// for the lifetime of the self pointer.
pub fn str(self: Id) [4]u8 {
return .{ self.a, self.b, self.c, self.d };
}
};
};
/// Metrics associated with the font that are useful for renderers to know.
pub const Metrics = struct {
/// Recommended cell width and height for a monospace grid using this font.
@ -77,3 +103,17 @@ pub const Foo = if (options.backend == .coretext) coretext.Face else void;
test {
@import("std").testing.refAllDecls(@This());
}
test "Variation.Id: wght should be 2003265652" {
const testing = std.testing;
const id = Variation.Id.init("wght");
try testing.expectEqual(@as(u32, 2003265652), @as(u32, @bitCast(id)));
try testing.expectEqualStrings("wght", &(id.str()));
}
test "Variation.Id: slnt should be 1936486004" {
const testing = std.testing;
const id: Variation.Id = .{ .a = 's', .b = 'l', .c = 'n', .d = 't' };
try testing.expectEqual(@as(u32, 1936486004), @as(u32, @bitCast(id)));
try testing.expectEqualStrings("slnt", &(id.str()));
}

View File

@ -1,4 +1,5 @@
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const macos = @import("macos");
@ -64,6 +65,12 @@ pub const Face = struct {
const ct_font = try base.copyWithAttributes(@floatFromInt(size.pixels()), null, null);
errdefer ct_font.release();
return try initFont(ct_font);
}
/// 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) !Face {
var hb_font = try harfbuzz.coretext.createFont(ct_font);
errdefer hb_font.destroy();
@ -76,6 +83,52 @@ pub const Face = struct {
.metrics = try calcMetrics(ct_font),
};
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;
}
@ -89,8 +142,8 @@ pub const Face = struct {
/// matrix applied to italicize it.
pub fn italicize(self: *const Face) !Face {
const ct_font = try self.font.copyWithAttributes(0.0, &italic_skew, null);
defer ct_font.release();
return try initFontCopy(ct_font, .{ .points = 0 });
errdefer ct_font.release();
return try initFont(ct_font);
}
/// Returns the font name. If allocation is required, buf will be used,
@ -115,6 +168,31 @@ pub const Face = struct {
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,
) !void {
// 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;
}
// 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);
self.deinit();
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 {
@ -492,3 +570,55 @@ test "in-memory" {
_ = try face.renderGlyph(alloc, &atlas, face.glyphIndex(i).?, .{});
}
}
test "variable" {
const testing = std.testing;
const alloc = testing.allocator;
const testFont = @import("../test.zig").fontVariable;
var atlas = try font.Atlas.init(alloc, 512, .greyscale);
defer atlas.deinit(alloc);
var lib = try font.Library.init();
defer lib.deinit();
var face = try Face.init(lib, testFont, .{ .points = 12 });
defer face.deinit();
try testing.expectEqual(font.Presentation.text, face.presentation);
// 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).?, .{});
}
}
test "variable set variation" {
const testing = std.testing;
const alloc = testing.allocator;
const testFont = @import("../test.zig").fontVariable;
var atlas = try font.Atlas.init(alloc, 512, .greyscale);
defer atlas.deinit(alloc);
var lib = try font.Library.init();
defer lib.deinit();
var face = try Face.init(lib, testFont, .{ .points = 12 });
defer face.deinit();
try testing.expectEqual(font.Presentation.text, face.presentation);
try face.setVariations(&.{
.{ .id = font.face.Variation.Id.init("wght"), .value = 400 },
});
// 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).?, .{});
}
}

View File

@ -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 {

BIN
src/font/res/Lilex-VF.ttf Normal file

Binary file not shown.

View File

@ -2,3 +2,4 @@ pub const fontRegular = @embedFile("res/Inconsolata-Regular.ttf");
pub const fontBold = @embedFile("res/Inconsolata-Bold.ttf");
pub const fontEmoji = @embedFile("res/NotoColorEmoji.ttf");
pub const fontEmojiText = @embedFile("res/NotoEmoji-Regular.ttf");
pub const fontVariable = @embedFile("res/Lilex-VF.ttf");