- Zig 96.1%
- Python 2.7%
- Nix 1.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
The package is now `font` rather than `zig_font`, matching the module it
exports, so a dependent writes `b.dependency("font", ...)`.
The fingerprint's high half is a checksum of the name and changes with
it; the low half, the package's random identity, is kept, since this is
the same package renamed and not a fork.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGQDQ1mkbqUhBH5poiYMVH
|
||
| .forgejo/workflows | ||
| LICENSES | ||
| src | ||
| tests | ||
| tools | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| build.zig.zon.nix | ||
| flake.lock | ||
| flake.nix | ||
| package.nix | ||
| README.md | ||
| REUSE.toml | ||
zig-font
A Zig 0.17 library that decodes TrueType and OpenType fonts into plain Zig values: metrics, character maps, names, outlines, layout lookups, variation data, color glyphs and embedded bitmaps. It also decodes the X Window System's bitmap fonts, BDF and PCF.
It does no I/O. You hand it the bytes of a font file and it hands back structures. Shaping, rasterizing, subsetting and anything else you might do with a font are left to the program that imports it.
The API reference is generated from the doc comments, which carry most of the
explanation of what each structure holds and why. It is published from the
default branch to https://jeff.jcollie.page/zig-font/. To read it locally,
run zig build docs-serve, which serves it at http://127.0.0.1:8000/; use
-Ddocs-port=N for a different port. The pages have to be served rather than
opened from disk, because the viewer fetches its data at runtime and a browser
refuses to do that from a file:// page.
Quick start
const std = @import("std");
const font = @import("font");
pub fn example(gpa: std.mem.Allocator, bytes: []const u8) !void {
var f = try font.Font.parse(gpa, bytes, .{});
defer f.deinit();
const name = f.name.?.find(font.tables.name.id.full_name) orelse "(unnamed)";
const units_per_em = f.head.units_per_em;
// From a character to a glyph, and from a glyph to its outline.
const glyph = f.cmap.?.lookup('A') orelse 0;
const path = try f.glyphPath(gpa, @intCast(glyph), &.{});
defer gpa.free(path.commands);
std.debug.print("{s}: {d} units/em, 'A' is glyph {d} with {d} contours\n", .{
name, units_per_em, glyph, path.contourCount(),
});
}
To use it from another project, run zig fetch --save <url> and import the
font module in build.zig:
const font = b.dependency("font", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("font", font.module("font"));
What it decodes
Font.parse decodes everything at once, and each table also has a decoder of
its own under font.tables, for a program that wants one table and not the
rest.
| Area | Tables |
|---|---|
| Containers | TrueType and OpenType sfnt, TrueType Collections (.ttc/.otc), WOFF 1.0, WOFF 2.0 (collections included) |
| Required | head, maxp |
| Metrics | hhea, hmtx, vhea, vmtx, VORG, hdmx, LTSH, VDMX, PCLT |
| Naming and mapping | name (decoded to UTF-8), OS/2 (versions 0–5), post (all versions, with glyph names), cmap (formats 0, 2, 4, 6, 8, 10, 12, 13 and 14) |
| TrueType outlines | loca, glyf (simple and composite glyphs), cvt , fpgm, prep, gasp |
| PostScript outlines | CFF (including CID-keyed fonts) and CFF2, with the charstrings run into paths |
| Layout | GDEF, GSUB and GPOS (every lookup type and subtable format), BASE, JSTF, MATH, legacy kern (OpenType and Apple versions) |
| Variations | fvar, avar (versions 1 and 2), gvar, cvar, HVAR, VVAR, MVAR, STAT |
| Color and bitmaps | COLR (version 0 and the version 1 paint graph), CPAL, CBLC/CBDT, EBLC/EBDT, EBSC, sbix, SVG |
| Other | meta, DSIG (decoded, not verified) |
| Bitmap fonts | BDF 2.1, 2.2 and the grayscale 2.3; PCF in every padding, bit order and byte order; either one gzip-compressed |
PNG images in bitmap tables and SVG documents are handed back as bytes rather
than decoded; for SVG documents, tables.svg.decompress inflates the
gzip-compressed ones on request.
WOFF and WOFF 2.0 files are unwrapped into the sfnt they were made from, which
is then decoded like any other. WOFF 2.0 compresses with Brotli, which the
standard library does not have, so it comes from
zig-brotli, the library's one
dependency. Its glyf, loca and hmtx transforms are undone, so the
rebuilt font has every glyph, point and instruction of the original, though
its glyf may pack them into different bytes. A WOFF2 collection is rebuilt as
a TrueType Collection, whose fonts Collection and Font.parseIndex decode as
they would from a .ttc.
Bitmap fonts
BDF and PCF fonts are not sfnt fonts, and Font does not take them.
BitmapFont.parse does, and decodes either format, gzip-compressed or not,
to the same glyphs:
pub fn drawA(gpa: std.mem.Allocator, bytes: []const u8) !void {
var f = try font.BitmapFont.parse(gpa, bytes, .{});
defer f.deinit();
const glyph = f.glyph('A') orelse f.defaultGlyph() orelse return;
for (0..glyph.bitmap.height) |y| {
for (0..glyph.bitmap.width) |x| {
const inked = glyph.bitmap.pixel(@intCast(x), @intCast(y)) != 0;
std.debug.print("{s}", .{if (inked) "#" else "."});
}
std.debug.print("\n", .{});
}
}
A glyph is its box relative to the origin, its advance, and its pixels, which
are handed back the same way whatever the file stored them as: most
significant bit first, each row starting on a byte. A PCF font may have been
compiled with any of four row paddings and any order of bits and bytes, and
that is undone here. Codes are looked up in the font's own encoding, which
its CHARSET_REGISTRY and CHARSET_ENCODING properties name, and are
Unicode code points only when those say ISO10646 and 1.
The font's properties are kept as the file has them, as integers and
strings, and so are the parts of each format the other has no place for:
BitmapFont.bdf holds a BDF font's version, comments, size and resolution,
and BitmapFont.pcf every table of a PCF font, ink metrics and accelerators
included.
How it works
Memory. A Font keeps everything in a single arena, and Font.deinit frees
it. By default parse copies the font's bytes into that arena first, so you
can free your own buffer as soon as parse returns. Fields that hold bytes,
such as hinting instructions, bitmap images, SVG documents and tables the
library does not decode, are slices of that copy rather than separate
allocations. With .copy = false the Font borrows your buffer instead, and
the buffer has to outlive it.
Broken fonts. head and maxp are required, and without them parse
fails. Every other table is optional: if one is present but cannot be decoded,
it is left null and recorded in Font.issues, and the rest of the font still
loads. Within a table, outlines work the same way: one malformed glyph is
recorded against that glyph and does not take the other 60,000 down with it.
Pass .strict = true to make any table failure fail the whole parse.
Hostile input. Every read goes through a bounds-checked cursor, so data that
runs past its end is error.Truncated rather than a panic. Counts are checked
against the bytes available before anything is allocated. Recursion such as
composite glyphs, charstring subroutines and the COLR paint graph is bounded
and checked for cycles. Subtables that a font may reference thousands of times,
such as coverage tables and paints, are decoded once and shared, so memory
stays proportional to the size of the file.
Outlines. Font.glyphPath returns any glyph's outline as a Path of
move, line, quadratic, cubic and close commands. It works for glyf, CFF
and CFF2 fonts alike, in font units with y pointing up. TrueType outlines
stay quadratic and CFF outlines stay cubic; neither is converted to the other.
Variations. Font.normalizeCoordinates converts user-space axis values,
such as wght=700, to the normalized coordinates the variation tables use.
It applies avar, and its integer arithmetic matches HarfBuzz exactly. Passing
those coordinates to glyphPath gives the outline at that point in the design
space: gvar deltas are applied for TrueType outlines, including interpolation
of untouched points, and CFF2 blends are applied for PostScript outlines. The
metric variation tables offer delta helpers of their own, for example
HVAR.advanceDelta and MVAR.delta.
Building and testing
The toolchain comes from Nix. nixpkgs does not carry Zig 0.17 yet, so the 0.17.0 release is taken from zig-overlay.
$ nix develop
$ zig build test --summary all # unit tests, then tests against real fonts
$ zig build test --fuzz # mutation fuzzing, until interrupted
$ zig build test --fuzz=1M # a bounded fuzzing run
$ zig build coverage # kcov report in zig-out/coverage
$ zig build docs # API reference in zig-out/docs
$ nix build .#zig-font # the package, tests included, in the sandbox
Every table has unit tests built from hand-made bytes. The fixture tests in
tests/ check decoded values against what fontTools reads from the same files.
Outlines are compared glyph by glyph with fontTools' pens, at the default
location and at other locations in variable fonts. A BDF font is text, and
is its own reference. Every PCF fixture is compiled from one of the BDF
fixtures by X.Org's bdftopcf, each in a different bit order, byte order and
padding, and has to decode to exactly the glyphs of the BDF it came from.
The fuzzer starts from the fixture fonts and edits them: it overwrites bytes
and sets fields to the values most likely to break a decoder. This keeps enough
of each file intact that the edits reach the decoders deep inside it. It then
decodes every table, draws every glyph and runs the lookups. Without --fuzz,
it runs each fixture once, unedited.
The test binaries are compiled by LLVM even in Debug builds. The self-hosted backend emits neither the coverage table the fuzzer steers by nor debug information that kcov can read.
Test fonts
tests/fonts/ contains small subsets of fonts released under the SIL Open Font
License, made by tools/make_fixtures.py from the fonts in nixpkgs. The OFL
does not allow a modified version to use a reserved font name, so subsets of
Source Sans and Anonymous Pro are renamed Fixture Sans and Fixture Mono. Two
fixtures are built from scratch: colr-v1-paints.ttf uses every COLRv1 paint
format, and sbix.ttf covers sbix, which no small OFL font in nixpkgs has.
The WOFF 2.0 fixtures are compressed from these, by fontTools and by
Google's reference encoder, so that the output of both is tested; only the
reference encoder can make a WOFF2 collection. The bitmap fonts are a subset
of Spleen, which is BSD-licensed and ships as BDF, and odd-metrics.bdf,
built from scratch with the awkward glyph boxes Spleen's uniform cells never
have; bdftopcf compiles both to PCF. REUSE.toml records the copyright and
license of each.
To regenerate them:
$ nix develop -c python3 tools/make_fixtures.py
Where this lives
git clone https://git.jcollie.dev/jeff/zig-font.git
It is on Radicle as
rad:z2z9bVWrjxkhQNdw3Uz6zgeTP92vd. A Radicle repository can only be found by
its identifier, so that is all a peer needs to fetch it:
rad clone rad:z2z9bVWrjxkhQNdw3Uz6zgeTP92vd
License
The code is MIT-licensed, and the project follows the
REUSE specification; reuse lint checks it. The
test fonts keep their own licenses, as recorded in REUSE.toml.
References cited
- Adobe Systems Incorporated. Glyph Bitmap Distribution Format (BDF) Specification. Technical Note #5005, version 2.2, 22 March 1993. https://adobe-type-tools.github.io/font-tech-notes/pdfs/5005.BDF_Spec.pdf
- Adobe Systems Incorporated. The Compact Font Format Specification. Technical Note #5176, version 1.0, 4 December 2003. https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
- Adobe Systems Incorporated. The Type 2 Charstring Format. Technical Note #5177, 16 March 2000. https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf
- Apple Computer, Inc. Map (external version) from Mac OS Roman character set to Unicode 2.1 and later. Unicode Consortium. https://www.unicode.org/Public/MAPPINGS/VENDORS/APPLE/ROMAN.TXT
- Apple Inc. TrueType Reference Manual. https://developer.apple.com/fonts/TrueType-Reference-Manual/
- Deutsch, P. and J-L. Gailly. ZLIB Compressed Data Format Specification version 3.3. RFC 1950, May 1996. https://www.rfc-editor.org/info/rfc1950
- Deutsch, P. GZIP file format specification version 4.3. RFC 1952, May 1996. https://www.rfc-editor.org/info/rfc1952
- Flowers, J. X Logical Font Description Conventions. Version 1.5, X Consortium Standard, X Version 11 Release 6.7. https://www.x.org/docs/XLFD/xlfd.pdf
- HarfBuzz project. avar2: OpenType avar table version 2. https://github.com/harfbuzz/boring-expansion-spec/blob/main/avar2.md
- Alakuijala, J. and Z. Szabadka. Brotli Compressed Data Format. RFC 7932, July 2016. https://www.rfc-editor.org/info/rfc7932
- Kew, J., T. Leming and E. van Blokland. WOFF File Format 1.0. W3C Recommendation, 13 December 2012. https://www.w3.org/TR/WOFF/
- Levantovsky, V. and R. Levien. WOFF File Format 2.0. W3C Recommendation, 8 August 2024. https://www.w3.org/TR/WOFF2/
- Microsoft Corporation. OpenType Specification, version 1.9.1, May 2024. https://learn.microsoft.com/en-us/typography/opentype/spec/
- X.Org Foundation. libXfont,
src/bitmap/pcfread.c. PCF has no published specification; this reader is the definition in practice. https://gitlab.freedesktop.org/xorg/lib/libxfont/-/blob/master/src/bitmap/pcfread.c