mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-06-15 16:13:56 +00:00
font: glyf outline decoder and rasterizer (#12893)
This adds a Glyf outline decoder and rasterizer. So it turns out that FreeType and CoreText have very shitty APIs for raw Glyf table rasterization. CoreText as far as I can find can't do it at all. In both cases you have to create a synthetic font with just this entry and rasterize the glyph. And the code to do all that was WAYYYYYY complex such that this made way more sense. We need this for the Glyph Protocol. **AI disclosure:** Hand-written parser, rasterizer. AI assisted validation and test writing. I read the spec myself. cc @qwerasd205
This commit is contained in:
836
src/font/glyf_rasterize.zig
Normal file
836
src/font/glyf_rasterize.zig
Normal file
@@ -0,0 +1,836 @@
|
||||
//! Rasterization for OpenType glyf outlines.
|
||||
//!
|
||||
//! This module intentionally lives in `font` rather than `font/opentype`
|
||||
//! because I wanted to keep `font/opentype` dependency free on the font
|
||||
//! package.
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const z2d = @import("z2d");
|
||||
|
||||
const face = @import("face.zig");
|
||||
const glyf = @import("opentype/glyf.zig");
|
||||
|
||||
/// Metrics describing the authored glyf coordinate space, since
|
||||
/// a glyf table doesn't contain this on its own.
|
||||
pub const DesignMetrics = struct {
|
||||
/// Units-per-em for outline/design coordinates.
|
||||
units_per_em: u32,
|
||||
|
||||
/// Authored advance width in design units.
|
||||
advance_width: u32,
|
||||
|
||||
/// Authored line height in design units.
|
||||
line_height: u32,
|
||||
};
|
||||
|
||||
/// An owned, tightly packed alpha8 bitmap.
|
||||
pub const Bitmap = struct {
|
||||
width: u32,
|
||||
height: u32,
|
||||
data: []u8,
|
||||
|
||||
// An empty 0x0 bitmap.
|
||||
pub const empty: Bitmap = .{ .width = 0, .height = 0, .data = "" };
|
||||
|
||||
pub fn initEmpty(alloc: Allocator, width: u32, height: u32) Allocator.Error!Bitmap {
|
||||
const data = try alloc.alloc(u8, @as(usize, width) * @as(usize, height));
|
||||
@memset(data, 0);
|
||||
return .{ .width = width, .height = height, .data = data };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Bitmap, alloc: Allocator) void {
|
||||
alloc.free(self.data);
|
||||
self.* = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Error = Allocator.Error || z2d.Path.Error || z2d.painter.FillError;
|
||||
|
||||
/// Rasterize a decoded glyf outline to a full-cell alpha bitmap.
|
||||
///
|
||||
/// The returned bitmap is always `grid_metrics.cell_width * cell_width` by
|
||||
/// `grid_metrics.cell_height`. `opts.constraint` is applied using the same
|
||||
/// `face.RenderOptions.Constraint` machinery used by the platform font
|
||||
/// backends.
|
||||
///
|
||||
/// The caller owns the returned bitmap.
|
||||
pub fn rasterize(
|
||||
alloc: Allocator,
|
||||
outline: glyf.Glyf.Outline,
|
||||
design: DesignMetrics,
|
||||
opts: face.RenderOptions,
|
||||
) Error!Bitmap {
|
||||
assert(design.units_per_em > 0);
|
||||
assert(design.advance_width > 0);
|
||||
assert(design.line_height > 0);
|
||||
|
||||
// Calculate our final width/height.
|
||||
const width: u32 = std.math.mul(
|
||||
u32,
|
||||
opts.grid_metrics.cell_width,
|
||||
opts.cell_width orelse 1,
|
||||
) catch std.math.maxInt(u32);
|
||||
const height = opts.grid_metrics.cell_height;
|
||||
assert(width > 0 and height > 0);
|
||||
|
||||
// If we have no contours or points then we have no drawable shape, but the
|
||||
// caller still asked for a cell-sized bitmap. Return that full bitmap with
|
||||
// zero coverage so downstream atlas/upload code doesn't need a separate
|
||||
// size contract for empty glyphs.
|
||||
if (outline.contours.len == 0 or outline.points.len == 0) return Bitmap.initEmpty(alloc, width, height);
|
||||
|
||||
// Glyf entries have a header bounding box, but this rasterizer operates on
|
||||
// the decoded Outline only. Recompute bounds from the decoded coordinate
|
||||
// data so placement and scaling follow the geometry we actually draw. If a
|
||||
// source glyf header disagrees with its points, the point data is the safer
|
||||
// source of truth for rasterization; the header belongs in decode-time
|
||||
// validation/metadata, not this font-level drawing API.
|
||||
const bounds: Bounds = bounds: {
|
||||
var bounds: Bounds = .{
|
||||
.x_min = @floatFromInt(outline.points[0].x),
|
||||
.y_min = @floatFromInt(outline.points[0].y),
|
||||
.x_max = @floatFromInt(outline.points[0].x),
|
||||
.y_max = @floatFromInt(outline.points[0].y),
|
||||
};
|
||||
for (outline.points[1..]) |p| {
|
||||
const x: f64 = @floatFromInt(p.x);
|
||||
const y: f64 = @floatFromInt(p.y);
|
||||
bounds.x_min = @min(bounds.x_min, x);
|
||||
bounds.y_min = @min(bounds.y_min, y);
|
||||
bounds.x_max = @max(bounds.x_max, x);
|
||||
bounds.y_max = @max(bounds.y_max, y);
|
||||
}
|
||||
break :bounds bounds;
|
||||
};
|
||||
|
||||
// Degenerate point bounds can't produce filled area and would make the
|
||||
// point-to-bitmap transform divide by zero, so return a full transparent
|
||||
// bitmap just like an empty outline.
|
||||
if (bounds.width() == 0 or bounds.height() == 0) return Bitmap.initEmpty(alloc, width, height);
|
||||
|
||||
// Build the surface we'll draw on. This is a simple alpha8 drawing.
|
||||
var sfc: z2d.Surface = try .init(
|
||||
.image_surface_alpha8,
|
||||
alloc,
|
||||
@intCast(width),
|
||||
@intCast(height),
|
||||
);
|
||||
defer sfc.deinit(alloc);
|
||||
|
||||
var path: z2d.Path = .empty;
|
||||
defer path.deinit(alloc);
|
||||
|
||||
const placement: Placement = .init(bounds, design, opts);
|
||||
for (0..outline.contours.len) |i| try appendContourPath(
|
||||
alloc,
|
||||
&path,
|
||||
outline.contour(i),
|
||||
bounds,
|
||||
placement,
|
||||
);
|
||||
|
||||
try z2d.painter.fill(
|
||||
alloc,
|
||||
&sfc,
|
||||
&.{ .opaque_pattern = .{
|
||||
.pixel = .{ .alpha8 = .{ .a = 255 } },
|
||||
} },
|
||||
path.nodes.items,
|
||||
.{},
|
||||
);
|
||||
|
||||
return .{
|
||||
.width = width,
|
||||
.height = height,
|
||||
.data = try alloc.dupe(u8, std.mem.sliceAsBytes(sfc.image_surface_alpha8.buf)),
|
||||
};
|
||||
}
|
||||
|
||||
const Bounds = struct {
|
||||
x_min: f64,
|
||||
y_min: f64,
|
||||
x_max: f64,
|
||||
y_max: f64,
|
||||
|
||||
fn width(self: Bounds) f64 {
|
||||
return self.x_max - self.x_min;
|
||||
}
|
||||
|
||||
fn height(self: Bounds) f64 {
|
||||
return self.y_max - self.y_min;
|
||||
}
|
||||
};
|
||||
|
||||
/// Cell-relative pixel rectangle where the decoded outline bounds should be
|
||||
/// rasterized within the output bitmap.
|
||||
///
|
||||
/// This is deliberately the placement of the outline's computed point bounds,
|
||||
/// not the full declared advance/line-height box. `advance_width` and
|
||||
/// `line_height` describe the design-space layout box the outline was drawn
|
||||
/// within: they include intentional bearings and whitespace around the visible
|
||||
/// points. We use that declared box when applying `RenderOptions.Constraint` so
|
||||
/// sizing and alignment preserve those bearings consistently with other font
|
||||
/// backends; once that is resolved, we rasterize only the actual outline bounds
|
||||
/// into this rectangle.
|
||||
///
|
||||
/// ```text
|
||||
/// output bitmap / terminal cell
|
||||
/// ╭────────────────────────────────────────────────────────────────────────╮ top
|
||||
/// │ │
|
||||
/// │ declared advance/line-height box │
|
||||
/// │ (outer layout box used for constraints) │
|
||||
/// │ ╭────────────────────────────────────────────────────────────────╮ │
|
||||
/// │ │ │ │
|
||||
/// │◀──────── x ────────▶╭────────── width ──────────╮ │ │
|
||||
/// │ │ │ Placement │ ▲ height │ │
|
||||
/// │ │ │ outline point bounds │ │ │ │
|
||||
/// │ │ │ pixels to draw │ │ │ │
|
||||
/// │ │ │ │ │ │ │
|
||||
/// │ │ ╰───────────────────────────╯ ▼ │ │
|
||||
/// │ ╰─────────────────▲──────────────────────────────────────────────╯ │
|
||||
/// │ │ y │
|
||||
/// ╰────────────────────────────────────────────────────────────────────────╯ bottom
|
||||
/// x is measured from the bitmap left to the Placement left.
|
||||
/// y is measured from the bitmap bottom to the Placement bottom.
|
||||
/// bitmap_height is the full top-to-bottom bitmap height.
|
||||
/// ```
|
||||
///
|
||||
/// Constraints are applied to the outer box so the whitespace remains part of
|
||||
/// alignment decisions. `Placement` is the inner rectangle after that outer box
|
||||
/// has been constrained.
|
||||
const Placement = struct {
|
||||
/// Left edge of the rasterized outline bounds in bitmap pixels, measured
|
||||
/// from the bitmap's left edge.
|
||||
x: f64,
|
||||
|
||||
/// Bottom edge of the rasterized outline bounds in bitmap pixels, measured
|
||||
/// from the bitmap's bottom edge. This matches the cell-relative y axis
|
||||
/// used by font.face.GlyphSize and is converted to z2d's y-down axis when
|
||||
/// points are transformed.
|
||||
y: f64,
|
||||
|
||||
/// Width of the rasterized outline bounds in bitmap pixels after applying
|
||||
/// font.face.RenderOptions.Constraint.
|
||||
width: f64,
|
||||
|
||||
/// Height of the rasterized outline bounds in bitmap pixels after applying
|
||||
/// font.face.RenderOptions.Constraint.
|
||||
height: f64,
|
||||
|
||||
/// Full bitmap height in pixels, used to convert cell-relative y-up-ish
|
||||
/// placement into the y-down coordinate system used by z2d surfaces.
|
||||
bitmap_height: f64,
|
||||
|
||||
/// Calculate where the decoded point bounds should land in the output
|
||||
/// bitmap.
|
||||
///
|
||||
/// The glyf protocol supplies declared metrics (`units_per_em`,
|
||||
/// `advance_width`, and `line_height`) in design units, while Ghostty's
|
||||
/// font constraint code works in cell-relative pixels. We first map the em
|
||||
/// square to one cell height, matching the linked glyph rasterizer's
|
||||
/// baseline model where design-space `y=0` is the bottom/baseline of the em
|
||||
/// and `y=units_per_em` is its top. Then we describe the actual outline
|
||||
/// bounds as a relative sub-rectangle of the declared advance/line-height
|
||||
/// box. That declared box includes any intentional side bearings or
|
||||
/// vertical whitespace around the outline; constraints should apply to that
|
||||
/// layout box rather than to the tight point bounds alone. This returns the
|
||||
/// final pixel rectangle for only the outline bounds that we will rasterize.
|
||||
fn init(
|
||||
bounds: Bounds,
|
||||
design: DesignMetrics,
|
||||
opts: face.RenderOptions,
|
||||
) Placement {
|
||||
// Start with protocol-like design units mapped so that the em square
|
||||
// occupies one cell. This makes units_per_em the scale reference and
|
||||
// preserves the linked rasterizer's y=0 baseline/bottom behavior.
|
||||
// Callers can then use RenderOptions.Constraint to fit/cover/stretch/
|
||||
// align the declared advance/line-height box using existing font logic.
|
||||
const scale = @as(f64, @floatFromInt(opts.grid_metrics.cell_height)) /
|
||||
@as(f64, @floatFromInt(design.units_per_em));
|
||||
|
||||
// Convert the decoded point bounds into the same pixel coordinate space
|
||||
// expected by RenderOptions.Constraint. This rectangle is the visible
|
||||
// outline bounds, not the full advance/line-height layout box.
|
||||
const glyph: face.GlyphSize = .{
|
||||
.width = bounds.width() * scale,
|
||||
.height = bounds.height() * scale,
|
||||
.x = bounds.x_min * scale,
|
||||
.y = bounds.y_min * scale,
|
||||
};
|
||||
|
||||
// Convert the declared layout box to pixels. This is the box that
|
||||
// carries intentional bearings/whitespace and should be constrained.
|
||||
const group_width = @as(f64, @floatFromInt(design.advance_width)) * scale;
|
||||
const group_height = @as(f64, @floatFromInt(design.line_height)) * scale;
|
||||
|
||||
// Apply the same fit/cover/stretch/alignment/padding rules used by
|
||||
// normal font rendering. The result is still the outline bounds, but
|
||||
// placed as if its containing advance/line-height box was constrained.
|
||||
const constraint: face.RenderOptions.Constraint = constraint: {
|
||||
var constraint = opts.constraint;
|
||||
if (group_width > 0 and group_height > 0) {
|
||||
// Tell Constraint that `glyph` is a sub-rectangle of the
|
||||
// declared layout box. Constraint will size/align the outer box
|
||||
// and then return the corresponding transformed inner box.
|
||||
constraint.relative_width = glyph.width / group_width;
|
||||
constraint.relative_height = glyph.height / group_height;
|
||||
constraint.relative_x = glyph.x / group_width;
|
||||
constraint.relative_y = glyph.y / group_height;
|
||||
}
|
||||
break :constraint constraint;
|
||||
};
|
||||
const constrained = constraint.constrain(
|
||||
glyph,
|
||||
opts.grid_metrics,
|
||||
opts.constraint_width,
|
||||
);
|
||||
|
||||
// Store the final outline placement plus the full bitmap height needed
|
||||
// later to flip from cell-relative y to z2d's y-down surface space.
|
||||
return .{
|
||||
.x = constrained.x,
|
||||
.y = constrained.y,
|
||||
.width = constrained.width,
|
||||
.height = constrained.height,
|
||||
.bitmap_height = @floatFromInt(opts.grid_metrics.cell_height),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const Point = struct {
|
||||
x: f64,
|
||||
y: f64,
|
||||
};
|
||||
|
||||
/// Append one contour to a z2d path.
|
||||
///
|
||||
/// Glyf contours are quadratic outlines with explicit on-curve points and
|
||||
/// off-curve control points. Consecutive off-curve points imply an on-curve
|
||||
/// point halfway between them, and a contour may begin with an off-curve point.
|
||||
/// This normalizes those cases while walking the closed contour and emits z2d
|
||||
/// line/cubic-curve operations in bitmap coordinates.
|
||||
fn appendContourPath(
|
||||
alloc: Allocator,
|
||||
path: *z2d.Path,
|
||||
contour: []const glyf.Glyf.Outline.Point,
|
||||
bounds: Bounds,
|
||||
placement: Placement,
|
||||
) Error!void {
|
||||
if (contour.len == 0) return;
|
||||
|
||||
const first = contour[0];
|
||||
const last = contour[contour.len - 1];
|
||||
|
||||
var current: Point = undefined;
|
||||
var i: usize = 0;
|
||||
|
||||
// Choose the starting on-curve point for this closed contour. If the first
|
||||
// point is off-curve then the contour logically starts either at the final
|
||||
// on-curve point, or at the implied midpoint between the final and first
|
||||
// off-curve points.
|
||||
if (first.on_curve) {
|
||||
i = 1;
|
||||
current = transformPoint(
|
||||
first,
|
||||
bounds,
|
||||
placement,
|
||||
);
|
||||
} else if (last.on_curve) {
|
||||
current = transformPoint(
|
||||
last,
|
||||
bounds,
|
||||
placement,
|
||||
);
|
||||
} else {
|
||||
current = midpoint(
|
||||
transformPoint(last, bounds, placement),
|
||||
transformPoint(first, bounds, placement),
|
||||
);
|
||||
}
|
||||
|
||||
// Move to the beginning
|
||||
try path.moveTo(alloc, current.x, current.y);
|
||||
|
||||
// Go through the points and connect em!
|
||||
while (i < contour.len) {
|
||||
const p = contour[i];
|
||||
|
||||
// On-curve points connect to the current point with a straight line.
|
||||
if (p.on_curve) {
|
||||
current = transformPoint(p, bounds, placement);
|
||||
try path.lineTo(alloc, current.x, current.y);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Off-curve points are quadratic control points. The following point is
|
||||
// either the curve endpoint or, if it is also off-curve, contributes an
|
||||
// implied on-curve endpoint halfway between the two controls.
|
||||
const control = transformPoint(p, bounds, placement);
|
||||
const next = contour[(i + 1) % contour.len];
|
||||
const end = if (next.on_curve) transformPoint(
|
||||
next,
|
||||
bounds,
|
||||
placement,
|
||||
) else midpoint(
|
||||
control,
|
||||
transformPoint(next, bounds, placement),
|
||||
);
|
||||
|
||||
// z2d paths only expose cubic curves, so convert the TrueType
|
||||
// quadratic segment to an equivalent cubic segment before appending it.
|
||||
const c1 = Point{
|
||||
.x = current.x + ((2.0 / 3.0) * (control.x - current.x)),
|
||||
.y = current.y + ((2.0 / 3.0) * (control.y - current.y)),
|
||||
};
|
||||
const c2 = Point{
|
||||
.x = end.x + ((2.0 / 3.0) * (control.x - end.x)),
|
||||
.y = end.y + ((2.0 / 3.0) * (control.y - end.y)),
|
||||
};
|
||||
try path.curveTo(
|
||||
alloc,
|
||||
c1.x,
|
||||
c1.y,
|
||||
c2.x,
|
||||
c2.y,
|
||||
end.x,
|
||||
end.y,
|
||||
);
|
||||
|
||||
current = end;
|
||||
|
||||
// If we consumed an explicit on-curve endpoint then skip it; otherwise
|
||||
// the next off-curve point still needs to be used as the control point
|
||||
// for the following quadratic segment.
|
||||
i += if (next.on_curve) 2 else 1;
|
||||
}
|
||||
|
||||
try path.close(alloc);
|
||||
}
|
||||
|
||||
/// Convert a decoded glyf point from design-space coordinates to z2d bitmap
|
||||
/// coordinates.
|
||||
///
|
||||
/// `bounds` describes the decoded outline's point/control bounds in glyf
|
||||
/// design units. `placement` describes where those bounds should land in the
|
||||
/// output bitmap after constraints are applied. Glyf coordinates are y-up; z2d
|
||||
/// surfaces are y-down, so this also flips the y axis using
|
||||
/// `placement.bitmap_height`.
|
||||
fn transformPoint(
|
||||
p: glyf.Glyf.Outline.Point,
|
||||
bounds: Bounds,
|
||||
placement: Placement,
|
||||
) Point {
|
||||
const scale_x = placement.width / bounds.width();
|
||||
const scale_y = placement.height / bounds.height();
|
||||
const x_design: f64 = @floatFromInt(p.x);
|
||||
const y_design: f64 = @floatFromInt(p.y);
|
||||
return .{
|
||||
.x = placement.x + ((x_design - bounds.x_min) * scale_x),
|
||||
.y = placement.bitmap_height - placement.y -
|
||||
((y_design - bounds.y_min) * scale_y),
|
||||
};
|
||||
}
|
||||
|
||||
/// Return the implied on-curve point between two off-curve TrueType control
|
||||
/// points.
|
||||
fn midpoint(a: Point, b: Point) Point {
|
||||
return .{
|
||||
.x = (a.x + b.x) / 2.0,
|
||||
.y = (a.y + b.y) / 2.0,
|
||||
};
|
||||
}
|
||||
|
||||
fn testMetrics(width: u32, height: u32) @import("Metrics.zig") {
|
||||
return .{
|
||||
.cell_width = width,
|
||||
.cell_height = height,
|
||||
.cell_baseline = 0,
|
||||
.underline_position = height,
|
||||
.underline_thickness = 1,
|
||||
.strikethrough_position = height / 2,
|
||||
.strikethrough_thickness = 1,
|
||||
.overline_position = 0,
|
||||
.overline_thickness = 1,
|
||||
.box_thickness = 1,
|
||||
.cursor_thickness = 1,
|
||||
.cursor_height = height,
|
||||
.icon_height = @floatFromInt(height),
|
||||
.icon_height_single = @floatFromInt(height),
|
||||
.face_width = @floatFromInt(width),
|
||||
.face_height = @floatFromInt(height),
|
||||
.face_y = 0,
|
||||
};
|
||||
}
|
||||
|
||||
test {
|
||||
_ = @import("glyf_rasterize_png_test.zig");
|
||||
}
|
||||
|
||||
test "glyf_rasterize: empty outline returns empty bitmap" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var bm = try rasterize(alloc, .{ .points = &.{}, .contours = &.{} }, .{
|
||||
.units_per_em = 1000,
|
||||
.advance_width = 1000,
|
||||
.line_height = 1000,
|
||||
}, .{
|
||||
.grid_metrics = testMetrics(20, 20),
|
||||
});
|
||||
defer bm.deinit(alloc);
|
||||
|
||||
try testing.expectEqual(@as(u32, 20), bm.width);
|
||||
try testing.expectEqual(@as(u32, 20), bm.height);
|
||||
try testing.expectEqual(@as(usize, 20 * 20), bm.data.len);
|
||||
for (bm.data) |v| try testing.expectEqual(@as(u8, 0), v);
|
||||
}
|
||||
|
||||
test "glyf_rasterize: square fills bitmap center" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const outline: glyf.Glyf.Outline = .{
|
||||
.points = &.{
|
||||
.{ .x = 0, .y = 0, .on_curve = true },
|
||||
.{ .x = 1000, .y = 0, .on_curve = true },
|
||||
.{ .x = 1000, .y = 1000, .on_curve = true },
|
||||
.{ .x = 0, .y = 1000, .on_curve = true },
|
||||
},
|
||||
.contours = &.{3},
|
||||
};
|
||||
|
||||
var bm = try rasterize(alloc, outline, .{
|
||||
.units_per_em = 1000,
|
||||
.advance_width = 1000,
|
||||
.line_height = 1000,
|
||||
}, .{
|
||||
.grid_metrics = testMetrics(20, 20),
|
||||
});
|
||||
defer bm.deinit(alloc);
|
||||
|
||||
try testing.expect(bm.data[10 * bm.width + 10] > 200);
|
||||
}
|
||||
|
||||
test "glyf_rasterize: quadratic contour renders" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const outline: glyf.Glyf.Outline = .{
|
||||
.points = &.{
|
||||
.{ .x = 0, .y = 0, .on_curve = true },
|
||||
.{ .x = 500, .y = 1000, .on_curve = false },
|
||||
.{ .x = 1000, .y = 0, .on_curve = true },
|
||||
},
|
||||
.contours = &.{2},
|
||||
};
|
||||
|
||||
var bm = try rasterize(alloc, outline, .{
|
||||
.units_per_em = 1000,
|
||||
.advance_width = 1000,
|
||||
.line_height = 1000,
|
||||
}, .{
|
||||
.grid_metrics = testMetrics(20, 20),
|
||||
});
|
||||
defer bm.deinit(alloc);
|
||||
|
||||
var nonzero = false;
|
||||
for (bm.data) |v| nonzero = nonzero or v != 0;
|
||||
try testing.expect(nonzero);
|
||||
}
|
||||
|
||||
test "glyf_rasterize: consecutive off-curve points render" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const outline: glyf.Glyf.Outline = .{
|
||||
.points = &.{
|
||||
.{ .x = 0, .y = 0, .on_curve = true },
|
||||
.{ .x = 250, .y = 1000, .on_curve = false },
|
||||
.{ .x = 750, .y = 1000, .on_curve = false },
|
||||
.{ .x = 1000, .y = 0, .on_curve = true },
|
||||
},
|
||||
.contours = &.{3},
|
||||
};
|
||||
|
||||
var bm = try rasterize(alloc, outline, .{
|
||||
.units_per_em = 1000,
|
||||
.advance_width = 1000,
|
||||
.line_height = 1000,
|
||||
}, .{
|
||||
.grid_metrics = testMetrics(20, 20),
|
||||
});
|
||||
defer bm.deinit(alloc);
|
||||
|
||||
var nonzero = false;
|
||||
for (bm.data) |v| nonzero = nonzero or v != 0;
|
||||
try testing.expect(nonzero);
|
||||
}
|
||||
|
||||
test "glyf_rasterize: units per em controls baseline scale" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const outline: glyf.Glyf.Outline = .{
|
||||
.points = &.{
|
||||
.{ .x = 0, .y = 0, .on_curve = true },
|
||||
.{ .x = 1000, .y = 0, .on_curve = true },
|
||||
.{ .x = 1000, .y = 1000, .on_curve = true },
|
||||
.{ .x = 0, .y = 1000, .on_curve = true },
|
||||
},
|
||||
.contours = &.{3},
|
||||
};
|
||||
|
||||
var bm = try rasterize(alloc, outline, .{
|
||||
.units_per_em = 2000,
|
||||
.advance_width = 1000,
|
||||
.line_height = 1000,
|
||||
}, .{
|
||||
.grid_metrics = testMetrics(20, 20),
|
||||
});
|
||||
defer bm.deinit(alloc);
|
||||
|
||||
// With a 2000-unit em in a 20px cell, this 1000-unit square occupies the
|
||||
// bottom half of the cell. This matches the linked rasterizer's y=0
|
||||
// baseline/bottom behavior and proves units_per_em is the scale reference.
|
||||
try testing.expect(bm.data[15 * bm.width + 5] > 200);
|
||||
try testing.expectEqual(@as(u8, 0), bm.data[5 * bm.width + 5]);
|
||||
}
|
||||
|
||||
test "glyf_rasterize: degenerate outline returns full empty bitmap" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const outline: glyf.Glyf.Outline = .{
|
||||
.points = &.{
|
||||
.{ .x = 0, .y = 0, .on_curve = true },
|
||||
.{ .x = 1000, .y = 0, .on_curve = true },
|
||||
.{ .x = 500, .y = 0, .on_curve = true },
|
||||
},
|
||||
.contours = &.{2},
|
||||
};
|
||||
|
||||
var bm = try rasterize(alloc, outline, .{
|
||||
.units_per_em = 1000,
|
||||
.advance_width = 1000,
|
||||
.line_height = 1000,
|
||||
}, .{
|
||||
.grid_metrics = testMetrics(20, 20),
|
||||
});
|
||||
defer bm.deinit(alloc);
|
||||
|
||||
try testing.expectEqual(@as(u32, 20), bm.width);
|
||||
try testing.expectEqual(@as(u32, 20), bm.height);
|
||||
try testing.expectEqual(@as(usize, 20 * 20), bm.data.len);
|
||||
for (bm.data) |v| try testing.expectEqual(@as(u8, 0), v);
|
||||
}
|
||||
|
||||
test "glyf_rasterize: contour can start off curve with final on curve point" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const outline: glyf.Glyf.Outline = .{
|
||||
.points = &.{
|
||||
.{ .x = 500, .y = 1000, .on_curve = false },
|
||||
.{ .x = 1000, .y = 0, .on_curve = true },
|
||||
.{ .x = 0, .y = 0, .on_curve = true },
|
||||
},
|
||||
.contours = &.{2},
|
||||
};
|
||||
|
||||
var bm = try rasterize(alloc, outline, .{
|
||||
.units_per_em = 1000,
|
||||
.advance_width = 1000,
|
||||
.line_height = 1000,
|
||||
}, .{
|
||||
.grid_metrics = testMetrics(20, 20),
|
||||
});
|
||||
defer bm.deinit(alloc);
|
||||
|
||||
var nonzero = false;
|
||||
for (bm.data) |v| nonzero = nonzero or v != 0;
|
||||
try testing.expect(nonzero);
|
||||
}
|
||||
|
||||
test "glyf_rasterize: contour can start with implied midpoint" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const outline: glyf.Glyf.Outline = .{
|
||||
.points = &.{
|
||||
.{ .x = 250, .y = 1000, .on_curve = false },
|
||||
.{ .x = 1000, .y = 0, .on_curve = true },
|
||||
.{ .x = 750, .y = 1000, .on_curve = false },
|
||||
.{ .x = 0, .y = 0, .on_curve = true },
|
||||
},
|
||||
.contours = &.{3},
|
||||
};
|
||||
|
||||
var bm = try rasterize(alloc, outline, .{
|
||||
.units_per_em = 1000,
|
||||
.advance_width = 1000,
|
||||
.line_height = 1000,
|
||||
}, .{
|
||||
.grid_metrics = testMetrics(20, 20),
|
||||
});
|
||||
defer bm.deinit(alloc);
|
||||
|
||||
var nonzero = false;
|
||||
for (bm.data) |v| nonzero = nonzero or v != 0;
|
||||
try testing.expect(nonzero);
|
||||
}
|
||||
|
||||
test "glyf_rasterize: multiple contours render independently" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const outline: glyf.Glyf.Outline = .{
|
||||
.points = &.{
|
||||
.{ .x = 0, .y = 0, .on_curve = true },
|
||||
.{ .x = 400, .y = 0, .on_curve = true },
|
||||
.{ .x = 400, .y = 400, .on_curve = true },
|
||||
.{ .x = 0, .y = 400, .on_curve = true },
|
||||
.{ .x = 600, .y = 600, .on_curve = true },
|
||||
.{ .x = 1000, .y = 600, .on_curve = true },
|
||||
.{ .x = 1000, .y = 1000, .on_curve = true },
|
||||
.{ .x = 600, .y = 1000, .on_curve = true },
|
||||
},
|
||||
.contours = &.{ 3, 7 },
|
||||
};
|
||||
|
||||
var bm = try rasterize(alloc, outline, .{
|
||||
.units_per_em = 1000,
|
||||
.advance_width = 1000,
|
||||
.line_height = 1000,
|
||||
}, .{
|
||||
.grid_metrics = testMetrics(20, 20),
|
||||
});
|
||||
defer bm.deinit(alloc);
|
||||
|
||||
try testing.expect(bm.data[16 * bm.width + 4] > 200);
|
||||
try testing.expect(bm.data[4 * bm.width + 16] > 200);
|
||||
try testing.expectEqual(@as(u8, 0), bm.data[10 * bm.width + 10]);
|
||||
}
|
||||
|
||||
test "glyf_rasterize: non-zero bearings preserve declared whitespace" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const outline: glyf.Glyf.Outline = .{
|
||||
.points = &.{
|
||||
.{ .x = 250, .y = 0, .on_curve = true },
|
||||
.{ .x = 750, .y = 0, .on_curve = true },
|
||||
.{ .x = 750, .y = 1000, .on_curve = true },
|
||||
.{ .x = 250, .y = 1000, .on_curve = true },
|
||||
},
|
||||
.contours = &.{3},
|
||||
};
|
||||
|
||||
var bm = try rasterize(alloc, outline, .{
|
||||
.units_per_em = 1000,
|
||||
.advance_width = 1000,
|
||||
.line_height = 1000,
|
||||
}, .{
|
||||
.grid_metrics = testMetrics(20, 20),
|
||||
});
|
||||
defer bm.deinit(alloc);
|
||||
|
||||
try testing.expectEqual(@as(u8, 0), bm.data[10 * bm.width + 2]);
|
||||
try testing.expect(bm.data[10 * bm.width + 10] > 200);
|
||||
try testing.expectEqual(@as(u8, 0), bm.data[10 * bm.width + 17]);
|
||||
}
|
||||
|
||||
test "glyf_rasterize: negative y coordinates descend below baseline" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const outline: glyf.Glyf.Outline = .{
|
||||
.points = &.{
|
||||
.{ .x = 0, .y = -250, .on_curve = true },
|
||||
.{ .x = 1000, .y = -250, .on_curve = true },
|
||||
.{ .x = 1000, .y = 750, .on_curve = true },
|
||||
.{ .x = 0, .y = 750, .on_curve = true },
|
||||
},
|
||||
.contours = &.{3},
|
||||
};
|
||||
|
||||
var bm = try rasterize(alloc, outline, .{
|
||||
.units_per_em = 1000,
|
||||
.advance_width = 1000,
|
||||
.line_height = 1000,
|
||||
}, .{
|
||||
.grid_metrics = testMetrics(20, 20),
|
||||
});
|
||||
defer bm.deinit(alloc);
|
||||
|
||||
try testing.expectEqual(@as(u8, 0), bm.data[2 * bm.width + 10]);
|
||||
try testing.expect(bm.data[10 * bm.width + 10] > 200);
|
||||
try testing.expect(bm.data[18 * bm.width + 10] > 200);
|
||||
}
|
||||
|
||||
test "glyf_rasterize: two-cell bitmap and constraint render within width" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const outline: glyf.Glyf.Outline = .{
|
||||
.points = &.{
|
||||
.{ .x = 0, .y = 0, .on_curve = true },
|
||||
.{ .x = 1000, .y = 0, .on_curve = true },
|
||||
.{ .x = 1000, .y = 1000, .on_curve = true },
|
||||
.{ .x = 0, .y = 1000, .on_curve = true },
|
||||
},
|
||||
.contours = &.{3},
|
||||
};
|
||||
|
||||
var bm = try rasterize(alloc, outline, .{
|
||||
.units_per_em = 1000,
|
||||
.advance_width = 1000,
|
||||
.line_height = 1000,
|
||||
}, .{
|
||||
.grid_metrics = testMetrics(20, 20),
|
||||
.cell_width = 2,
|
||||
.constraint_width = 2,
|
||||
.constraint = .{
|
||||
.size = .cover,
|
||||
.align_horizontal = .center,
|
||||
.align_vertical = .center,
|
||||
},
|
||||
});
|
||||
defer bm.deinit(alloc);
|
||||
|
||||
try testing.expectEqual(@as(u32, 40), bm.width);
|
||||
try testing.expectEqual(@as(u32, 20), bm.height);
|
||||
try testing.expectEqual(@as(u8, 0), bm.data[10 * bm.width + 2]);
|
||||
try testing.expect(bm.data[10 * bm.width + 20] > 200);
|
||||
try testing.expectEqual(@as(u8, 0), bm.data[10 * bm.width + 37]);
|
||||
}
|
||||
|
||||
test "glyf_rasterize: line height does not change unconstrained em scale" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const outline: glyf.Glyf.Outline = .{
|
||||
.points = &.{
|
||||
.{ .x = 0, .y = 0, .on_curve = true },
|
||||
.{ .x = 1000, .y = 0, .on_curve = true },
|
||||
.{ .x = 1000, .y = 1000, .on_curve = true },
|
||||
.{ .x = 0, .y = 1000, .on_curve = true },
|
||||
},
|
||||
.contours = &.{3},
|
||||
};
|
||||
|
||||
var bm = try rasterize(alloc, outline, .{
|
||||
.units_per_em = 1000,
|
||||
.advance_width = 1000,
|
||||
.line_height = 2000,
|
||||
}, .{
|
||||
.grid_metrics = testMetrics(20, 20),
|
||||
});
|
||||
defer bm.deinit(alloc);
|
||||
|
||||
try testing.expect(bm.data[10 * bm.width + 10] > 200);
|
||||
try testing.expect(bm.data[2 * bm.width + 10] > 200);
|
||||
try testing.expect(bm.data[17 * bm.width + 10] > 200);
|
||||
}
|
||||
301
src/font/glyf_rasterize_png_test.zig
Normal file
301
src/font/glyf_rasterize_png_test.zig
Normal file
@@ -0,0 +1,301 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const wuffs = @import("wuffs");
|
||||
const z2d = @import("z2d");
|
||||
|
||||
const glyf_rasterize = @import("glyf_rasterize.zig");
|
||||
const glyf = @import("opentype/glyf.zig");
|
||||
|
||||
const log = std.log.scoped(.glyf_rasterize);
|
||||
|
||||
const test_glyf_payloads = [_][]const u8{
|
||||
// Nerd Font branch, folder, home, heart, and Rust cog outlines from:
|
||||
// https://github.com/raphamorim/glyph-protocol-examples/blob/main/bubbletea/main.go
|
||||
"AAIARv8zAhIDnQAZAB0AABcjNTQ3Njc3Njc2NTUjNxcjFRQGBwcGBwYVEQcRM82HJxs3SyoUE2aPjmY0NCUvDxSHh83rVzgoIzAbKSZAoaenvF5kIhkfHSM9AVdXAn8=",
|
||||
"AAEAAP/UA5wC/AAVAAAXIiY1ETQ2MzMyFxcWMyEyFgcRFgYjcy9ERC/nOCQjER0BITBEAQFEMCxELwJBMEQvLhdEL/4yL0Q=",
|
||||
"AAEAAP+aBBEDNgA+AAABFAYjIxMUBxUUBisFIiY9AjQmIyMiBh0CFAYrAiIiJwYiIyMiJjc1MjQ1NSMiJjQ3ATYzMhcBFgQOJBY5AQEqHh0GBzsrHioiGHMYIioeLDkBBAMBBAIcHiwBAToYIhIBzg4aFw8BzBcBahgi/t4KBB4eKioeLHQYIiIYdCweKgICKh7KBAJ+IDISAZQODP5qFA==",
|
||||
"AAEAAP/dA5sC+QAZAAATJjU1NDY3NhYXFzc2NhcWFhUVFAcBBiMiJ1ZWel09eCwWFSx4PV16Vv66FB0eFAEhUHULXpAQCiYsFhYsJgoQkF4LdVD+zxMT",
|
||||
"AAoAAP/YAyEC+AENARcBWgFlAW8BfQGIAbsBxAHNAAAAMhYXFhYyNjc2MzIXFhcWFxY3NjMyFhcWFxY3NjMyFxcHBxcWNzYXFgcGFxYzMhcWBw4CFAcUFQcUFxYWFRQGFhcWFgcGFBcWFxYGBwYUFxYGBw4CFhYXFgcGBwYVFBcWBwYjIgYXFgYnJgcGFxcHBiInJgYHBiMiJyYHBgcGBiMiJyYiBwYiJyYmBwYGJyYmJyYHBiImJyYmBwYiJjc3JyYHBicmNzYmIwYmNzY2Nzc0JyYmNTQ3NiYnJiY3NjQnJiY0Njc2NjQnJicmJjY3NjYnJjc3Mjc2NScmJyYnJjYXMjY1NCYmJyc3NhcWNzcnJjYzMhcWNjc2NjMyFxY3Njc2NjMyFxYyNzY3FyIHBhYzMjYnJgczBwYHBhUUMzIXFhYHBgcOAhUGFQciFhcWFxYXFhcWFjc2NzY3NjMzNzYvAiYnJjU0NzcnJicmJicnBwYGJyYnBwYGFxYyNzYmJyYFIgcGFBcWNicmBRcWBwYPAgYXFzM1NRcVMzY3NjU0JyYjBxUzMhcWFQcjIhUGFjMyNzYyFxYXFhUWFhcWNzY/AjY2Fxc2NjQjIiYnJicmJyYnJiMGIgcGFjMyNiclIgcGFjc2JyYBjQQICgcECAgIEQMJBgUCAwUGEBMDBgQEAwUDFBIFAwQEAQEEBRIZBQUGBQMCFxUFBwwBAgICARgSChoEFBcEExEUDwQECBATERMEGAoGCAQEBhEHAxUZCAsGAxcYBAUGChoVAgMBAQQEBhQSCgMECgQRFAUEBgcGBAUQEAgLDQwOCwoODQgFBg4CBRUPDAQEBAgTFAYIAQEEBREaBAYGBQQYGQYKAQQCARgSCg0NBBQXBBMQERIEBg4KCAIDCg0ECBEUBA0QBwQFEBcBAQICAgsIGRgCAgIBBAMFGhIFBAEBCAMFEhMIBAQEBgUREQQGCAcEBgQQDwoLCQQFCQcMDBARCg0HPgEPQjEfoJ8NJzACAykCBAQCAQEEAgIDGAgEAwggCg0BAQMCDBABAgIBHRsDBw4PAx8xFkQTCRQSEAkHEvoMDgcHGAgEBAcFAjAHBA0OFBQTBf3wBAsIAh0cAQMKBFOENjcIERsLLzEkJAECAXl5ARgBBBQZEAYEBQYBRCEnKiYSBgYGECAcARg/NhQLDgkLBQkQBimQEAQPChESCA4BVRAGCigLChEFAvgEEQsIBgkREA8FCAIBDA0KERYCAgkIBAQWFwIDBQYFBBkVAwIDBhkDBAQEAQEBAQcEAwQHBSQICAgMEREICwoFBggKDAgQEQwJBAQCCAgHGAYDAwQIAxAXBwMGFBkKBgQCAxYWBAQJCAQVHAwOAwQQEwYREBMVFBMCEA0GAgQmAgQPDAoSFgQJCQgXFgICBAYEBhgVBgEMFgQKAwMGBAQEBgQTEQgJCQwQEAgLCwQMBAkHBAoDAwkLDAgGCAgSFwcEAwQHAgIGBQQXDAEEBQEGCgQTBAcGBAICFxYICAkEFhEKDA0BARcSBBEPEw8ERQcKHiAKBScEESgaBAIDCjghJh4BBAIBAQEBBAECAhUjEQMJAgcIGBMBAQURFgcNDAMHCgYgIQY0IxAcBAETEwgDBBObARgMCwwIFAQEAgIIHAYKKAsDAwkYDQQNDRAjLg9eXgE4AQQHDhQHA4g0AgQqKwICGgUECAkVHAEHFAMDCAcKAx4iCgcGARwCBAwPJjAHDgQCwgMJIiIJAg0UFBIRDgQ=",
|
||||
};
|
||||
|
||||
/// Return deterministic font metrics for the PNG reference test.
|
||||
///
|
||||
/// These are intentionally minimal: the rasterizer only needs cell geometry,
|
||||
/// face geometry, and icon heights for constraint calculations.
|
||||
fn testMetrics(width: u32, height: u32) @import("Metrics.zig") {
|
||||
return .{
|
||||
.cell_width = width,
|
||||
.cell_height = height,
|
||||
.cell_baseline = 0,
|
||||
.underline_position = height,
|
||||
.underline_thickness = 1,
|
||||
.strikethrough_position = height / 2,
|
||||
.strikethrough_thickness = 1,
|
||||
.overline_position = 0,
|
||||
.overline_thickness = 1,
|
||||
.box_thickness = 1,
|
||||
.cursor_thickness = 1,
|
||||
.cursor_height = height,
|
||||
.icon_height = @floatFromInt(height),
|
||||
.icon_height_single = @floatFromInt(height),
|
||||
.face_width = @floatFromInt(width),
|
||||
.face_height = @floatFromInt(height),
|
||||
.face_y = 0,
|
||||
};
|
||||
}
|
||||
|
||||
/// Decode a base64-encoded glyf protocol payload into an owned outline.
|
||||
///
|
||||
/// The payload is a complete simple-glyph `glyf` table entry. The returned
|
||||
/// outline owns decoded point and contour storage and must be deinitialized by
|
||||
/// the caller.
|
||||
fn decodeGlyfPayload(alloc: Allocator, payload: []const u8) !glyf.Glyf.Outline {
|
||||
const decoder = std.base64.standard.Decoder;
|
||||
const size = try decoder.calcSizeForSlice(payload);
|
||||
const data = try alloc.alloc(u8, size);
|
||||
defer alloc.free(data);
|
||||
|
||||
try decoder.decode(data, payload);
|
||||
const entry = try glyf.Glyf.Entry.init(data);
|
||||
return try entry.decode(alloc);
|
||||
}
|
||||
|
||||
/// Copy a tightly packed alpha bitmap into the alpha atlas at `dst_x`, `dst_y`.
|
||||
///
|
||||
/// The destination rectangle must fit inside the atlas. This is a test helper,
|
||||
/// so it trusts the hardcoded atlas layout rather than clipping.
|
||||
fn blitBitmap(atlas: *z2d.Surface, bm: glyf_rasterize.Bitmap, dst_x: usize, dst_y: usize) void {
|
||||
const dst_width: usize = @intCast(atlas.getWidth());
|
||||
const dst = std.mem.sliceAsBytes(atlas.image_surface_alpha8.buf);
|
||||
for (0..bm.height) |y| {
|
||||
const src_start = y * bm.width;
|
||||
const src_end = src_start + bm.width;
|
||||
const dst_start = (dst_y + y) * dst_width + dst_x;
|
||||
@memcpy(dst[dst_start .. dst_start + bm.width], bm.data[src_start..src_end]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw faint terminal-cell outlines into one row of the alpha atlas.
|
||||
///
|
||||
/// The boxes make cell advance and placement behavior visible in the reference
|
||||
/// PNG without overpowering the rendered glyph coverage.
|
||||
fn drawCellBoxes(atlas: *z2d.Surface, y: usize, cell_width: usize, cell_height: usize) void {
|
||||
const width: usize = @intCast(atlas.getWidth());
|
||||
const dst = std.mem.sliceAsBytes(atlas.image_surface_alpha8.buf);
|
||||
const alpha = 64;
|
||||
|
||||
var x: usize = 0;
|
||||
while (x < width) : (x += cell_width) {
|
||||
const right = @min(x + cell_width - 1, width - 1);
|
||||
const bottom = y + cell_height - 1;
|
||||
|
||||
for (x..right + 1) |px| {
|
||||
dst[y * width + px] = @max(dst[y * width + px], alpha);
|
||||
dst[bottom * width + px] = @max(dst[bottom * width + px], alpha);
|
||||
}
|
||||
for (y..bottom + 1) |py| {
|
||||
dst[py * width + x] = @max(dst[py * width + x], alpha);
|
||||
dst[py * width + right] = @max(dst[py * width + right], alpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare a generated atlas PNG against the checked-in reference image.
|
||||
///
|
||||
/// On missing reference or mismatch, copy the generated PNG into the workspace
|
||||
/// as `glyf_rasterize_test.png`. On pixel mismatch, also write
|
||||
/// `glyf_rasterize_diff.png`, where red is reference-only coverage and green is
|
||||
/// newly generated coverage. Returns true when a difference was found.
|
||||
fn diffAtlas(
|
||||
alloc: Allocator,
|
||||
atlas: *z2d.Surface,
|
||||
generated_path: []const u8,
|
||||
) !bool {
|
||||
const ref_path = "src/font/testdata/glyf_rasterize.png";
|
||||
|
||||
const generated_file = try std.fs.openFileAbsolute(generated_path, .{ .mode = .read_only });
|
||||
defer generated_file.close();
|
||||
const generated_bytes = try generated_file.readToEndAlloc(alloc, std.math.maxInt(usize));
|
||||
defer alloc.free(generated_bytes);
|
||||
|
||||
const cwd_absolute = try std.fs.cwd().realpathAlloc(alloc, ".");
|
||||
defer alloc.free(cwd_absolute);
|
||||
|
||||
const ref_file = std.fs.cwd().openFile(ref_path, .{ .mode = .read_only }) catch |err| {
|
||||
log.err("Can't open reference file {s}: {}", .{ ref_path, err });
|
||||
|
||||
const test_path = try std.fmt.allocPrint(alloc, "{s}/glyf_rasterize_test.png", .{cwd_absolute});
|
||||
defer alloc.free(test_path);
|
||||
try std.fs.copyFileAbsolute(generated_path, test_path, .{});
|
||||
return true;
|
||||
};
|
||||
defer ref_file.close();
|
||||
const ref_bytes = try ref_file.readToEndAlloc(alloc, std.math.maxInt(usize));
|
||||
defer alloc.free(ref_bytes);
|
||||
|
||||
if (std.mem.eql(u8, generated_bytes, ref_bytes)) return false;
|
||||
|
||||
const test_path = try std.fmt.allocPrint(alloc, "{s}/glyf_rasterize_test.png", .{cwd_absolute});
|
||||
defer alloc.free(test_path);
|
||||
try std.fs.copyFileAbsolute(generated_path, test_path, .{});
|
||||
|
||||
const ref_rgba = try wuffs.png.decode(alloc, ref_bytes);
|
||||
defer alloc.free(ref_rgba.data);
|
||||
|
||||
if (ref_rgba.width != atlas.getWidth() or ref_rgba.height != atlas.getHeight()) {
|
||||
log.err(
|
||||
"glyf rasterize visual output dimensions differ from reference: " ++
|
||||
"test={s} ({d}x{d}), reference={s} ({d}x{d})",
|
||||
.{ test_path, atlas.getWidth(), atlas.getHeight(), ref_path, ref_rgba.width, ref_rgba.height },
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
var diff = try z2d.Surface.init(
|
||||
.image_surface_rgb,
|
||||
alloc,
|
||||
atlas.getWidth(),
|
||||
atlas.getHeight(),
|
||||
);
|
||||
defer diff.deinit(alloc);
|
||||
|
||||
const test_gray = std.mem.sliceAsBytes(atlas.image_surface_alpha8.buf);
|
||||
const diff_pix = diff.image_surface_rgb.buf;
|
||||
var differs = false;
|
||||
for (test_gray, 0..) |t, i| {
|
||||
const r = ref_rgba.data[i * 4];
|
||||
if (t == r) {
|
||||
diff_pix[i].r = t / 3;
|
||||
diff_pix[i].g = t / 3;
|
||||
diff_pix[i].b = t / 3;
|
||||
} else {
|
||||
differs = true;
|
||||
diff_pix[i].r = r;
|
||||
diff_pix[i].g = t;
|
||||
}
|
||||
}
|
||||
|
||||
if (!differs) {
|
||||
log.err(
|
||||
"generated glyf rasterize PNG bytes differ from reference but pixels match; " ++
|
||||
"test={s}, reference={s}",
|
||||
.{ test_path, ref_path },
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
const diff_path = "./glyf_rasterize_diff.png";
|
||||
try z2d.png_exporter.writeToPNGFile(diff, diff_path, .{});
|
||||
log.err(
|
||||
"glyf rasterize visual output differs from reference: test={s}, reference={s}, diff={s}",
|
||||
.{ test_path, ref_path, diff_path },
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
test "glyf_rasterize: bubbletea glyph protocol examples match reference image" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// The generated PNG is a visual atlas for reading placement behavior.
|
||||
// Each column below is one terminal cell. The five payloads are rendered in
|
||||
// order: branch, folder, home, heart, rust.
|
||||
//
|
||||
// ```text
|
||||
// columns: 0 1 2 3 4 5 6 7 8 9
|
||||
// ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
|
||||
// row 0 │B│F│H│♥│R│ │ │ │ │ │ narrow/default: one-cell bitmap stride
|
||||
// ├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
|
||||
// row 1 │B │F │H │♥ │R │ width=2: same glyphs, two-cell bitmaps
|
||||
// ├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
|
||||
// row 2 │ B│ F│ H│ ♥│ R│ width=2 + horizontal center alignment
|
||||
// ├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
|
||||
// row 3 │B │F │H │♥ │R │ advance_width=2000: wider design box
|
||||
// └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
|
||||
// ```
|
||||
//
|
||||
// The faint grid lines in the PNG are these cell boundaries. Rows 2 and 3
|
||||
// are the design-metric/placement regression checks: row 2 centers a
|
||||
// one-cell-wide design box inside two cells, while row 3 centers a two-cell
|
||||
// design box so the visible glyph returns to the start of each span.
|
||||
const cell_width = 20;
|
||||
const cell_height = 20;
|
||||
const columns = test_glyf_payloads.len;
|
||||
const narrow_stride_x = cell_width;
|
||||
const wide_stride_x = cell_width * 2;
|
||||
const row_count = 4;
|
||||
|
||||
var atlas = try z2d.Surface.init(
|
||||
.image_surface_alpha8,
|
||||
alloc,
|
||||
@intCast(wide_stride_x * columns),
|
||||
cell_height * row_count,
|
||||
);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
for (test_glyf_payloads, 0..) |payload, i| {
|
||||
var outline = try decodeGlyfPayload(alloc, payload);
|
||||
defer outline.deinit(alloc);
|
||||
|
||||
var narrow = try glyf_rasterize.rasterize(alloc, outline, .{
|
||||
.units_per_em = 1000,
|
||||
.advance_width = 1000,
|
||||
.line_height = 1000,
|
||||
}, .{
|
||||
.grid_metrics = testMetrics(cell_width, cell_height),
|
||||
});
|
||||
defer narrow.deinit(alloc);
|
||||
blitBitmap(&atlas, narrow, i * narrow_stride_x, 0);
|
||||
|
||||
var wide = try glyf_rasterize.rasterize(alloc, outline, .{
|
||||
.units_per_em = 1000,
|
||||
.advance_width = 1000,
|
||||
.line_height = 1000,
|
||||
}, .{
|
||||
.grid_metrics = testMetrics(cell_width, cell_height),
|
||||
.cell_width = 2,
|
||||
});
|
||||
defer wide.deinit(alloc);
|
||||
blitBitmap(&atlas, wide, i * wide_stride_x, cell_height);
|
||||
|
||||
var centered = try glyf_rasterize.rasterize(alloc, outline, .{
|
||||
.units_per_em = 1000,
|
||||
.advance_width = 1000,
|
||||
.line_height = 1000,
|
||||
}, .{
|
||||
.grid_metrics = testMetrics(cell_width, cell_height),
|
||||
.cell_width = 2,
|
||||
.constraint_width = 2,
|
||||
.constraint = .{ .align_horizontal = .center },
|
||||
});
|
||||
defer centered.deinit(alloc);
|
||||
blitBitmap(&atlas, centered, i * wide_stride_x, cell_height * 2);
|
||||
|
||||
var designed_wide = try glyf_rasterize.rasterize(alloc, outline, .{
|
||||
.units_per_em = 1000,
|
||||
.advance_width = 2000,
|
||||
.line_height = 1000,
|
||||
}, .{
|
||||
.grid_metrics = testMetrics(cell_width, cell_height),
|
||||
.cell_width = 2,
|
||||
.constraint_width = 2,
|
||||
.constraint = .{ .align_horizontal = .center },
|
||||
});
|
||||
defer designed_wide.deinit(alloc);
|
||||
blitBitmap(&atlas, designed_wide, i * wide_stride_x, cell_height * 3);
|
||||
}
|
||||
|
||||
for (0..row_count) |row| drawCellBoxes(&atlas, row * cell_height, cell_width, cell_height);
|
||||
|
||||
var dir = testing.tmpDir(.{});
|
||||
defer dir.cleanup();
|
||||
const tmp_dir = try dir.dir.realpathAlloc(alloc, ".");
|
||||
defer alloc.free(tmp_dir);
|
||||
|
||||
const generated_path = try std.fmt.allocPrint(alloc, "{s}/glyf_rasterize.png", .{tmp_dir});
|
||||
defer alloc.free(generated_path);
|
||||
try z2d.png_exporter.writeToPNGFile(atlas, generated_path, .{});
|
||||
|
||||
try testing.expect(!try diffAtlas(alloc, &atlas, generated_path));
|
||||
}
|
||||
@@ -15,6 +15,7 @@ pub const Collection = @import("Collection.zig");
|
||||
pub const DeferredFace = @import("DeferredFace.zig");
|
||||
pub const Face = face.Face;
|
||||
pub const Glyph = @import("Glyph.zig");
|
||||
pub const glyf_rasterize = @import("glyf_rasterize.zig");
|
||||
pub const Metrics = @import("Metrics.zig");
|
||||
pub const opentype = @import("opentype.zig");
|
||||
pub const shape = @import("shape.zig");
|
||||
|
||||
@@ -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 `contours` 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: []const sfnt.uint16,
|
||||
|
||||
/// The backing storage of all points in the entry.
|
||||
points: []const 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) []const 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));
|
||||
}
|
||||
|
||||
BIN
src/font/testdata/glyf_rasterize.png
vendored
Normal file
BIN
src/font/testdata/glyf_rasterize.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
Reference in New Issue
Block a user