Files
ghostty/src/font/glyf_rasterize.zig
Mitchell Hashimoto b661adad2e font: add exact glyph protocol constraints
Extend glyph render constraints with cell-span sizing modes for height,
width, contain, cover bounds, and stretch bounds. These preserve the
existing face-targeted behavior for platform fonts, emoji, and Nerd Font
rules while giving registered glyphs a target based on terminal cell
spans.

Map Glyph Protocol registration options to the new constraint modes so
sizing follows the spec formulas based on authored advance width and line
height. Baseline alignment now places design-space y=0 on the terminal
text baseline instead of approximating it as start alignment.

Document the placement formulas in the local protocol summary and add
focused tests for constraint mapping, cell-span padding, line-height and
advance scaling, contain versus cover behavior, stretch, and baseline
placement.
2026-06-05 13:37:10 -07:00

977 lines
34 KiB
Zig

//! 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 Glyph = @import("Glyph.zig");
const glyf = @import("opentype/glyf.zig");
const DesignMetrics = Glyph.DesignMetrics;
/// 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
/// `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: Glyph.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.Glyph.Size 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.Glyph.RenderOptions.Constraint.
width: f64,
/// Height of the rasterized outline bounds in bitmap pixels after applying
/// font.Glyph.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: Glyph.RenderOptions,
) Placement {
// Start with design units mapped so that the em square occupies one
// cell. This normalizes coordinates into the same cell-relative pixel
// space used by RenderOptions.Constraint. Protocol sizing constraints
// then rescale the declared advance/line-height box so the final
// transform follows the protocol formulas based on advance width and
// line height rather than units-per-em.
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: Glyph.Size = .{
.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: Glyph.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);
}
test "glyf_rasterize: cell-span height sizing uses line height" {
const testing = std.testing;
const placement = Placement.init(.{
.x_min = 0,
.y_min = 0,
.x_max = 1000,
.y_max = 1000,
}, .{
.units_per_em = 1000,
.advance_width = 1000,
.line_height = 2000,
}, .{
.grid_metrics = testMetrics(20, 20),
.constraint = .{
.target = .cell_span,
.size = .height,
.align_horizontal = .start,
.align_vertical = .start,
},
});
// Final scale is cell_height / lh = 20 / 2000 = 0.01, so this
// 1000-unit square is 10px high rather than a full 20px em.
try testing.expectApproxEqAbs(@as(f64, 10), placement.width, 0.000001);
try testing.expectApproxEqAbs(@as(f64, 10), placement.height, 0.000001);
}
test "glyf_rasterize: cell-span advance sizing uses registered width" {
const testing = std.testing;
const placement = Placement.init(.{
.x_min = 0,
.y_min = 0,
.x_max = 1000,
.y_max = 1000,
}, .{
.units_per_em = 1000,
.advance_width = 1000,
.line_height = 1000,
}, .{
.grid_metrics = testMetrics(20, 20),
.constraint_width = 2,
.constraint = .{
.target = .cell_span,
.size = .width,
.align_horizontal = .start,
.align_vertical = .start,
},
});
// Final scale is span_width / aw = 40 / 1000 = 0.04.
try testing.expectApproxEqAbs(@as(f64, 40), placement.width, 0.000001);
try testing.expectApproxEqAbs(@as(f64, 40), placement.height, 0.000001);
}
test "glyf_rasterize: cell-span contain and cover choose different axes" {
const testing = std.testing;
const bounds: Bounds = .{
.x_min = 0,
.y_min = 0,
.x_max = 1000,
.y_max = 1000,
};
const design: DesignMetrics = .{
.units_per_em = 1000,
.advance_width = 1000,
.line_height = 2000,
};
const base_opts: Glyph.RenderOptions = .{
.grid_metrics = testMetrics(20, 20),
.constraint = .{
.target = .cell_span,
.align_horizontal = .start,
.align_vertical = .start,
},
};
var contain_opts = base_opts;
contain_opts.constraint.size = .contain;
const contain = Placement.init(bounds, design, contain_opts);
var cover_opts = base_opts;
cover_opts.constraint.size = .cover_bounds;
const cover = Placement.init(bounds, design, cover_opts);
try testing.expectApproxEqAbs(@as(f64, 10), contain.width, 0.000001);
try testing.expectApproxEqAbs(@as(f64, 10), contain.height, 0.000001);
try testing.expectApproxEqAbs(@as(f64, 20), cover.width, 0.000001);
try testing.expectApproxEqAbs(@as(f64, 20), cover.height, 0.000001);
}
test "glyf_rasterize: cell-span stretch uses independent axes" {
const testing = std.testing;
const placement = Placement.init(.{
.x_min = 0,
.y_min = 0,
.x_max = 1000,
.y_max = 2000,
}, .{
.units_per_em = 1000,
.advance_width = 1000,
.line_height = 2000,
}, .{
.grid_metrics = testMetrics(20, 20),
.constraint_width = 2,
.constraint = .{
.target = .cell_span,
.size = .stretch_bounds,
.align_horizontal = .start,
.align_vertical = .start,
},
});
try testing.expectApproxEqAbs(@as(f64, 40), placement.width, 0.000001);
try testing.expectApproxEqAbs(@as(f64, 20), placement.height, 0.000001);
}
test "glyf_rasterize: cell-span baseline aligns design y zero" {
const testing = std.testing;
var metrics = testMetrics(20, 20);
metrics.cell_baseline = 5;
const bounds: Bounds = .{
.x_min = 0,
.y_min = -250,
.x_max = 1000,
.y_max = 750,
};
const placement = Placement.init(bounds, .{
.units_per_em = 1000,
.advance_width = 1000,
.line_height = 1000,
}, .{
.grid_metrics = metrics,
.constraint = .{
.target = .cell_span,
.size = .height,
.align_horizontal = .start,
.align_vertical = .baseline,
},
});
const scale_y = placement.height / bounds.height();
const design_zero_y = placement.y + ((0 - bounds.y_min) * scale_y);
try testing.expectApproxEqAbs(@as(f64, 5), design_zero_y, 0.000001);
}