font: add glyf rasterizer
parent
d8f56b790e
commit
8eff74ef76
|
|
@ -0,0 +1,832 @@
|
|||
//! 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 "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);
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -25,10 +25,10 @@ pub const Glyf = struct {
|
|||
pub const Outline = struct {
|
||||
/// List of contour end points. Calculate the full list of
|
||||
/// points using points[prev...this+1]
|
||||
contours: []sfnt.uint16,
|
||||
contours: []const sfnt.uint16,
|
||||
|
||||
/// The backing storage of all points in the entry.
|
||||
points: []Point,
|
||||
points: []const Point,
|
||||
|
||||
/// A single decoded point in a simple glyph contour.
|
||||
pub const Point = struct {
|
||||
|
|
@ -41,7 +41,7 @@ pub const Glyf = struct {
|
|||
///
|
||||
/// The returned slice references `points` and is invalidated when
|
||||
/// this outline is deinitialized.
|
||||
pub fn contour(self: Outline, index: usize) []Point {
|
||||
pub fn contour(self: Outline, index: usize) []const Point {
|
||||
const start = if (index == 0)
|
||||
0
|
||||
else
|
||||
|
|
|
|||
Loading…
Reference in New Issue