// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! A font at one point in its design space: a decoded `font.Font` together //! with the normalized variation coordinates everything drawn from it uses. //! //! A static font has no coordinates and a face of it is just the font. A //! variable font's face is one instance of it — a weight, a width — and two //! faces of the same font at different coordinates draw different outlines //! and have different advances. //! //! The face borrows the font, which must outlive it, and is identified by //! its address: a `GlyphCache` keys on `*const Face`, so a face must not move //! while anything cached from it is live, and `GlyphCache.evictFace` must be //! called before it is freed. //! //! A face of a font with an `SVG ` table keeps the documents it has parsed, //! so drawing from one face on two threads at once is not safe. const std = @import("std"); const Allocator = std.mem.Allocator; const zfont = @import("font"); const F2Dot14 = zfont.F2Dot14; const Fixed = zfont.Fixed; const Tag = zfont.Tag; const bitmap = @import("bitmap.zig"); const svg = @import("svg.zig"); const color_bitmap = @import("color_bitmap.zig"); const hint = @import("hint.zig"); const Face = @This(); font: *const zfont.Font, /// Normalized coordinates, one per `fvar` axis in its order; empty for a /// static font, and for a variable font at its default instance. coords: []const F2Dot14, units_per_em: u16, /// Draw glyphs from the font's `EBLC`/`EBDT` bitmaps where it has them; see /// `Options.embedded_bitmaps`. embedded_bitmaps: bool, /// The `SVG ` documents parsed so far; null when the font has none. svg_documents: ?*svg.Documents = null, /// How outlines are fitted to the pixel grid; see `Hinting`. hinting: Hinting = .none, /// What the autohinter measured of the font; null when the face is not /// hinted or has no outlines. hint_globals: ?*hint.Globals = null, /// The font's instructions, set up for the sizes drawn at; null unless /// the face's hinting is `.full` and the font has instructions. tt_hinter: ?*hint.tt.Hinter = null, /// How a face's outlines are fitted to the pixel grid, after fontconfig's /// `hintstyle`. pub const Hinting = enum { /// Drawn exactly as designed. none, /// Vertically only, by the autohinter: baselines, x-heights and the /// tops and bottoms of stems on whole pixels, with every horizontal /// position and advance as designed. `hintslight`. light, /// What the font's own instructions ask for, for a TrueType font that /// has them; their vertical hinting, that is, as a ClearType /// rasterizer in backward compatibility mode and FreeType's default /// interpreter run them, with every horizontal position as designed. /// A font without instructions, and a glyph that will not load for /// them, is hinted as by `light`. `hintfull`, and `hintmedium`. full, }; /// One axis setting, in the axis's own user units: a weight of 100 to 900, a /// width in percent. pub const Variation = struct { tag: Tag, value: f32, }; pub const Options = struct { /// A named instance from `fvar` to start from, by index. Out of range is /// ignored, as fontconfig's index encoding can name one that a broken /// font does not have. named_instance: ?u16 = null, /// Axis settings applied after the named instance. An axis the font /// does not have is ignored, as CSS and fontconfig ignore one; a value /// outside the axis's range is clamped to it. variations: []const Variation = &.{}, /// Draw a glyph from its embedded bitmap, at a size the font has one /// for, rather than from its outline; and from the nearest bitmap, /// scaled, when it has no outline at all. fontconfig's `embeddedbitmap` /// says the same, and some configurations turn it off for fonts whose /// bitmaps look worse than their outlines. embedded_bitmaps: bool = true, /// Fit outlines to the pixel grid. Off by default, so that a face drawn /// at any transformation looks the same; text on screen generally /// wants it, and fontconfig's `hinting` and `hintstyle` say which. hinting: Hinting = .none, }; pub const InitError = Allocator.Error || zfont.Error; pub fn init(gpa: Allocator, f: *const zfont.Font, options: Options) InitError!Face { var face = try initInstance(gpa, f, options); errdefer face.deinit(gpa); face.hinting = options.hinting; if (options.hinting != .none and face.hasOutlines()) { const g = try gpa.create(hint.Globals); errdefer gpa.destroy(g); g.* = try hint.Globals.init(gpa, &face); face.hint_globals = g; } if (options.hinting == .full and hint.tt.instructed(f)) { const h = try gpa.create(hint.tt.Hinter); h.* = .{}; face.tt_hinter = h; } return face; } fn initInstance(gpa: Allocator, f: *const zfont.Font, options: Options) InitError!Face { var face: Face = .{ .font = f, .coords = &.{}, .units_per_em = if (f.head.units_per_em == 0) 1000 else f.head.units_per_em, .embedded_bitmaps = options.embedded_bitmaps, }; if (f.svg != null) { const docs = try gpa.create(svg.Documents); docs.* = .{}; face.svg_documents = docs; } errdefer face.deinitDocuments(gpa); const fvar = f.fvar orelse return face; if (fvar.axes.len == 0) return face; const user = try gpa.alloc(Fixed, fvar.axes.len); defer gpa.free(user); for (user, fvar.axes) |*u, axis| u.* = axis.default_value; if (options.named_instance) |i| if (i < fvar.instances.len) { const inst = fvar.instances[i]; for (user, 0..) |*u, a| if (a < inst.coordinates.len) { u.* = inst.coordinates[a]; }; }; for (options.variations) |v| { const a = fvar.axisIndex(v.tag) orelse continue; if (!std.math.isFinite(v.value)) continue; const axis = fvar.axes[a]; const lo = @min(axis.min_value.toFloat(), axis.max_value.toFloat()); const hi = @max(axis.min_value.toFloat(), axis.max_value.toFloat()); user[a] = .fromFloat(std.math.clamp(@as(f64, v.value), lo, hi)); } const coords = try gpa.alloc(F2Dot14, fvar.axes.len); errdefer gpa.free(coords); @memset(coords, .{ .raw = 0 }); try f.normalizeCoordinates(gpa, user, coords); // All zero is the default instance, which every variation table gives no // deltas at. Saying so with an empty slice lets the outline code take // the static path, which is faster and is what a static font gets. for (coords) |c| { if (c.raw != 0) { face.coords = coords; return face; } } gpa.free(coords); return face; } pub fn deinit(face: *Face, gpa: Allocator) void { if (face.coords.len > 0) gpa.free(face.coords); if (face.hint_globals) |g| gpa.destroy(g); if (face.tt_hinter) |h| { h.deinit(gpa); gpa.destroy(h); } face.deinitDocuments(gpa); face.* = undefined; } fn deinitDocuments(face: *Face, gpa: Allocator) void { const docs = face.svg_documents orelse return; docs.deinit(gpa); gpa.destroy(docs); face.svg_documents = null; } /// How many pixels one font unit is at `size` pixels per em. pub fn scale(face: *const Face, size: f32) f32 { return size / @as(f32, @floatFromInt(face.units_per_em)); } /// The number of glyphs in the font. pub fn glyphCount(face: *const Face) u16 { return face.font.maxp.num_glyphs; } /// The glyph `codepoint` maps to in the font's best `cmap` subtable, or /// null if it maps to none. pub fn glyphIndex(face: *const Face, codepoint: u21) ?u16 { const cmap = face.font.cmap orelse return null; const gid = cmap.lookup(codepoint) orelse return null; if (gid == 0 or gid > std.math.maxInt(u16)) return null; return @intCast(gid); } /// The horizontal advance of `glyph_id` in font units, varied by `HVAR` at /// the face's coordinates. Zero for a glyph the font has no metrics for. /// /// A variable TrueType font with no `HVAR` varies its advances through the /// phantom points of `gvar`, which zig-font does not apply; such a font gets /// its default advances. pub fn advance(face: *const Face, glyph_id: u16) f32 { const hmtx = face.font.hmtx orelse return 0; if (glyph_id >= hmtx.metrics.len) return 0; var a: f32 = @floatFromInt(hmtx.metrics[glyph_id].advance); if (face.coords.len > 0) if (face.font.hvar) |hvar| { a += hvar.advanceDelta(glyph_id, face.coords) catch 0; }; return a; } /// The kerning between two glyphs from the legacy `kern` table, in font /// units, negative to bring them closer. A shaper applying `GPOS` kerning /// should not add this as well. pub fn kerning(face: *const Face, left: u16, right: u16) f32 { const kern = face.font.kern orelse return 0; return @floatFromInt(kern.pairValue(left, right)); } /// Vertical metrics, in font units, y up: `ascender` positive, `descender` /// negative. pub const Metrics = struct { ascender: f32, descender: f32, line_gap: f32, /// The top of the underline, below the baseline when negative. underline_position: f32, underline_thickness: f32, /// The bottom of the strikeout stroke, above the baseline. strikeout_position: f32, strikeout_thickness: f32, /// The distance from one baseline to the next. pub fn lineHeight(m: Metrics) f32 { return m.ascender - m.descender + m.line_gap; } /// Every field multiplied by `factor`; `Face.scale` gives the factor /// for a pixel size. pub fn scaled(m: Metrics, factor: f32) Metrics { var out = m; inline for (@typeInfo(Metrics).@"struct".field_names) |name| @field(out, name) *= factor; return out; } }; /// The face's vertical metrics: `OS/2`'s typographic ones when the font sets /// `USE_TYPO_METRICS`, `hhea`'s otherwise, as FreeType and HarfBuzz choose; /// varied by `MVAR` at the face's coordinates. pub fn metrics(face: *const Face) Metrics { const f = face.font; const upem: f32 = @floatFromInt(face.units_per_em); var m: Metrics = .{ .ascender = upem * 0.8, .descender = -upem * 0.2, .line_gap = 0, .underline_position = -upem * 0.1, .underline_thickness = upem * 0.05, .strikeout_position = upem * 0.25, .strikeout_thickness = upem * 0.05, }; var from_typo = false; if (f.os2) |os2| { if (os2.metrics) |om| if (os2.fs_selection.use_typo_metrics) { m.ascender = @floatFromInt(om.s_typo_ascender); m.descender = @floatFromInt(om.s_typo_descender); m.line_gap = @floatFromInt(om.s_typo_line_gap); from_typo = true; }; if (os2.y_strikeout_size > 0) { m.strikeout_position = @floatFromInt(os2.y_strikeout_position); m.strikeout_thickness = @floatFromInt(os2.y_strikeout_size); } } if (!from_typo) if (f.hhea) |hhea| { if (hhea.ascender != 0 or hhea.descender != 0) { m.ascender = @floatFromInt(hhea.ascender); m.descender = @floatFromInt(hhea.descender); m.line_gap = @floatFromInt(hhea.line_gap); } }; if (f.post) |post| if (post.underline_thickness > 0) { m.underline_position = @floatFromInt(post.underline_position); m.underline_thickness = @floatFromInt(post.underline_thickness); }; if (face.coords.len > 0) if (f.mvar) |mvar| { // MVAR names the OS/2 typographic metrics; FreeType applies the // same deltas to hhea's, since the two describe the same thing. m.ascender += mvarDelta(mvar, "hasc", face.coords); m.descender += mvarDelta(mvar, "hdsc", face.coords); m.line_gap += mvarDelta(mvar, "hlgp", face.coords); m.underline_position += mvarDelta(mvar, "undo", face.coords); m.underline_thickness += mvarDelta(mvar, "unds", face.coords); m.strikeout_position += mvarDelta(mvar, "stro", face.coords); m.strikeout_thickness += mvarDelta(mvar, "strs", face.coords); }; return m; } fn mvarDelta(mvar: zfont.tables.mvar.Mvar, comptime tag: *const [4]u8, coords: []const F2Dot14) f32 { const d = mvar.delta(.init(tag), coords) catch return 0; return d orelse 0; } /// The outline of `glyph_id` at the face's coordinates, in font units, y /// up. Owned by the caller: free `commands` with `gpa`. pub fn glyphOutline(face: *const Face, gpa: Allocator, glyph_id: u16) zfont.Error!zfont.Path { return face.font.glyphPath(gpa, glyph_id, face.coords); } /// Whether the font has outlines: `glyf`, `CFF ` or `CFF2`. pub fn hasOutlines(face: *const Face) bool { const f = face.font; return f.glyf != null or f.cff != null or f.cff2 != null; } /// Whether the face has anything this renderer can draw: outlines, `SVG ` /// documents, `CBDT` or `sbix` pictures, or monochrome or grayscale `EBDT` /// bitmaps it is allowed to use. pub fn canDraw(face: *const Face) bool { return face.hasOutlines() or face.font.svg != null or color_bitmap.hasAny(face) or (face.embedded_bitmaps and bitmap.hasMasks(face)); } /// What kind of color glyph `glyph_id` is, if it is one. pub const ColorKind = enum { /// Not a color glyph; drawn from its outline in the text color. none, /// A `COLR` version 0 glyph: outlines layered in palette colors. colr_v0, /// A `COLR` version 1 glyph: a paint graph. colr_v1, /// A glyph drawn from an `SVG ` document. svg, /// A `CBDT` picture: a PNG, as Google's emoji fonts carry them. cbdt, /// An `sbix` picture: a PNG or JPEG, as Apple's emoji fonts carry them. sbix, }; /// The best color form `glyph_id` has. A `COLR` version 1 paint graph /// comes first, as the cheapest to draw of the two forms that can carry a /// whole design; then an `SVG ` document; then `COLR` version 0 layers, /// which a font with SVG glyphs often carries only as a fallback for /// renderers that cannot draw them. Pictures come last, `CBDT` before /// `sbix` as HarfBuzz orders them, as they are drawn for a few sizes and /// scaled to any other, where the rest are drawn for every size alike. pub fn colorKind(face: *const Face, glyph_id: u16) ColorKind { const colr = face.font.colr; if (colr) |c| if (c.basePaint(glyph_id) != null) return .colr_v1; if (svg.covers(face, glyph_id)) return .svg; if (colr) |c| if (c.layersV0(glyph_id) != null) return .colr_v0; if (color_bitmap.hasCbdt(face, glyph_id)) return .cbdt; if (color_bitmap.hasSbix(face, glyph_id)) return .sbix; return .none; } test "a static font has no coordinates" { const testing = std.testing; // The smallest thing `Font` will take as a face: no tables but the two // it requires is not something it parses, so this goes through init's // early return on a font with no fvar via a zeroed struct instead. var f: zfont.Font = undefined; f.fvar = null; f.svg = null; f.head.units_per_em = 2048; var face = try Face.init(testing.allocator, &f, .{ .variations = &.{.{ .tag = .init("wght"), .value = 700 }} }); defer face.deinit(testing.allocator); try testing.expectEqual(@as(usize, 0), face.coords.len); try testing.expectEqual(@as(f32, 0.5), face.scale(1024)); }