font/coretext: ability to set variation axes

This commit is contained in:
Mitchell Hashimoto
2023-08-27 07:56:17 -07:00
parent fb0f80f9ca
commit 9d0729f17c
7 changed files with 186 additions and 3 deletions

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);
}

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;
}
};

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

@ -64,11 +64,33 @@ 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();
const traits = ct_font.getSymbolicTraits();
// Get variation axes
// if (ct_font.copyAttribute(.variation_axes)) |axes| {
// defer axes.release();
// const len = axes.getCount();
// for (0..len) |i| {
// const dict = axes.getValueAtIndex(macos.foundation.Dictionary, i);
// const Key = macos.text.FontVariationAxisKey;
// const name_ = dict.getValue(Key.name.Value(), Key.name.key());
// if (name_) |name_val| {
// var buf: [1024]u8 = undefined;
// const namestr = name_val.cstring(&buf, .utf8) orelse "";
// log.warn("AXES: {s}", .{namestr});
// }
// }
// }
var result: Face = .{
.font = ct_font,
.hb_font = hb_font,
@ -89,8 +111,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 +137,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 +539,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).?, .{});
}
}

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");