Something went wrong. Try again.
An OpenType text shaper for Zig, ported from HarfBuzz and checked against its test suite, for fonts decoded by zig-font.
Something went wrong. Try again.
14 kB · 369 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// 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), };}