//! 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 directly over the bitmap we'll return. const data = try alloc.alloc(u8, @as(usize, width) * @as(usize, height)); errdefer alloc.free(data); var sfc: z2d.Surface = .initBuffer( .image_surface_alpha8, null, std.mem.bytesAsSlice(z2d.pixel.Alpha8, data), @intCast(width), @intCast(height), ); 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 = data, }; } 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 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: 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); }