Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
220 changes: 180 additions & 40 deletions src/font/glyf_rasterize.zig
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,33 @@ const DesignMetrics = Glyph.DesignMetrics;
pub const Bitmap = struct {
width: u32,
height: u32,

/// Horizontal bearing, in pixels, from the nominal bitmap origin to the
/// returned bitmap origin. This is usually zero, but can be negative when
/// `Placement.x` is negative and the returned bitmap grows leftward to keep
/// the placed outline from being clipped. The renderer passes this through
/// as the glyph's `offset_x`.
offset_x: i32,

data: []u8,

// An empty 0x0 bitmap.
pub const empty: Bitmap = .{ .width = 0, .height = 0, .data = "" };
pub const empty: Bitmap = .{
.width = 0,
.height = 0,
.offset_x = 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 };
return .{
.width = width,
.height = height,
.offset_x = 0,
.data = data,
};
}

pub fn deinit(self: *Bitmap, alloc: Allocator) void {
Expand All @@ -37,12 +55,14 @@ pub const Bitmap = struct {

pub const Error = Allocator.Error || z2d.Path.Error || z2d.painter.FillError;

/// Rasterize a decoded glyf outline to a full-cell alpha bitmap.
/// Rasterize a decoded glyf outline to an 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 nominal bitmap rectangle starts as
/// `grid_metrics.cell_width * cell_width` by `grid_metrics.cell_height`, but the
/// returned bitmap may be wider if `Placement` puts the outline outside that
/// rectangle. `Bitmap.offset_x` places the returned bitmap relative to the
/// nominal bitmap origin. `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(
Expand All @@ -55,20 +75,23 @@ pub fn rasterize(
assert(design.advance_width > 0);
assert(design.line_height > 0);

// Calculate our final width/height.
const width: u32 = std.math.mul(
// Calculate the nominal bitmap rectangle. Placement is resolved relative to
// this rectangle, but this is not necessarily the final bitmap size: if the
// resulting Placement extends horizontally outside this rectangle, we grow
// the returned bitmap and preserve that overflow with Bitmap.offset_x.
const nominal_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);
const nominal_height = opts.grid_metrics.cell_height;
assert(nominal_width > 0 and nominal_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);
if (outline.contours.len == 0 or outline.points.len == 0) return Bitmap.initEmpty(alloc, nominal_width, nominal_height);

// Glyf entries have a header bounding box, but this rasterizer operates on
// the decoded Outline only. Recompute bounds from the decoded coordinate
Expand Down Expand Up @@ -97,21 +120,52 @@ pub fn rasterize(
// 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);
if (bounds.width() == 0 or bounds.height() == 0) return Bitmap.initEmpty(alloc, nominal_width, nominal_height);

// `placement` is in nominal bitmap coordinates: x=0 is the nominal left
// edge and x=nominal_width is the nominal right edge. The z2d surface we draw
// into, however, has its own bitmap coordinate system whose left edge is
// always x=0. If the placed outline extends outside the nominal rectangle,
// grow the returned bitmap just enough to include both:
//
// * the whole nominal rectangle, so empty/transparent in-cell pixels are
// still represented; and
// * the whole placed outline bounds, so Placement overflow is not clipped
// away before the renderer sees it.
//
// When overflow extends to the left, `left` is negative. We shift
// `placement.x` right by `-left` before drawing into the bitmap, then return
// `offset_x = left` so the renderer shifts the bitmap back to its intended
// position relative to the nominal origin. Example: a 20px-wide Placement
// centered in a 10px nominal rectangle has left=-5, bitmap_width=20,
// placement.x=0 in bitmap coordinates, and offset_x=-5 at render time.
var placement: Placement = .init(bounds, design, opts);
const left = @min(@as(f64, 0), @floor(placement.x));
const right = @max(
@as(f64, @floatFromInt(nominal_width)),
@ceil(placement.x + placement.width),
);
assert(right > left);

const bitmap_width: u32 = @intFromFloat(@min(
@as(f64, @floatFromInt(std.math.maxInt(u32))),
right - left,
));
const offset_x: i32 = @intFromFloat(left);
placement.x -= left;

// 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),
@intCast(bitmap_width),
@intCast(nominal_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,
Expand All @@ -131,8 +185,9 @@ pub fn rasterize(
);

return .{
.width = width,
.height = height,
.width = bitmap_width,
.height = nominal_height,
.offset_x = offset_x,
.data = try alloc.dupe(u8, std.mem.sliceAsBytes(sfc.image_surface_alpha8.buf)),
};
}
Expand All @@ -152,8 +207,7 @@ const Bounds = struct {
}
};

/// Cell-relative pixel rectangle where the decoded outline bounds should be
/// rasterized within the output bitmap.
/// Pixel rectangle where the decoded outline bounds should be rasterized.
///
/// This is deliberately the placement of the outline's computed point bounds,
/// not the full declared advance/line-height box. `advance_width` and
Expand All @@ -165,7 +219,7 @@ const Bounds = struct {
/// into this rectangle.
///
/// ```text
/// output bitmap / terminal cell
/// nominal bitmap rectangle
/// ╭────────────────────────────────────────────────────────────────────────╮ top
/// │ │
/// │ declared advance/line-height box │
Expand All @@ -181,7 +235,10 @@ const Bounds = struct {
/// │ ╰─────────────────▲──────────────────────────────────────────────╯ │
/// │ │ y │
/// ╰────────────────────────────────────────────────────────────────────────╯ bottom
/// x is measured from the bitmap left to the Placement left.
/// x is measured from the nominal rectangle's left edge to the
/// Placement left edge. It may be negative when constraints place
/// the outline before the nominal origin; rasterize grows the
/// returned bitmap and reports that as Bitmap.offset_x.
/// y is measured from the bitmap bottom to the Placement bottom.
/// bitmap_height is the full top-to-bottom bitmap height.
/// ```
Expand Down Expand Up @@ -213,29 +270,29 @@ const Placement = struct {
bitmap_height: f64,

/// Calculate where the decoded point bounds should land in the output
/// bitmap.
/// bitmap coordinate space.
///
/// 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.
/// `design` 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 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.
// Start with design units mapped so that the em square occupies one
// cell height. This makes units_per_em the scale reference and
// preserves the 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));

Expand Down Expand Up @@ -270,12 +327,57 @@ const Placement = struct {
}
break :constraint constraint;
};
const constrained = constraint.constrain(
var constrained = constraint.constrain(
glyph,
opts.grid_metrics,
opts.constraint_width,
);

// `RenderOptions.Constraint` is shared with normal font rendering and
// intentionally clamps oversized glyphs so they do not protrude before
// the cell origin. Placement supports a looser invariant: `center`
// aligns midpoints and `end` aligns trailing edges, even when the
// scaled layout box is wider than the nominal bitmap rectangle. In
// those cases overflow before x=0 is a valid Placement value and must
// be preserved by the wider bitmap/negative-bearing logic above.
//
// When constraint sizing is `.none`, the base transform has already
// chosen the scale. Re-apply only the alignment/padding part here in
// nominal bitmap coordinates, without the font helper's clamping.
if (constraint.size == .none) {
const nominal_width = @as(f64, @floatFromInt(opts.grid_metrics.cell_width)) *
@as(f64, @floatFromInt(opts.constraint_width));
const nominal_height: f64 = @floatFromInt(opts.grid_metrics.cell_height);

const group: Glyph.Size = .{
.width = group_width,
.height = group_height,
.x = glyph.x - (group_width * constraint.relative_x),
.y = glyph.y - (group_height * constraint.relative_y),
};

const start_x = constraint.pad_left * nominal_width;
const end_x = nominal_width * (1 - constraint.pad_right) - group.width;
const aligned_group_x = switch (constraint.align_horizontal) {
.none => group.x,
.start, .center1 => start_x,
.center => (start_x + end_x) / 2,
.end => end_x,
};

const start_y = constraint.pad_bottom * nominal_height;
const end_y = nominal_height * (1 - constraint.pad_top) - group.height;
const aligned_group_y = switch (constraint.align_vertical) {
.none => group.y,
.start => start_y,
.center, .center1 => (start_y + end_y) / 2,
.end => end_y,
};

constrained.x = aligned_group_x + (group.width * constraint.relative_x);
constrained.y = aligned_group_y + (group.height * constraint.relative_y);
}

// 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 .{
Expand Down Expand Up @@ -503,6 +605,44 @@ test "glyf_rasterize: square fills bitmap center" {
try testing.expect(bm.data[10 * bm.width + 10] > 200);
}

test "glyf_rasterize: centered Placement preserves horizontal overflow" {
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(10, 20),
.constraint = .{
.align_horizontal = .center,
.align_vertical = .center,
},
});
defer bm.deinit(alloc);

// The nominal bitmap rectangle is 10x20. The 1000x1000 design box maps to
// a 20x20 Placement, and center alignment places that Placement at x=-5.
// The returned bitmap keeps the full 20px outline instead of clipping to
// the nominal rectangle, and reports the left overflow as offset_x.
try testing.expectEqual(@as(u32, 20), bm.width);
try testing.expectEqual(@as(u32, 20), bm.height);
try testing.expectEqual(@as(i32, -5), bm.offset_x);
try testing.expect(bm.data[10 * bm.width + 1] > 200);
try testing.expect(bm.data[10 * bm.width + 18] > 200);
}

test "glyf_rasterize: quadratic contour renders" {
const testing = std.testing;
const alloc = testing.allocator;
Expand Down
17 changes: 17 additions & 0 deletions src/font/opentype/glyf.zig
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,23 @@ pub const Glyf = struct {
return self.points[start..end];
}

/// Deep copy this outline using `alloc`.
///
/// The returned outline owns its contour and point storage and must be
/// released with `deinit`.
pub fn clone(self: *const Outline, alloc: Allocator) Allocator.Error!Outline {
const contours = try alloc.dupe(sfnt.uint16, self.contours);
errdefer alloc.free(contours);

const points = try alloc.dupe(Point, self.points);
errdefer alloc.free(points);

return .{
.contours = contours,
.points = points,
};
}

/// Free all memory owned by this outline. Pass in the same
/// allocator used for decoding.
pub fn deinit(self: *Outline, alloc: Allocator) void {
Expand Down
Loading
Loading