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.
15 kB · 385 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// 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));}