font(coretext): improve atlas padding calculations

- Simplifies and clarifies the math for how the bounding box for
rendered glyphs is computed
- Reduces margin from 2px between glyphs to 1px by only padding the
bottom and right side of each glyph
- Avoids excessive padding to glyph box when font thicken is enabled or
when using a synthetic bold (it was previously 4x as much padding as
necessary in some cases)
This commit is contained in:
Qwerasd
2024-12-19 13:44:30 -05:00
committed by Mitchell Hashimoto
parent 4ca6413ec9
commit 0e21293d43

View File

@ -292,31 +292,45 @@ pub const Face = struct {
var glyphs = [_]macos.graphics.Glyph{@intCast(glyph_index)}; var glyphs = [_]macos.graphics.Glyph{@intCast(glyph_index)};
// Get the bounding rect for rendering this glyph. // Get the bounding rect for rendering this glyph.
const rect = self.font.getBoundingRectsForGlyphs(.horizontal, &glyphs, null); // This is in a coordinate space with (0.0, 0.0)
// in the bottom left and +Y pointing up.
var rect = self.font.getBoundingRectsForGlyphs(.horizontal, &glyphs, null);
// The x/y that we render the glyph at. The Y value has to be flipped // If we're rendering a synthetic bold then we will gain 50% of
// because our coordinates in 3D space are (0, 0) bottom left with // the line width on every edge, which means we should increase
// +y being up. // our width and height by the line width and subtract half from
const render_x = @floor(rect.origin.x); // our origin points.
const render_y = @ceil(-rect.origin.y); if (self.synthetic_bold) |line_width| {
rect.size.width += line_width;
rect.size.height += line_width;
rect.origin.x -= line_width / 2;
rect.origin.y -= line_width / 2;
}
// The ascent is the amount of pixels above the baseline this glyph // We make an assumption that font smoothing ("thicken")
// is rendered. The ascent can be calculated by adding the full // adds no more than 1 extra pixel to any edge. We don't
// glyph height to the origin. // add extra size if it's a sbix color font though, since
const glyph_ascent = @ceil(rect.size.height + rect.origin.y); // bitmaps aren't affected by smoothing.
const sbix = self.color != null and self.color.?.sbix;
if (opts.thicken and !sbix) {
rect.size.width += 2.0;
rect.size.height += 2.0;
rect.origin.x -= 1.0;
rect.origin.y -= 1.0;
}
// The glyph height is basically rect.size.height but we do the // We compute the minimum and maximum x and y values.
// ascent plus the descent because both are rounded elements that // We round our min points down and max points up.
// will make us more accurate. const x0: i32, const x1: i32, const y0: i32, const y1: i32 = .{
const height: u32 = @intFromFloat(glyph_ascent + render_y); @intFromFloat(@floor(rect.origin.x)),
@intFromFloat(@ceil(rect.origin.x) + @ceil(rect.size.width)),
// The glyph width is our advertised bounding with plus the rounding @intFromFloat(@floor(rect.origin.y)),
// difference from our rendering X. @intFromFloat(@ceil(rect.origin.y) + @ceil(rect.size.height)),
const width: u32 = @intFromFloat(@ceil(rect.size.width + (rect.origin.x - render_x))); };
// This bitmap is blank. I've seen it happen in a font, I don't know why. // This bitmap is blank. I've seen it happen in a font, I don't know why.
// If it is empty, we just return a valid glyph struct that does nothing. // If it is empty, we just return a valid glyph struct that does nothing.
if (width == 0 or height == 0) return font.Glyph{ if (x1 <= x0 or y1 <= y0) return font.Glyph{
.width = 0, .width = 0,
.height = 0, .height = 0,
.offset_x = 0, .offset_x = 0,
@ -326,25 +340,8 @@ pub const Face = struct {
.advance_x = 0, .advance_x = 0,
}; };
// Additional padding we need to add to the bitmap context itself const width: u32 = @intCast(x1 - x0);
// due to the glyph being larger than standard. const height: u32 = @intCast(y1 - y0);
const padding_ctx: u32 = padding_ctx: {
// If we're doing thicken, then getBoundsForGlyphs does not take
// into account the anti-aliasing that will be added to the glyph.
// We need to add some padding to allow that to happen. A padding of
// 2 is usually enough for anti-aliasing.
var result: u32 = if (opts.thicken) 2 else 0;
// If we have a synthetic bold, add padding for the stroke width
if (self.synthetic_bold) |line_width| {
// x2 for top and bottom padding
result += @intFromFloat(@ceil(line_width) * 2);
}
break :padding_ctx result;
};
const padded_width: u32 = width + (padding_ctx * 2);
const padded_height: u32 = height + (padding_ctx * 2);
// Settings that are specific to if we are rendering text or emoji. // Settings that are specific to if we are rendering text or emoji.
const color: struct { const color: struct {
@ -380,17 +377,17 @@ pub const Face = struct {
// usually stabilizes pretty quickly and is very infrequent so I think // usually stabilizes pretty quickly and is very infrequent so I think
// the allocation overhead is acceptable compared to the cost of // the allocation overhead is acceptable compared to the cost of
// caching it forever or having to deal with a cache lifetime. // caching it forever or having to deal with a cache lifetime.
const buf = try alloc.alloc(u8, padded_width * padded_height * color.depth); const buf = try alloc.alloc(u8, width * height * color.depth);
defer alloc.free(buf); defer alloc.free(buf);
@memset(buf, 0); @memset(buf, 0);
const context = macos.graphics.BitmapContext.context; const context = macos.graphics.BitmapContext.context;
const ctx = try macos.graphics.BitmapContext.create( const ctx = try macos.graphics.BitmapContext.create(
buf, buf,
padded_width, width,
padded_height, height,
8, 8,
padded_width * color.depth, width * color.depth,
color.space, color.space,
color.context_opts, color.context_opts,
); );
@ -405,8 +402,8 @@ pub const Face = struct {
context.fillRect(ctx, .{ context.fillRect(ctx, .{
.origin = .{ .x = 0, .y = 0 }, .origin = .{ .x = 0, .y = 0 },
.size = .{ .size = .{
.width = @floatFromInt(padded_width), .width = @floatFromInt(width),
.height = @floatFromInt(padded_height), .height = @floatFromInt(height),
}, },
}); });
@ -437,67 +434,57 @@ pub const Face = struct {
// We want to render the glyphs at (0,0), but the glyphs themselves // We want to render the glyphs at (0,0), but the glyphs themselves
// are offset by bearings, so we have to undo those bearings in order // are offset by bearings, so we have to undo those bearings in order
// to get them to 0,0. We also add the padding so that they render // to get them to 0,0.
// slightly off the edge of the bitmap.
const padding_ctx_f64: f64 = @floatFromInt(padding_ctx);
self.font.drawGlyphs(&glyphs, &.{ self.font.drawGlyphs(&glyphs, &.{
.{ .{
.x = -1 * (render_x - padding_ctx_f64), .x = @floatFromInt(-x0),
.y = render_y + padding_ctx_f64, .y = @floatFromInt(-y0),
}, },
}, ctx); }, ctx);
const region = region: { const region = region: {
// We need to add a 1px padding to the font so that we don't // We reserve a region that's 1px wider and taller than we need
// get fuzzy issues when blending textures. // in order to create a 1px separation between adjacent glyphs
const padding = 1; // to prevent interpolation with adjacent glyphs while sampling
// from the atlas.
// Get the full padded region
var region = try atlas.reserve( var region = try atlas.reserve(
alloc, alloc,
padded_width + (padding * 2), // * 2 because left+right width + 1,
padded_height + (padding * 2), // * 2 because top+bottom height + 1,
); );
// Modify the region so that we remove the padding so that // We adjust the region width and height back down since we
// we write to the non-zero location. The data in an Altlas // don't need the extra pixel, we just needed to reserve it
// is always initialized to zero (Atlas.clear) so we don't // so that it isn't used for other glyphs in the future.
// need to worry about zero-ing that. region.width -= 1;
region.x += padding; region.height -= 1;
region.y += padding;
region.width -= padding * 2;
region.height -= padding * 2;
break :region region; break :region region;
}; };
atlas.set(region, buf); atlas.set(region, buf);
const metrics = opts.grid_metrics orelse self.metrics; const metrics = opts.grid_metrics orelse self.metrics;
const offset_y: i32 = offset_y: {
// Our Y coordinate in 3D is (0, 0) bottom left, +y is UP.
// We need to calculate our baseline from the bottom of a cell.
const baseline_from_bottom: f64 = @floatFromInt(metrics.cell_baseline);
// Next we offset our baseline by the bearing in the font. We // This should be the distance from the bottom of
// ADD here because CoreText y is UP. // the cell to the top of the glyph's bounding box.
const baseline_with_offset = baseline_from_bottom + glyph_ascent; //
// The calculation is distance from bottom of cell to
// Add our context padding we may have created. // baseline plus distance from baseline to top of glyph.
const baseline_with_padding = baseline_with_offset + padding_ctx_f64; const offset_y: i32 = @as(i32, @intCast(metrics.cell_baseline)) + y1;
break :offset_y @intFromFloat(@ceil(baseline_with_padding));
};
// This should be the distance from the left of
// the cell to the left of the glyph's bounding box.
const offset_x: i32 = offset_x: { const offset_x: i32 = offset_x: {
// Don't forget to apply our context padding if we have one var result: i32 = x0;
var result: i32 = @intFromFloat(render_x - padding_ctx_f64);
// If our cell was resized to be wider then we center our // If our cell was resized then we adjust our glyph's
// glyph in the cell. // position relative to the new center. This keeps glyphs
// centered in the cell whether it was made wider or narrower.
if (metrics.original_cell_width) |original_width| { if (metrics.original_cell_width) |original_width| {
if (original_width < metrics.cell_width) { const before: i32 = @intCast(original_width);
const diff = (metrics.cell_width - original_width) / 2; const after: i32 = @intCast(metrics.cell_width);
result += @intCast(diff); // Increase the offset by half of the difference
} // between the widths to keep things centered.
result += @divTrunc(after - before, 2);
} }
break :offset_x result; break :offset_x result;
@ -507,21 +494,9 @@ pub const Face = struct {
var advances: [glyphs.len]macos.graphics.Size = undefined; var advances: [glyphs.len]macos.graphics.Size = undefined;
_ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances); _ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances);
// std.log.warn("renderGlyph rect={} width={} height={} render_x={} render_y={} offset_y={} ascent={} cell_height={} cell_baseline={}", .{
// rect,
// width,
// height,
// render_x,
// render_y,
// offset_y,
// glyph_ascent,
// self.metrics.cell_height,
// self.metrics.cell_baseline,
// });
return .{ return .{
.width = padded_width, .width = width,
.height = padded_height, .height = height,
.offset_x = offset_x, .offset_x = offset_x,
.offset_y = offset_y, .offset_y = offset_y,
.atlas_x = region.x, .atlas_x = region.x,