Mac Font Discovery with CoreText (#17)

This implements font discovery so the `--font-family` flag works for macOS. Fonts are looked up using the Core Text API so any installed font on the Mac system can be used. 

We still use FreeType for rendering, and CoreText doesn't _quite_ give us all the information we need to build the exact face in FreeType. So a TODO after this is to now implement glyph _rendering_ using Core Text and Core Graphics. Until then, a couple fonts don't quite work (i.e. Monaco, a big one!) but many do!
This commit is contained in:
Mitchell Hashimoto
2022-10-01 22:21:30 -07:00
committed by GitHub
parent 791739de9c
commit 12c9482d48
5 changed files with 293 additions and 2 deletions

View File

@@ -9,6 +9,7 @@ const DeferredFace = @This();
const std = @import("std");
const assert = std.debug.assert;
const fontconfig = @import("fontconfig");
const macos = @import("macos");
const options = @import("main.zig").options;
const Library = @import("main.zig").Library;
const Face = @import("main.zig").Face;
@@ -20,6 +21,9 @@ face: ?Face = null,
/// Fontconfig
fc: if (options.fontconfig) ?Fontconfig else void = if (options.fontconfig) null else {},
/// CoreText
ct: if (options.coretext) ?CoreText else void = if (options.coretext) null else {},
/// Fontconfig specific data. This is only present if building with fontconfig.
pub const Fontconfig = struct {
/// The pattern for this font. This must be the "render prepared" pattern.
@@ -38,6 +42,17 @@ pub const Fontconfig = struct {
}
};
/// CoreText specific data. This is only present when building with CoreText.
pub const CoreText = struct {
/// The initialized font
font: *macos.text.Font,
pub fn deinit(self: *CoreText) void {
self.font.release();
self.* = undefined;
}
};
/// Initialize a deferred face that is already pre-loaded. The deferred face
/// takes ownership over the loaded face, deinit will deinit the loaded face.
pub fn initLoaded(face: Face) DeferredFace {
@@ -47,6 +62,7 @@ pub fn initLoaded(face: Face) DeferredFace {
pub fn deinit(self: *DeferredFace) void {
if (self.face) |*face| face.deinit();
if (options.fontconfig) if (self.fc) |*fc| fc.deinit();
if (options.coretext) if (self.ct) |*ct| ct.deinit();
self.* = undefined;
}
@@ -63,6 +79,13 @@ pub fn name(self: DeferredFace) ![:0]const u8 {
return (try fc.pattern.get(.fullname, 0)).string;
}
if (options.coretext) {
if (self.ct) |ct| {
const display_name = ct.font.copyDisplayName();
return display_name.cstringPtr(.utf8) orelse "<unsupported internal encoding>";
}
}
return "TODO: built-in font names";
}
@@ -80,6 +103,11 @@ pub fn load(
return;
}
if (options.coretext) {
try self.loadCoreText(lib, size);
return;
}
// Unreachable because we must be already loaded or have the
// proper configuration for one of the other deferred mechanisms.
unreachable;
@@ -100,6 +128,45 @@ fn loadFontconfig(
self.face = try Face.initFile(lib, filename, face_index, size);
}
fn loadCoreText(
self: *DeferredFace,
lib: Library,
size: Face.DesiredSize,
) !void {
assert(self.face == null);
const ct = self.ct.?;
// Get the URL for the font so we can get the filepath
const url = ct.font.copyAttribute(.url);
defer url.release();
// Get the path from the URL
const path = url.copyPath() orelse return error.FontHasNoFile;
defer path.release();
// URL decode the path
const blank = try macos.foundation.String.createWithBytes("", .utf8, false);
defer blank.release();
const decoded = try macos.foundation.URL.createStringByReplacingPercentEscapes(
path,
blank,
);
defer decoded.release();
// Decode into a c string. 1024 bytes should be enough for anybody.
var buf: [1024]u8 = undefined;
const path_slice = decoded.cstring(buf[0..1023], .utf8) orelse
return error.FontPathCantDecode;
// Freetype requires null-terminated. We always leave space at
// the end for a zero so we set that up here.
buf[path_slice.len] = 0;
// TODO: face index 0 is not correct long term and we should switch
// to using CoreText for rendering, too.
self.face = try Face.initFile(lib, buf[0..path_slice.len :0], 0, size);
}
/// Returns true if this face can satisfy the given codepoint and
/// presentation. If presentation is null, then it just checks if the
/// codepoint is present at all.
@@ -136,6 +203,20 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool {
}
}
// If we are using coretext, we check the loaded CT font.
if (options.coretext) {
if (self.ct) |ct| {
// 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 };
return ct.font.getGlyphsForCharacters(unichars[0..len], glyphs[0..len]);
}
}
// This is unreachable because discovery mechanisms terminate, and
// if we're not using a discovery mechanism, the face MUST be loaded.
unreachable;
@@ -186,3 +267,34 @@ test "fontconfig" {
try testing.expect(def.hasCodepoint(' ', null));
try testing.expect(def.face.?.glyphIndex(' ') != null);
}
test "coretext" {
if (!options.coretext) return error.SkipZigTest;
const discovery = @import("main.zig").discovery;
const testing = std.testing;
// Load freetype
var lib = try Library.init();
defer lib.deinit();
// Get a deferred face from fontconfig
var def = def: {
var fc = discovery.CoreText.init();
var it = try fc.discover(.{ .family = "Monaco", .size = 12 });
defer it.deinit();
break :def (try it.next()).?;
};
defer def.deinit();
try testing.expect(!def.loaded());
try testing.expect(def.hasCodepoint(' ', null));
// Verify we can get the name
const n = try def.name();
try testing.expect(n.len > 0);
// Load it and verify it works
try def.load(lib, .{ .points = 12 });
try testing.expect(def.hasCodepoint(' ', null));
try testing.expect(def.face.?.glyphIndex(' ') != null);
}