Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531//! A deferred face represents a single font face with all the information//! necessary to load it, but defers loading the full face until it is//! needed.//!//! This allows us to have many fallback fonts to look for glyphs, but//! only load them if they're really needed.const DeferredFace = @This();
const std = @import("std");const Allocator = std.mem.Allocator;const fontconfig = @import("fontconfig");const macos = @import("macos");const font = @import("main.zig");const options = @import("main.zig").options;const Library = @import("main.zig").Library;const Face = @import("main.zig").Face;const Presentation = @import("main.zig").Presentation;
const log = std.log.scoped(.deferred_face);
/// Fontconfigfc: if (options.backend == .fontconfig_freetype) ?Fontconfig else void = if (options.backend == .fontconfig_freetype) null else {},
/// CoreTextct: if (font.Discover == font.discovery.CoreText) ?CoreText else void = if (font.Discover == font.discovery.CoreText) null else {},
/// Windows (FreeType directory scan)win: if (options.backend == .freetype_windows) ?Windows else void = if (options.backend == .freetype_windows) null else {},
/// Canvaswc: if (options.backend == .web_canvas) ?WebCanvas else void = if (options.backend == .web_canvas) 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. /// (i.e. call FcFontRenderPrepare). pattern: *fontconfig.Pattern,
/// Charset and Langset are used for quick lookup if a codepoint and /// presentation style are supported. They can be derived from pattern /// but are cached since they're frequently used. 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; }};
/// Windows specific data. Only present with the freetype_windows backend.////// Unlike Fontconfig/CoreText which carry lightweight descriptor handles,/// the Windows backend has no external descriptor service — the "deferred"/// metadata is the FreeType face itself. We keep a pre-loaded face (loaded/// at discovery time) to answer `hasCodepoint` cheaply without re-opening/// the file on every query, and remember the path so `load()` can open a/// fresh face at the caller's requested size/options.pub const Windows = struct { /// Path to the font file. Owned here. path: [:0]const u8,
/// Face index within the file (for .ttc collections). face_index: i32,
/// Variations to apply on load. variations: []const font.face.Variation,
/// Pre-loaded face used for cheap metadata queries (glyphIndex, /// hasColor). The size it was opened at is irrelevant for these /// queries since the CMap is size-independent. Deinit'd with us. peek: Face,
/// Whether the face presents as emoji (has color glyphs) or text. presentation: Presentation,
/// Allocator that owns `path`. alloc: Allocator,
pub fn deinit(self: *Windows) void { self.peek.deinit(); self.alloc.free(self.path); self.* = undefined; }};
/// CoreText specific data. This is only present when building with CoreText.pub const CoreText = struct { /// The initialized font font: *macos.text.Font,
/// Variations to apply to this font. We apply the variations to the /// search descriptor but sometimes when the font collection is /// made the variation axes are reset so we have to reapply them. variations: []const font.face.Variation,
pub fn deinit(self: *CoreText) void { self.font.release(); self.* = undefined; }};
/// WebCanvas specific data. This is only present when building with canvas.pub const WebCanvas = struct { /// The allocator to use for fonts alloc: Allocator,
/// The string to use for the "font" attribute for the canvas font_str: [:0]const u8,
/// The presentation for this font. presentation: Presentation,
pub fn deinit(self: *WebCanvas) void { self.alloc.free(self.font_str); self.* = undefined; }};
pub fn deinit(self: *DeferredFace) void { switch (options.backend) { .fontconfig_freetype => if (self.fc) |*fc| fc.deinit(), .freetype => {}, .freetype_windows => if (self.win) |*w| w.deinit(), .web_canvas => if (self.wc) |*wc| wc.deinit(), .coretext, .coretext_freetype, .coretext_harfbuzz, .coretext_noshape, => if (self.ct) |*ct| ct.deinit(), } self.* = undefined;}
/// Returns the family name of the font.pub fn familyName(self: DeferredFace, buf: []u8) ![]const u8 { switch (options.backend) { .freetype => {},
.freetype_windows => if (self.win) |w| return try w.peek.name(buf),
.fontconfig_freetype => if (self.fc) |fc| return (try fc.pattern.get(.family, 0)).string,
.coretext, .coretext_freetype, .coretext_harfbuzz, .coretext_noshape, => if (self.ct) |ct| { const family_name = ct.font.copyAttribute(.family_name) orelse return "unknown"; return family_name.cstringPtr(.utf8) orelse unsupported: { break :unsupported family_name.cstring(buf, .utf8) orelse return error.OutOfMemory; }; },
.web_canvas => if (self.wc) |wc| return wc.font_str, }
return "";}
/// Returns the name of this face. The memory is always owned by the/// face so it doesn't have to be freed.pub fn name(self: DeferredFace, buf: []u8) ![]const u8 { switch (options.backend) { .freetype => {},
.freetype_windows => if (self.win) |w| return try w.peek.name(buf),
.fontconfig_freetype => if (self.fc) |fc| return (try fc.pattern.get(.fullname, 0)).string,
.coretext, .coretext_freetype, .coretext_harfbuzz, .coretext_noshape, => if (self.ct) |ct| { const display_name = ct.font.copyDisplayName(); return display_name.cstringPtr(.utf8) orelse unsupported: { // "NULL if the internal storage of theString does not allow // this to be returned efficiently." In this case, we need // to allocate. But we can't return an allocated string because // we don't have an allocator. Let's use the stack and log it. break :unsupported display_name.cstring(buf, .utf8) orelse return error.OutOfMemory; }; },
.web_canvas => if (self.wc) |wc| return wc.font_str, }
return "";}
/// Load the deferred font face. This does nothing if the face is loaded.pub fn load( self: *DeferredFace, lib: Library, opts: font.face.Options,) !Face { return switch (options.backend) { .fontconfig_freetype => try self.loadFontconfig(lib, opts), .freetype_windows => try self.loadWindows(lib, opts), .coretext, .coretext_harfbuzz, .coretext_noshape => try self.loadCoreText(lib, opts), .coretext_freetype => try self.loadCoreTextFreetype(lib, opts), .web_canvas => try self.loadWebCanvas(opts),
// Unreachable because we must be already loaded or have the // proper configuration for one of the other deferred mechanisms. .freetype => unreachable, };}
fn loadFontconfig( self: *DeferredFace, lib: Library, opts: font.face.Options,) !Face { const fc = self.fc.?;
// Filename and index for our face so we can load it const filename = (try fc.pattern.get(.file, 0)).string; const face_index = (try fc.pattern.get(.index, 0)).integer;
var face = try Face.initFile(lib, filename, face_index, opts); errdefer face.deinit(); try face.setVariations(fc.variations, opts); return face;}
fn loadWindows( self: *DeferredFace, lib: Library, opts: font.face.Options,) !Face { const w = self.win.?;
var face = try Face.initFile(lib, w.path, w.face_index, opts); errdefer face.deinit(); try face.setVariations(w.variations, opts); return face;}
fn loadCoreText( self: *DeferredFace, lib: Library, opts: font.face.Options,) !Face { _ = lib; const ct = self.ct.?; var face = try Face.initFontCopy(ct.font, opts); errdefer face.deinit(); try face.setVariations(ct.variations, opts); return face;}
fn loadCoreTextFreetype( self: *DeferredFace, lib: Library, opts: font.face.Options,) !Face { const ct = self.ct.?;
// Get the URL for the font so we can get the filepath const url = ct.font.copyAttribute(.url) orelse return error.FontHasNoFile; 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;
// Face index 0 is not always correct. We don't ship this configuration // in a release build. Users should use the pure CoreText builds. //std.log.warn("path={s}", .{path_slice}); var face = try Face.initFile(lib, buf[0..path_slice.len :0], 0, opts); errdefer face.deinit(); try face.setVariations(ct.variations, opts);
return face;}
fn loadWebCanvas( self: *DeferredFace, opts: font.face.Options,) !Face { const wc = self.wc.?; return try .initNamed(wc.alloc, wc.font_str, opts, wc.presentation);}
/// 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.////// This should not require the face to be loaded IF we're using a/// discovery mechanism (i.e. fontconfig). If no discovery is used,/// the face is always expected to be loaded.pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool { switch (options.backend) { .fontconfig_freetype => { // If we are using fontconfig, use the fontconfig metadata to // avoid loading the face. if (self.fc) |fc| { // Check if char exists if (!fc.charset.hasChar(cp)) return false;
// If we have a presentation, check it matches if (p) |desired| { const emoji_lang = "und-zsye"; const actual: Presentation = if (fc.langset.hasLang(emoji_lang)) .emoji else .text;
return desired == actual; }
return true; } },
.freetype_windows => { // Use the pre-loaded peek face for a cheap CMap lookup. if (self.win) |w| { if (p) |desired| if (w.presentation != desired) return false; return w.peek.glyphIndex(cp) != null; } },
.coretext, .coretext_freetype, .coretext_harfbuzz, .coretext_noshape, => { // If we are using coretext, we check the loaded CT font. if (self.ct) |ct| { // This presentation check isn't as detailed as isColorGlyph // because forced presentation modes are only used for emoji and // emoji should always have color glyphs set. This can be // more correct by using the isColorGlyph logic but I'd want // to find a font that actually requires this so we can write // a test for it before changing it. if (p) |desired_p| { const traits = ct.font.getSymbolicTraits(); const actual_p: Presentation = if (traits.color_glyphs) .emoji else .text; if (actual_p != desired_p) return false; }
// 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]); } },
// Canvas always has the codepoint because we have no way of // really checking and we let the browser handle it. .web_canvas => if (self.wc) |wc| { // Fast-path if we have a specific presentation and we // don't match, then it is definitely not this face. if (p) |desired| if (wc.presentation != desired) return false;
// Slow-path: we initialize the font, render it, and check // if it works and the presentation matches. var face = Face.initNamed( wc.alloc, wc.font_str, .{ .points = 12 }, wc.presentation, ) catch |err| { log.warn("failed to init face for codepoint check " ++ "face={s} err={}", .{ wc.font_str, err, });
return false; }; defer face.deinit(); return face.glyphIndex(cp) != null; },
.freetype => {}, }
// This is unreachable because discovery mechanisms terminate, and // if we're not using a discovery mechanism, the face MUST be loaded. unreachable;}
/// The wasm-compatible API.pub const Wasm = struct { const wasm = @import("../os/wasm.zig"); const alloc = wasm.alloc;
export fn deferred_face_new(ptr: [*]const u8, len: usize, presentation: u16) ?*DeferredFace { return deferred_face_new_(ptr, len, presentation) catch |err| { log.warn("error creating deferred face err={}", .{err}); return null; }; }
fn deferred_face_new_(ptr: [*]const u8, len: usize, presentation: u16) !*DeferredFace { const font_str = try alloc.dupeZ(u8, ptr[0..len]); errdefer alloc.free(font_str);
var face: DeferredFace = .{ .wc = .{ .alloc = alloc, .font_str = font_str, .presentation = @enumFromInt(presentation), }, }; errdefer face.deinit();
const result = try alloc.create(DeferredFace); errdefer alloc.destroy(result); result.* = face; return result; }
export fn deferred_face_free(ptr: ?*DeferredFace) void { if (ptr) |v| { v.deinit(); alloc.destroy(v); } }
export fn deferred_face_load(self: *DeferredFace, pts: f32) void { self.load(.{}, .{ .points = pts }) catch |err| { log.warn("error loading deferred face err={}", .{err}); return; }; }};
test "fontconfig" { if (options.backend != .fontconfig_freetype) return error.SkipZigTest;
const discovery = @import("main.zig").discovery; const testing = std.testing; const alloc = testing.allocator;
// Load freetype var lib = try Library.init(alloc); defer lib.deinit();
// Get a deferred face from fontconfig var def = def: { var fc = discovery.Fontconfig.init(lib); defer fc.deinit(); var it = try fc.discover(alloc, .{ .family = "monospace", .size = 12 }); defer it.deinit(); break :def (try it.next()).?; }; defer def.deinit();
// Verify we can get the name var buf: [1024]u8 = undefined; const n = try def.name(&buf); try testing.expect(n.len > 0);
// Load it and verify it works var face = try def.load(lib, .{ .size = .{ .points = 12 } }); defer face.deinit(); try testing.expect(face.glyphIndex(' ') != null);}
test "coretext" { if (options.backend != .coretext) return error.SkipZigTest;
const discovery = @import("main.zig").discovery; const testing = std.testing; const alloc = testing.allocator;
// Load freetype var lib = try Library.init(alloc); defer lib.deinit();
// Get a deferred face from fontconfig var def = def: { var fc = discovery.CoreText.init(lib); var it = try fc.discover(alloc, .{ .family = "Monaco", .size = 12 }); defer it.deinit(); break :def (try it.next()).?; }; defer def.deinit(); try testing.expect(def.hasCodepoint(' ', null));
// Verify we can get the name var buf: [1024]u8 = undefined; const n = try def.name(&buf); try testing.expect(n.len > 0);
// Load it and verify it works var face = try def.load(lib, .{ .size = .{ .points = 12 } }); defer face.deinit(); try testing.expect(face.glyphIndex(' ') != null);}