Merge 51995a7822 into bfe633a948
commit
3ff3daa268
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
Loading…
Reference in New Issue