font: add glyf entry decoder to outline

Add Glyf.Outline for decoding the contours and points of a Glyf.
pull/12893/head
Mitchell Hashimoto 2026-06-01 13:34:00 -07:00
parent 5758e14931
commit d8f56b790e
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
1 changed files with 446 additions and 0 deletions

View File

@ -1,4 +1,5 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const sfnt = @import("sfnt.zig");
/// Glyph Data Table
@ -15,6 +16,49 @@ const sfnt = @import("sfnt.zig");
pub const Glyf = struct {
data: []const u8,
/// A decoded glyph outline.
///
/// The `countours` slice is the list of end point indices and
/// `points` owns all the points. Glyf guarantees that contour
/// points are sequential so we can just store the end and calculate
/// the points that way. Use the helpers to make it ergonomic.
pub const Outline = struct {
/// List of contour end points. Calculate the full list of
/// points using points[prev...this+1]
contours: []sfnt.uint16,
/// The backing storage of all points in the entry.
points: []Point,
/// A single decoded point in a simple glyph contour.
pub const Point = struct {
x: i32,
y: i32,
on_curve: bool,
};
/// Return the point slice for the contour at `index`.
///
/// The returned slice references `points` and is invalidated when
/// this outline is deinitialized.
pub fn contour(self: Outline, index: usize) []Point {
const start = if (index == 0)
0
else
@as(usize, self.contours[index - 1]) + 1;
const end = @as(usize, self.contours[index]) + 1;
return self.points[start..end];
}
/// Free all memory owned by this outline. Pass in the same
/// allocator used for decoding.
pub fn deinit(self: *Outline, alloc: Allocator) void {
alloc.free(self.contours);
alloc.free(self.points);
self.* = undefined;
}
};
/// https://learn.microsoft.com/en-us/typography/opentype/spec/glyf#table-organization
pub const Entry = struct {
header: Header,
@ -241,6 +285,12 @@ pub const Glyf = struct {
TooManyPoints,
};
/// Errors that can be returned from `Entry.decode()`.
pub const DecodeError = SizeError || Allocator.Error || error{
/// Coordinate delta accumulation overflowed.
CoordinateOverflow,
};
/// Determines the size (in bytes) of this entry.
///
/// If the entry is valid, returns the number of bytes
@ -393,6 +443,149 @@ pub const Glyf = struct {
// No issues found, the glyf entry is valid, return its length.
return @sizeOf(Header) + fbs.pos;
}
/// Decode this simple glyph entry into an owned outline.
///
/// NOTE: Currently produces errors when given composite glyphs
/// or any glyphs that have hinting instructions included.
pub fn decode(self: Entry, alloc: Allocator) DecodeError!Glyf.Outline {
// We only support simple glyphs.
switch (self.entryType()) {
.simple => {},
.composite => return error.CompositeNotSupported,
}
var fbs = std.io.fixedBufferStream(self.data);
const reader = fbs.reader();
// A zero-contour glyph may be header-only. See size for the
// reason for the hardcoded 2 here.
const num_contours: usize = @intCast(self.header.numberOfContours);
if (num_contours == 0 and self.data.len < 2) return .{
.points = &.{},
.contours = &.{},
};
// We now know our full amount of contour ending points.
const end_points = try alloc.alloc(sfnt.uint16, num_contours);
errdefer alloc.free(end_points);
// If we have no contours, then the only possible remaining
// field is instructionLength. Instructions are not supported.
if (num_contours == 0) {
const instructions_length = try reader.readInt(sfnt.uint16, .big);
if (instructions_length > 0) return error.InstructionsNotSupported;
return .{ .points = &.{}, .contours = end_points };
}
// The number of points is determined by the final end point
// entry since the entries have to be monotonic (something
// we verify below).
const point_count: usize = point_count: {
var prev_end_point: isize = -1;
// Go through the end points array and update our end_points
// with the valid index. The final endpoint tells us our point
// count, since endpoints are stored as inclusive point indices.
for (0..end_points.len) |i| {
const index = try reader.readInt(sfnt.uint16, .big);
if (index <= prev_end_point) return error.EndPointsOutOfOrder;
prev_end_point = index;
end_points[i] = index;
}
// The final point tells us our point count.
break :point_count @as(usize, end_points[end_points.len - 1]) + 1;
};
// Instructions are not supported.
const instructions_length = try reader.readInt(sfnt.uint16, .big);
if (instructions_length > 0) return error.InstructionsNotSupported;
// Allocate our points right away even though the next entries
// are flags. We want to do this so that if the allocator is
// a bump allocator, the flags free will actually free it.
const points = try alloc.alloc(Glyf.Outline.Point, point_count);
errdefer alloc.free(points);
// This is EXTREMELY annoying but all the flags are separate
// from the points so we have to do some allocation here since
// its a dynamic amount and we need to save the values for later.
//
// Typical glyphs have small point counts, so use stack storage
// first while still falling back to the caller's allocator for
// unusually large outlines.
var flags_stack = std.heap.stackFallback(4096, alloc);
const flags_alloc = flags_stack.get();
const flags = try flags_alloc.alloc(SimpleFlags, point_count);
defer flags_alloc.free(flags);
{
var point_i: usize = 0;
while (point_i < point_count) {
const flag: SimpleFlags = @bitCast(try reader.readByte());
flags[point_i] = flag;
point_i += 1;
if (flag.repeat) {
const repeat_count: usize = try reader.readByte();
if (point_i + repeat_count > point_count) return error.TooManyPoints;
for (0..repeat_count) |_| {
flags[point_i] = flag;
point_i += 1;
}
}
}
}
// Go through x coordinate deltas
var x: i32 = 0;
for (flags, points) |flag, *point| {
const dx: i32 = if (flag.x_short) short: {
break :short if (flag.x_repeat_or_sign)
@as(i32, try reader.readByte())
else
-@as(i32, try reader.readByte());
} else if (!flag.x_repeat_or_sign)
@as(i32, try reader.readInt(sfnt.int16, .big))
else
0;
x = std.math.add(
i32,
x,
dx,
) catch return error.CoordinateOverflow;
point.x = x;
}
// Go through y coordinate deltas
var y: i32 = 0;
for (flags, points) |flag, *point| {
const dy: i32 = if (flag.y_short) short: {
break :short if (flag.y_repeat_or_sign)
@as(i32, try reader.readByte())
else
-@as(i32, try reader.readByte());
} else if (!flag.y_repeat_or_sign)
@as(i32, try reader.readInt(sfnt.int16, .big))
else
0;
y = std.math.add(
i32,
y,
dy,
) catch return error.CoordinateOverflow;
point.y = y;
point.on_curve = flag.on_curve;
}
return .{
.points = points,
.contours = end_points,
};
}
};
/// Initialize the table from the provided data.
@ -451,6 +644,33 @@ pub fn getGlyph(font: sfnt.SFNT, index: usize) !struct { usize, Glyf.Entry } {
return .{ end_offset - start_offset, try glyf.entry(start_offset) };
}
fn testAppendInt(
buf: *std.ArrayList(u8),
alloc: Allocator,
comptime T: type,
value: T,
) !void {
var bytes: [@sizeOf(T)]u8 = undefined;
std.mem.writeInt(T, &bytes, value, .big);
try buf.appendSlice(alloc, &bytes);
}
fn testAppendHeader(
buf: *std.ArrayList(u8),
alloc: Allocator,
number_of_contours: i16,
x_min: i16,
y_min: i16,
x_max: i16,
y_max: i16,
) !void {
try testAppendInt(buf, alloc, i16, number_of_contours);
try testAppendInt(buf, alloc, i16, x_min);
try testAppendInt(buf, alloc, i16, y_min);
try testAppendInt(buf, alloc, i16, x_max);
try testAppendInt(buf, alloc, i16, y_max);
}
test "glyf" {
const testing = std.testing;
const alloc = testing.allocator;
@ -475,12 +695,184 @@ test "glyf" {
try testing.expect(glyph_A.entryType() == .simple);
try testing.expect(len_A >= try glyph_A.size());
var outline_A = try glyph_A.decode(alloc);
defer outline_A.deinit(alloc);
try testing.expectEqual(@as(usize, @intCast(glyph_A.header.numberOfContours)), outline_A.contours.len);
try testing.expect(outline_A.points.len > 0);
// Glyph "Ĩ" is at index 265.
const len_Itilde, const glyph_Itilde = try getGlyph(font, 265);
try testing.expect(glyph_Itilde.entryType() == .simple);
try testing.expect(len_Itilde >= try glyph_Itilde.size());
}
test "glyf: decode triangle" {
const testing = std.testing;
const alloc = testing.allocator;
var buf: std.ArrayList(u8) = .empty;
defer buf.deinit(alloc);
try testAppendHeader(&buf, alloc, 1, 100, 100, 900, 900);
try testAppendInt(&buf, alloc, u16, 2); // endPtsOfContours[0]
try testAppendInt(&buf, alloc, u16, 0); // instructionLength
try buf.append(alloc, 0x01); // on curve
try buf.append(alloc, 0x01); // on curve
try buf.append(alloc, 0x01); // on curve
try testAppendInt(&buf, alloc, i16, 500);
try testAppendInt(&buf, alloc, i16, -400);
try testAppendInt(&buf, alloc, i16, 800);
try testAppendInt(&buf, alloc, i16, 900);
try testAppendInt(&buf, alloc, i16, -800);
try testAppendInt(&buf, alloc, i16, 0);
const glyph = try Glyf.Entry.init(buf.items);
var outline = try glyph.decode(alloc);
defer outline.deinit(alloc);
try testing.expectEqual(@as(i16, 100), glyph.header.xMin);
try testing.expectEqual(@as(i16, 900), glyph.header.xMax);
try testing.expectEqual(@as(usize, 1), outline.contours.len);
try testing.expectEqual(@as(usize, 3), outline.points.len);
const contour = outline.contour(0);
try testing.expectEqual(@as(usize, 3), contour.len);
try testing.expectEqual(Glyf.Outline.Point{ .x = 500, .y = 900, .on_curve = true }, contour[0]);
try testing.expectEqual(Glyf.Outline.Point{ .x = 100, .y = 100, .on_curve = true }, contour[1]);
try testing.expectEqual(Glyf.Outline.Point{ .x = 900, .y = 100, .on_curve = true }, contour[2]);
}
test "glyf: decode multiple contours" {
const testing = std.testing;
const alloc = testing.allocator;
var buf: std.ArrayList(u8) = .empty;
defer buf.deinit(alloc);
try testAppendHeader(&buf, alloc, 2, 0, 0, 30, 10);
try testAppendInt(&buf, alloc, u16, 1); // first contour ends at point 1
try testAppendInt(&buf, alloc, u16, 3); // second contour ends at point 3
try testAppendInt(&buf, alloc, u16, 0); // instructionLength
for (0..4) |_| try buf.append(alloc, 0x01); // on curve
for ([_]i16{ 0, 10, 10, 10 }) |dx| try testAppendInt(&buf, alloc, i16, dx);
for ([_]i16{ 0, 0, 10, 0 }) |dy| try testAppendInt(&buf, alloc, i16, dy);
const glyph = try Glyf.Entry.init(buf.items);
var outline = try glyph.decode(alloc);
defer outline.deinit(alloc);
try testing.expectEqual(@as(usize, 2), outline.contours.len);
try testing.expectEqual(@as(usize, 4), outline.points.len);
try testing.expectEqual(@as(u16, 1), outline.contours[0]);
try testing.expectEqual(@as(u16, 3), outline.contours[1]);
try testing.expectEqual(@as(usize, 2), outline.contour(0).len);
try testing.expectEqual(@as(usize, 2), outline.contour(1).len);
try testing.expectEqual(outline.points[0..2].ptr, outline.contour(0).ptr);
try testing.expectEqual(outline.points[2..4].ptr, outline.contour(1).ptr);
}
test "glyf: decode repeat and short vector flags" {
const testing = std.testing;
const alloc = testing.allocator;
var buf: std.ArrayList(u8) = .empty;
defer buf.deinit(alloc);
try testAppendHeader(&buf, alloc, 1, 0, -16, 16, 0);
try testAppendInt(&buf, alloc, u16, 3); // four points
try testAppendInt(&buf, alloc, u16, 0); // instructionLength
try buf.append(alloc, 0x01 | 0x02 | 0x04 | 0x08 | 0x10); // on, x short positive, y short negative, repeat
try buf.append(alloc, 3); // repeat for the next three points
for ([_]u8{ 1, 2, 4, 8 }) |dx| try buf.append(alloc, dx);
for ([_]u8{ 1, 2, 4, 8 }) |dy| try buf.append(alloc, dy);
const glyph = try Glyf.Entry.init(buf.items);
var outline = try glyph.decode(alloc);
defer outline.deinit(alloc);
try testing.expectEqual(@as(usize, 4), outline.points.len);
try testing.expectEqual(Glyf.Outline.Point{ .x = 1, .y = -1, .on_curve = true }, outline.points[0]);
try testing.expectEqual(Glyf.Outline.Point{ .x = 3, .y = -3, .on_curve = true }, outline.points[1]);
try testing.expectEqual(Glyf.Outline.Point{ .x = 7, .y = -7, .on_curve = true }, outline.points[2]);
try testing.expectEqual(Glyf.Outline.Point{ .x = 15, .y = -15, .on_curve = true }, outline.points[3]);
}
test "glyf: decode off curve and same coordinate flags" {
const testing = std.testing;
const alloc = testing.allocator;
var buf: std.ArrayList(u8) = .empty;
defer buf.deinit(alloc);
try testAppendHeader(&buf, alloc, 1, 0, 0, 7, 9);
try testAppendInt(&buf, alloc, u16, 1); // two points
try testAppendInt(&buf, alloc, u16, 0); // instructionLength
try buf.append(alloc, 0x10 | 0x20); // off curve, x same, y same
try buf.append(alloc, 0x01 | 0x02 | 0x04 | 0x10 | 0x20); // on curve, short positive x/y
try buf.append(alloc, 7); // x delta
try buf.append(alloc, 9); // y delta
const glyph = try Glyf.Entry.init(buf.items);
var outline = try glyph.decode(alloc);
defer outline.deinit(alloc);
try testing.expectEqual(Glyf.Outline.Point{ .x = 0, .y = 0, .on_curve = false }, outline.points[0]);
try testing.expectEqual(Glyf.Outline.Point{ .x = 7, .y = 9, .on_curve = true }, outline.points[1]);
}
test "glyf: decode one-point contour" {
const testing = std.testing;
const alloc = testing.allocator;
var buf: std.ArrayList(u8) = .empty;
defer buf.deinit(alloc);
try testAppendHeader(&buf, alloc, 1, 0, 0, 0, 0);
try testAppendInt(&buf, alloc, u16, 0); // endPtsOfContours[0]
try testAppendInt(&buf, alloc, u16, 0); // instructionLength
try buf.append(alloc, 0x01 | 0x10 | 0x20); // on curve, x same, y same
const glyph = try Glyf.Entry.init(buf.items);
var outline = try glyph.decode(alloc);
defer outline.deinit(alloc);
try testing.expectEqual(@as(usize, 1), outline.points.len);
try testing.expectEqual(@as(usize, 1), outline.contours.len);
try testing.expectEqual(@as(u16, 0), outline.contours[0]);
try testing.expectEqual(Glyf.Outline.Point{ .x = 0, .y = 0, .on_curve = true }, outline.contour(0)[0]);
}
test "glyf: decode contour ending at max point index" {
const testing = std.testing;
const alloc = testing.allocator;
var buf: std.ArrayList(u8) = .empty;
defer buf.deinit(alloc);
try testAppendHeader(&buf, alloc, 1, 0, 0, 0, 0);
try testAppendInt(&buf, alloc, u16, std.math.maxInt(u16)); // 65536 points
try testAppendInt(&buf, alloc, u16, 0); // instructionLength
const flag = 0x01 | 0x10 | 0x20; // on curve, x same, y same
var remaining: usize = @as(usize, std.math.maxInt(u16)) + 1;
while (remaining > 0) {
const run = @min(remaining, 256);
if (run == 1) {
try buf.append(alloc, flag);
} else {
try buf.append(alloc, flag | 0x08); // repeat
try buf.append(alloc, @intCast(run - 1));
}
remaining -= run;
}
const glyph = try Glyf.Entry.init(buf.items);
var outline = try glyph.decode(alloc);
defer outline.deinit(alloc);
try testing.expectEqual(@as(usize, 65536), outline.points.len);
try testing.expectEqual(@as(usize, 65536), outline.contour(0).len);
}
test "glyf: reject glyphs with instructions and composite glyphs" {
const testing = std.testing;
const alloc = testing.allocator;
@ -496,6 +888,10 @@ test "glyf: reject glyphs with instructions and composite glyphs" {
Glyf.Entry.SizeError.InstructionsNotSupported,
glyph_notdef.size(),
);
try testing.expectError(
Glyf.Entry.DecodeError.InstructionsNotSupported,
glyph_notdef.decode(alloc),
);
// Glyph "Á" is at index 2.
const len_Aacute, const glyph_Aacute = try getGlyph(font, 2);
@ -505,6 +901,10 @@ test "glyf: reject glyphs with instructions and composite glyphs" {
Glyf.Entry.SizeError.CompositeNotSupported,
glyph_Aacute.size(),
);
try testing.expectError(
Glyf.Entry.DecodeError.CompositeNotSupported,
glyph_Aacute.decode(alloc),
);
}
test "glyf: reject truncated" {
@ -522,6 +922,7 @@ test "glyf: reject truncated" {
// it before the full length (which is 228 bytes).
glyph_nul.data = glyph_nul.data[0 .. 227 - @sizeOf(Glyf.Entry.Header)];
try testing.expectError(Glyf.Entry.SizeError.EndOfStream, glyph_nul.size());
try testing.expectError(Glyf.Entry.DecodeError.EndOfStream, glyph_nul.decode(alloc));
}
test "glyf: reject endpoints out of order" {
@ -544,6 +945,10 @@ test "glyf: reject endpoints out of order" {
// copied, we can just const cast it back to mutable lol.
std.mem.bytesAsSlice(u16, @as([]u8, @constCast(glyph_nul.data)))[3] = 0;
try testing.expectError(Glyf.Entry.SizeError.EndPointsOutOfOrder, glyph_nul.size());
try testing.expectError(
Glyf.Entry.DecodeError.EndPointsOutOfOrder,
glyph_nul.decode(alloc),
);
}
test "glyf: reject too many points" {
@ -568,10 +973,12 @@ test "glyf: reject too many points" {
@as([]u8, @constCast(glyph_nul.data))[107] |= 0x08;
@as([]u8, @constCast(glyph_nul.data))[108] = 0xFF;
try testing.expectError(Glyf.Entry.SizeError.TooManyPoints, glyph_nul.size());
try testing.expectError(Glyf.Entry.DecodeError.TooManyPoints, glyph_nul.decode(alloc));
}
test "glyf: zero-contour glyph can be header-only" {
const testing = std.testing;
const alloc = testing.allocator;
const header: Glyf.Entry.Header = .{
.numberOfContours = 0,
@ -582,4 +989,43 @@ test "glyf: zero-contour glyph can be header-only" {
};
const glyph = try Glyf.Entry.init(std.mem.asBytes(&header));
try testing.expectEqual(@sizeOf(Glyf.Entry.Header), try glyph.size());
var outline = try glyph.decode(alloc);
defer outline.deinit(alloc);
try testing.expectEqual(@as(usize, 0), outline.points.len);
try testing.expectEqual(@as(usize, 0), outline.contours.len);
}
test "glyf: zero-contour glyph can include instruction length" {
const testing = std.testing;
const alloc = testing.allocator;
var buf: std.ArrayList(u8) = .empty;
defer buf.deinit(alloc);
try testAppendHeader(&buf, alloc, 0, 0, 0, 0, 0);
try testAppendInt(&buf, alloc, u16, 0); // instructionLength
const glyph = try Glyf.Entry.init(buf.items);
try testing.expectEqual(@sizeOf(Glyf.Entry.Header) + 2, try glyph.size());
var outline = try glyph.decode(alloc);
defer outline.deinit(alloc);
try testing.expectEqual(@as(usize, 0), outline.points.len);
try testing.expectEqual(@as(usize, 0), outline.contours.len);
}
test "glyf: zero-contour glyph rejects instructions" {
const testing = std.testing;
const alloc = testing.allocator;
var buf: std.ArrayList(u8) = .empty;
defer buf.deinit(alloc);
try testAppendHeader(&buf, alloc, 0, 0, 0, 0, 0);
try testAppendInt(&buf, alloc, u16, 1); // instructionLength
const glyph = try Glyf.Entry.init(buf.items);
try testing.expectError(Glyf.Entry.SizeError.InstructionsNotSupported, glyph.size());
try testing.expectError(Glyf.Entry.DecodeError.InstructionsNotSupported, glyph.decode(alloc));
}