// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-FileCopyrightText: © 2009 Red Hat, Inc. // SPDX-FileCopyrightText: © 2011,2014 Google, Inc. // SPDX-License-Identifier: MIT AND MIT-Modern-Variant //! A font as the shaper asks questions of it: which glyph a character maps //! to, how far a glyph advances, how big it is. This is what HarfBuzz calls //! its `ot` font functions (`hb-ot-font.cc`), answering from the tables //! zig-font decoded, at one location in a variable font's design space. //! //! Answers are in the face's scale: `x_scale` units per em horizontally and //! `y_scale` vertically. By default that is the font's own units per em, so //! positions come out in font units. A different scale is applied the way //! HarfBuzz applies it, value by value with rounding at each step, so that //! the results agree with HarfBuzz's to the unit rather than nearly. const std = @import("std"); const round = @import("round.zig"); const zfont = @import("font"); const cmap_mod = @import("cmap.zig"); const Face = @This(); const F2Dot14 = zfont.F2Dot14; /// Borrowed; must outlive the face. font: *const zfont.Font, /// For the scratch space a few answers need, such as applying `gvar` to a /// glyph's phantom points. Nothing allocated is kept. gpa: std.mem.Allocator, /// Normalized coordinates, one per `fvar` axis, or none for the default /// instance. Borrowed. coords: []const F2Dot14 = &.{}, units_per_em: i32, cmap: cmap_mod.Accelerator, x_scale: i32, y_scale: i32, // Derived from the scales, as HarfBuzz's `mults_changed` does. x_mult: i64, y_mult: i64, x_multf: f32, y_multf: f32, pub fn init(gpa: std.mem.Allocator, f: *const zfont.Font, coords: []const F2Dot14) Face { var nonzero = false; for (coords) |c| if (c.raw != 0) { nonzero = true; }; const upem: i32 = if (f.head.units_per_em == 0) 1000 else f.head.units_per_em; var face: Face = .{ .font = f, .gpa = gpa, .coords = if (nonzero) coords else &.{}, .units_per_em = upem, .cmap = .init(f), .x_scale = upem, .y_scale = upem, .x_mult = 0, .y_mult = 0, .x_multf = 0, .y_multf = 0, }; face.setScale(upem, upem); return face; } /// Sets how many units the em is divided into: `units_per_em` to work in /// font units, or a pixel size times 64 for 26.6 fixed point, say. pub fn setScale(face: *Face, x_scale: i32, y_scale: i32) void { face.x_scale = x_scale; face.y_scale = y_scale; const upem: f32 = @floatFromInt(face.units_per_em); face.x_multf = @as(f32, @floatFromInt(x_scale)) / upem; face.y_multf = @as(f32, @floatFromInt(y_scale)) / upem; face.x_mult = mult(x_scale, face.units_per_em); face.y_mult = mult(y_scale, face.units_per_em); } fn mult(scale: i32, upem: i32) i64 { const s: i64 = scale; return if (s < 0) -@divTrunc((-s) << 16, upem) else @divTrunc(s << 16, upem); } fn emMult(v: i16, m: i64) i32 { return @truncate((@as(i64, v) * m + 32768) >> 16); } /// `em_scale_x`: a font-unit value at the face's scale. The value is taken /// as 16 bits, as HarfBuzz takes it. pub fn emScaleX(face: *const Face, v: i32) i32 { return emMult(@truncate(v), face.x_mult); } pub fn emScaleY(face: *const Face, v: i32) i32 { return emMult(@truncate(v), face.y_mult); } /// `em_scalef_x`: a fractional font-unit value at the face's scale, rounded. pub fn emScalefX(face: *const Face, v: f32) i32 { return round.toInt(v * face.x_multf); } pub fn emScalefY(face: *const Face, v: f32) i32 { return round.toInt(v * face.y_multf); } /// `em_fscale_x`: a font-unit value at the face's scale, unrounded. pub fn emFscaleX(face: *const Face, v: i16) f32 { return @as(f32, @floatFromInt(v)) * face.x_multf; } pub fn emFscaleY(face: *const Face, v: i16) f32 { return @as(f32, @floatFromInt(v)) * face.y_multf; } pub fn hasNonzeroCoords(face: *const Face) bool { return face.coords.len > 0; } pub fn glyphCount(face: *const Face) u32 { return face.font.maxp.num_glyphs; } /// The glyph `cp` maps to in `cmap`, or null when it maps to none. Glyph 0, /// `.notdef`, counts as none. pub fn nominalGlyph(face: *const Face, cp: u32) ?u32 { return face.cmap.nominal(cp); } pub fn hasGlyph(face: *const Face, cp: u32) bool { return face.nominalGlyph(cp) != null; } /// The glyph for `cp` followed by variation selector `vs`, from `cmap` /// format 14, or null when the font does not support the sequence. pub fn variationGlyph(face: *const Face, cp: u32, vs: u32) ?u32 { return face.cmap.variation(cp, vs); } fn advanceWithoutVar(m: zfont.tables.hmtx.Hmtx, gid: u32) i32 { if (gid >= m.metrics.len) return 0; return m.metrics[gid].advance; } /// The horizontal advance in font units, varied: `hmtx` with `HVAR`, or /// without `HVAR` the distance between `gvar`'s varied phantom points. fn hAdvanceUnscaled(face: *const Face, hmtx: zfont.tables.hmtx.Hmtx, gid: u32) i32 { const advance = advanceWithoutVar(hmtx, gid); if (!face.hasNonzeroCoords()) return advance; if (face.font.hvar) |hvar| { const d = hvar.advanceDelta(gid, face.coords) catch 0; return @intFromFloat(@max(0, @as(f32, @floatFromInt(advance)) + round.roundf(d))); } if (face.font.gvar != null and face.font.glyf != null) { if (face.phantomDeltas(gid)) |d| { return @intFromFloat(@max(0, round.roundf(@as(f32, @floatFromInt(advance)) + d[1].x - d[0].x))); } } return advance; } /// `hb_ot_get_glyph_h_advances`, for one glyph. pub fn hAdvance(face: *const Face, gid: u32) i32 { const hmtx = face.font.hmtx orelse return face.emScaleX(@divTrunc(face.units_per_em, 2)); return face.emScaleX(face.hAdvanceUnscaled(hmtx, gid)); } /// `hb_ot_get_glyph_v_advances`, for one glyph. Negative: in HarfBuzz's /// convention y points up, and vertical text runs down. pub fn vAdvance(face: *const Face, gid: u32) i32 { const vmtx = face.font.vmtx orelse { const e = face.fontExtents(); return -(e.ascender - e.descender); }; var advance = advanceWithoutVar(vmtx, gid); if (face.hasNonzeroCoords()) { if (face.font.vvar) |vvar| { const d = vvar.advanceDelta(gid, face.coords) catch 0; advance = @intFromFloat(@max(0, @as(f32, @floatFromInt(advance)) + round.roundf(d))); } else if (face.font.gvar != null and face.font.glyf != null) { if (face.phantomDeltas(gid)) |d| { advance = @intFromFloat(@max(0, round.roundf(@as(f32, @floatFromInt(advance)) - (d[3].y - d[2].y)))); } } } return face.emScaleY(-advance); } /// `hb_ot_get_glyph_v_origins`, for one glyph: where the vertical origin is /// relative to the horizontal one. pub fn vOrigin(face: *const Face, gid: u32) struct { x: i32, y: i32 } { const x = @divTrunc(face.hAdvance(gid), 2); if (face.font.vorg) |vorg| { var y: f32 = @floatFromInt(vorg.originY(@intCast(@min(gid, 0xFFFF)))); if (face.hasNonzeroCoords()) if (face.font.vvar) |vvar| { y += (vvar.verticalOriginDelta(gid, face.coords) catch null) orelse 0; }; return .{ .x = x, .y = face.emScalefY(y) }; } if (face.font.vmtx) |vmtx| if (face.font.glyf) |glyf| { // The top phantom point: the glyph's top plus its top side bearing. if (gid < glyf.glyphs.len and gid < vmtx.metrics.len) { const tsb: f32 = @floatFromInt(vmtx.metrics[gid].side_bearing); const top: f32 = switch (glyf.glyphs[gid]) { .simple => |sg| @floatFromInt(sg.bounds.y_max), .composite => |c| @floatFromInt(c.bounds.y_max), else => 0, }; var y = top + tsb; if (face.hasNonzeroCoords()) if (face.phantomDeltas(gid)) |d| { y += d[2].y; }; return .{ .x = x, .y = face.emScalefY(y) }; } }; const fe = face.fontExtents(); if (face.extents(gid)) |e| { const font_advance = fe.ascender - fe.descender; return .{ .x = x, .y = e.y_bearing + ((font_advance - -e.height) >> 1) }; } return .{ .x = x, .y = fe.ascender }; } /// The deltas `gvar` gives the four phantom points of a TrueType glyph, /// which carry its metrics: left, right, top, bottom. Null when it gives /// none. fn phantomDeltas(face: *const Face, gid: u32) ?[4]zfont.outline.Point { const glyf = face.font.glyf orelse return null; const gvar = face.font.gvar orelse return null; if (gid >= glyf.glyphs.len) return null; const tuples = gvar.glyph(gid); if (tuples.len == 0) return null; // Phantom points are never interpolated, so their own deltas are all // that count; the outline's points only have to be there for the // indices to line up. const gpa = face.gpa; const n: usize = switch (glyf.glyphs[gid]) { .simple => |s| s.points.len, .composite => |c| c.components.len, .empty => 0, .malformed => return null, }; const end_pts: []u16 = gpa.alloc(u16, if (glyf.glyphs[gid] == .composite) n else 0) catch return null; defer gpa.free(end_pts); for (end_pts, 0..) |*e, i| e.* = @intCast(i); const points = gpa.alloc(zfont.outline.Point, n + 4) catch return null; defer gpa.free(points); @memset(points, .{ .x = 0, .y = 0 }); const ends: []const u16 = switch (glyf.glyphs[gid]) { .simple => |s| s.end_pts, else => end_pts, }; const moved = zfont.tables.gvar.apply(gpa, tuples, face.coords, points, ends) catch return null; defer gpa.free(moved); var out: [4]zfont.outline.Point = undefined; for (&out, moved[n..][0..4]) |*o, m| o.* = m; return out; } pub const Extents = struct { x_bearing: i32, y_bearing: i32, width: i32, /// Negative: from the top of the glyph down to its bottom. height: i32, }; /// `scale_glyph_extents`: extents in font units at the face's scale, each /// corner scaled and rounded. fn scaleExtents(face: *const Face, e: Extents) Extents { const x1: f32 = @floatFromInt(face.emScaleX(e.x_bearing)); const y1: f32 = @floatFromInt(face.emScaleY(e.y_bearing)); const x2: f32 = @floatFromInt(face.emScaleX(e.x_bearing + e.width)); const y2: f32 = @floatFromInt(face.emScaleY(e.y_bearing + e.height)); const xb: i32 = round.toInt(x1); const yb: i32 = round.toInt(y1); return .{ .x_bearing = xb, .y_bearing = yb, .width = @as(i32, round.toInt(x2)) - xb, .height = @as(i32, round.toInt(y2)) - yb, }; } /// `hb_ot_get_glyph_extents`: the glyph's ink box, from its outline. Null /// for a glyph with no outline the face can read. pub fn extents(face: *const Face, gid: u32) ?Extents { if (gid > std.math.maxInt(u16)) return null; if (face.font.glyf) |glyf| { if (gid >= glyf.glyphs.len) return null; if (!face.hasNonzeroCoords()) { const b = switch (glyf.glyphs[gid]) { .simple => |sg| sg.bounds, .composite => |c| c.bounds, .empty => return .{ .x_bearing = 0, .y_bearing = 0, .width = 0, .height = 0 }, .malformed => return null, }; // As rasterizers do, the glyph is placed by its left side // bearing rather than its xMin. var lsb: i32 = @min(b.x_min, b.x_max); if (face.font.hmtx) |hmtx| if (gid < hmtx.metrics.len) { lsb = hmtx.metrics[gid].side_bearing; }; return face.scaleExtents(.{ .x_bearing = lsb, .y_bearing = @max(b.y_min, b.y_max), .width = @as(i32, @max(b.x_min, b.x_max)) - @min(b.x_min, b.x_max), .height = @as(i32, @min(b.y_min, b.y_max)) - @max(b.y_min, b.y_max), }); } } else if (face.font.cff == null and face.font.cff2 == null) return null; const path = face.font.glyphPath(face.gpa, @intCast(gid), face.coords) catch return null; defer face.gpa.free(path.commands); var e: Extents = .{ .x_bearing = 0, .y_bearing = 0, .width = 0, .height = 0 }; if (path.controlBounds()) |b| { if (b.x_min < b.x_max) { e.x_bearing = round.toInt(b.x_min); e.width = round.toInt(b.x_max - @as(f32, @floatFromInt(e.x_bearing))); } if (b.y_min < b.y_max) { e.y_bearing = round.toInt(b.y_max); e.height = round.toInt(b.y_min - @as(f32, @floatFromInt(e.y_bearing))); } } return face.scaleExtents(e); } pub const FontExtents = struct { ascender: i32, descender: i32, line_gap: i32, }; fn metric(face: *const Face, comptime tag: *const [4]u8, value: i16) f32 { var v: f32 = @floatFromInt(value); if (face.hasNonzeroCoords()) if (face.font.mvar) |mvar| { v += (mvar.delta(zfont.Tag.init(tag), face.coords) catch null) orelse 0; }; return v; } /// `get_h_extents_with_fallback`: the font's ascender, descender and line /// gap, from `OS/2` when it says to use its typographic metrics and `hhea` /// otherwise, varied by `MVAR`; failing both, 80% and 20% of the em. pub fn fontExtents(face: *const Face) FontExtents { const typo = if (face.font.os2) |os2| (if (os2.fs_selection.use_typo_metrics) os2.metrics else null) else null; var asc: f32 = undefined; var desc: f32 = undefined; var gap: f32 = undefined; if (typo) |m| { asc = face.metric("hasc", m.s_typo_ascender); desc = face.metric("hdsc", m.s_typo_descender); gap = face.metric("hlgp", m.s_typo_line_gap); } else if (face.font.hhea) |hhea| { asc = face.metric("hasc", hhea.ascender); desc = face.metric("hdsc", hhea.descender); gap = face.metric("hlgp", hhea.line_gap); } else { const a: i32 = @intFromFloat(@as(f32, @floatFromInt(face.y_scale)) * 0.8); return .{ .ascender = a, .descender = a - face.y_scale, .line_gap = 0 }; } return .{ .ascender = face.emScalefY(@abs(asc)), .descender = face.emScalefY(-@abs(desc)), .line_gap = face.emScalefY(gap), }; }