Something went wrong. Try again.
Draws glyphs from zig-font onto z2d surfaces from positioned glyph runs, with fontconfig-based font loading and fallback.
Something went wrong. Try again.
9.0 kB · 222 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Loads the fonts fontconfig's configuration finds into faces the renderer//! can draw, and keeps them.//!//! A font file is read and decoded once however many faces are made from//! it: a variable font matched at several weights is one `font.Font` and//! several `Face`s. Everything the loader hands out lives until `deinit`,//! at a stable address, so a `GlyphCache` can key on the faces.
const std = @import("std");const Allocator = std.mem.Allocator;const Io = std.Io;const font = @import("font");const fc = @import("font_config");const renderer = @import("font_renderer");const Face = renderer.Face;const FontList = @import("FontList.zig");
const Loader = @This();
gpa: Allocator,io: Io,config: *const fc.Config,fonts: std.ArrayList(*FontEntry) = .empty,faces: std.ArrayList(*FaceEntry) = .empty,
/// The largest font file that will be read.pub const max_file_size = 256 * 1024 * 1024;
const FontEntry = struct { path: []const u8, face_index: u32, data: []const u8, font: font.Font,};
const FaceEntry = struct { font: *FontEntry, named_instance: ?u16, variations: []const Face.Variation, face: Face,};
/// A loader that finds fonts with `config`, which must outlive it and have/// been scanned.pub fn init(gpa: Allocator, io: Io, config: *const fc.Config) Loader { return .{ .gpa = gpa, .io = io, .config = config };}
pub fn deinit(l: *Loader) void { for (l.faces.items) |e| { e.face.deinit(l.gpa); l.gpa.free(e.variations); l.gpa.destroy(e); } l.faces.deinit(l.gpa); for (l.fonts.items) |e| { e.font.deinit(); l.gpa.free(e.data); l.gpa.free(e.path); l.gpa.destroy(e); } l.fonts.deinit(l.gpa); l.* = undefined;}
pub const LoadError = Allocator.Error || font.Error || Io.Dir.ReadFileAllocError || error{ /// A pattern with no file to load. NoFile,};
/// The face a matched pattern describes: its `file`, the face and named/// instance its `index` encodes as FreeType does (`instance << 16 | face`),/// the axis settings in its `fontvariations`, which/// `Config.renderPrepare` writes for a variable font, whether its/// `embeddedbitmap` allows the font's bitmaps, and the hinting its/// `hinting` and `hintstyle` ask for (see `hintingOf`).pub fn face(l: *Loader, pattern: *const fc.Pattern) LoadError!*Face { const path = pattern.getString(.file, 0) orelse return error.NoFile; const index: u32 = @bitCast(pattern.getInteger(.index, 0) orelse 0); const instance: u16 = @truncate(index >> 16);
var vars: std.ArrayList(Face.Variation) = .empty; defer vars.deinit(l.gpa); if (pattern.getString(.fontvariations, 0)) |s| try parseVariations(l.gpa, s, &vars);
return l.loadFace(path, index & 0xffff, .{ // 0 is the font as it is and 0x8000 the whole variable font, which // fontconfig gives its own pattern; 1 and up are the named // instances, counted from one. .named_instance = if (instance == 0 or instance == 0x8000) null else instance - 1, .variations = vars.items, .embedded_bitmaps = (pattern.getBool(.embeddedbitmap, 0) orelse .true) != .false, .hinting = hintingOf(pattern), });}
/// The hinting a pattern asks for: none when its `hinting` is false or its/// `hintstyle` is `hintnone`, `light` for `hintslight`, and `full` for/// `hintmedium` and `hintfull`, fontconfig's default. That is cairo's/// mapping, in which only `hintslight` asks for FreeType's light mode./// `autohint` asks for the autohinter rather than the font's own/// instructions, and this renderer's autohinter is the light one.pub fn hintingOf(pattern: *const fc.Pattern) Face.Hinting { if ((pattern.getBool(.hinting, 0) orelse .true) == .false) return .none; const style = pattern.getInteger(.hintstyle, 0) orelse fc.object.hint.full; if (style <= fc.object.hint.none) return .none; if (style == fc.object.hint.slight) return .light; if ((pattern.getBool(.autohint, 0) orelse .false) == .true) return .light; return .full;}
/// Face `face_index` of the font file at `path`, at `options`, loading the/// file if it has not been loaded.pub fn loadFace(l: *Loader, path: []const u8, face_index: u32, options: Face.Options) LoadError!*Face { const fe = try l.loadFont(path, face_index); for (l.faces.items) |e| { if (e.font == fe and std.meta.eql(e.named_instance, options.named_instance) and e.face.embedded_bitmaps == options.embedded_bitmaps and e.face.hinting == options.hinting and sameVariations(e.variations, options.variations)) return &e.face; } const entry = try l.gpa.create(FaceEntry); errdefer l.gpa.destroy(entry); const variations = try l.gpa.dupe(Face.Variation, options.variations); errdefer l.gpa.free(variations); entry.* = .{ .font = fe, .named_instance = options.named_instance, .variations = variations, .face = try Face.init(l.gpa, &fe.font, .{ .named_instance = options.named_instance, .variations = variations, .embedded_bitmaps = options.embedded_bitmaps, .hinting = options.hinting, }), }; errdefer entry.face.deinit(l.gpa); try l.faces.append(l.gpa, entry); return &entry.face;}
fn sameVariations(a: []const Face.Variation, b: []const Face.Variation) bool { if (a.len != b.len) return false; for (a, b) |x, y| { if (!x.tag.eql(y.tag) or x.value != y.value) return false; } return true;}
fn loadFont(l: *Loader, path: []const u8, face_index: u32) LoadError!*FontEntry { for (l.fonts.items) |e| { if (e.face_index == face_index and std.mem.eql(u8, e.path, path)) return e; } const entry = try l.gpa.create(FontEntry); errdefer l.gpa.destroy(entry); const owned_path = try l.gpa.dupe(u8, path); errdefer l.gpa.free(owned_path); const data = try Io.Dir.cwd().readFileAlloc(l.io, path, l.gpa, .limited(max_file_size)); errdefer l.gpa.free(data); entry.* = .{ .path = owned_path, .face_index = face_index, .data = data, // The bytes are kept alongside, so the font borrows them rather // than holding a second copy. .font = try font.Font.parseIndex(l.gpa, data, face_index, .{ .copy = false }), }; errdefer entry.font.deinit(); try l.fonts.append(l.gpa, entry); return entry;}
/// The fonts that best match `query`, best first, as a fallback list:/// `query` goes through the configuration's rules as `fc-match` puts it,/// and fonts adding no character to those before them are left out, as/// `fc-match -s` leaves them out. Nothing is loaded until asked for.pub fn resolve(l: *Loader, query: *const fc.Pattern) (Allocator.Error || fc.Config.MatchError)!FontList { var prepared = try query.duplicate(l.gpa); errdefer prepared.deinit(); try l.config.prepare(&prepared); const fonts = try l.config.sort(l.gpa, &prepared, true, null); errdefer l.gpa.free(fonts); const states = try l.gpa.alloc(FontList.State, fonts.len); @memset(states, .unloaded); return .{ .loader = l, .query = prepared, .fonts = fonts, .states = states };}
/// `resolve` for a query in fontconfig's text form, `Family-12:bold`.pub fn resolveName(l: *Loader, name: []const u8) (Allocator.Error || fc.Config.MatchError || fc.name.ParseError)!FontList { var query = try fc.name.parse(l.gpa, name); defer query.deinit(); return l.resolve(&query);}
/// Parses fontconfig's `fontvariations`: `tag=value` pairs separated by/// commas, as in `wght=700,wdth=75`. A pair that does not parse is skipped.pub fn parseVariations(gpa: Allocator, s: []const u8, out: *std.ArrayList(Face.Variation)) Allocator.Error!void { var it = std.mem.tokenizeScalar(u8, s, ','); while (it.next()) |pair| { const eq = std.mem.findScalar(u8, pair, '=') orelse continue; const tag = std.mem.trim(u8, pair[0..eq], " "); if (tag.len == 0 or tag.len > 4) continue; var t: [4]u8 = @splat(' '); @memcpy(t[0..tag.len], tag); const value = std.fmt.parseFloat(f32, std.mem.trim(u8, pair[eq + 1 ..], " ")) catch continue; try out.append(gpa, .{ .tag = .{ .bytes = t }, .value = value }); }}
test "fontvariations parse to axis settings" { var out: std.ArrayList(Face.Variation) = .empty; defer out.deinit(std.testing.allocator); try parseVariations(std.testing.allocator, "wght=700, wdth=75.5,bad,=3,opsz=x", &out); try std.testing.expectEqual(@as(usize, 2), out.items.len); try std.testing.expect(out.items[0].tag.eql(.init("wght"))); try std.testing.expectEqual(@as(f32, 700), out.items[0].value); try std.testing.expect(out.items[1].tag.eql(.init("wdth"))); try std.testing.expectEqual(@as(f32, 75.5), out.items[1].value);}