From 96fbff681bf2e22440a8542daa4b3e96c14cf600 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Mon, 1 Sep 2025 18:28:51 -0700 Subject: [PATCH 1/3] Center before quantizing bitmap glyphs --- src/font/face/coretext.zig | 20 ++++++++++---------- src/font/face/freetype.zig | 20 ++++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 8c9611c04..a44fb7043 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -378,16 +378,6 @@ pub const Face = struct { var width = glyph_size.width; var height = glyph_size.height; - // If this is a bitmap glyph, it will always render as full pixels, - // not fractional pixels, so we need to quantize its position and - // size accordingly to align to full pixels so we get good results. - if (sbix) { - width = cell_width - @round(cell_width - width - x) - @round(x); - height = cell_height - @round(cell_height - height - y) - @round(y); - x = @round(x); - y = @round(y); - } - // We center all glyphs within the pixel-rounded and adjusted // cell width if it's larger than the face width, so that they // aren't weirdly off to the left. @@ -400,6 +390,16 @@ pub const Face = struct { x += (cell_width - metrics.face_width) / 2; } + // If this is a bitmap glyph, it will always render as full pixels, + // not fractional pixels, so we need to quantize its position and + // size accordingly to align to full pixels so we get good results. + if (sbix) { + width = cell_width - @round(cell_width - width - x) - @round(x); + height = cell_height - @round(cell_height - height - y) - @round(y); + x = @round(x); + y = @round(y); + } + // Our whole-pixel bearings for the final glyph. // The fractional portion will be included in the rasterized position. const px_x: i32 = @intFromFloat(@floor(x)); diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index bdcd82ab3..8be7647e5 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -492,16 +492,6 @@ pub const Face = struct { var x = glyph_size.x; var y = glyph_size.y; - // If this is a bitmap glyph, it will always render as full pixels, - // not fractional pixels, so we need to quantize its position and - // size accordingly to align to full pixels so we get good results. - if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_BITMAP) { - width = cell_width - @round(cell_width - width - x) - @round(x); - height = cell_height - @round(cell_height - height - y) - @round(y); - x = @round(x); - y = @round(y); - } - // We center all glyphs within the pixel-rounded and adjusted // cell width if it's larger than the face width, so that they // aren't weirdly off to the left. @@ -520,6 +510,16 @@ pub const Face = struct { x += @round((cell_width - metrics.face_width) / 2); } + // If this is a bitmap glyph, it will always render as full pixels, + // not fractional pixels, so we need to quantize its position and + // size accordingly to align to full pixels so we get good results. + if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_BITMAP) { + width = cell_width - @round(cell_width - width - x) - @round(x); + height = cell_height - @round(cell_height - height - y) - @round(y); + x = @round(x); + y = @round(y); + } + // Now we can render the glyph. var bitmap: freetype.c.FT_Bitmap = undefined; _ = freetype.c.FT_Bitmap_Init(&bitmap); From 5c129205a55d802341c536cd940042ea9cb75edb Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Thu, 2 Oct 2025 18:17:42 -0700 Subject: [PATCH 2/3] Use correct and consistent pre-constraint glyph rect In Freetype, measure rect after emboldening, so constraints apply to the true glyph size like in CoreText. In CoreText, don't let font smoothing affect the rect (only the canvas). --- src/font/face/coretext.zig | 31 +++++++++++++------------------ src/font/face/freetype.zig | 22 +++++++++++----------- 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index a44fb7043..52eb1d668 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -319,17 +319,6 @@ pub const Face = struct { rect.origin.y -= line_width / 2; }; - // We make an assumption that font smoothing ("thicken") - // adds no more than 1 extra pixel to any edge. We don't - // add extra size if it's a sbix color font though, since - // bitmaps aren't affected by smoothing. - 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; - } - // If our rect is smaller than a quarter pixel in either axis // then it has no outlines or they're too small to render. // @@ -400,10 +389,16 @@ pub const Face = struct { y = @round(y); } + // We make an assumption that font smoothing ("thicken") + // adds no more than 1 extra pixel to any edge. We don't + // add extra size if it's a sbix color font though, since + // bitmaps aren't affected by smoothing. + const canvas_padding: u32 = if (opts.thicken and !sbix) 1 else 0; + // Our whole-pixel bearings for the final glyph. // The fractional portion will be included in the rasterized position. - const px_x: i32 = @intFromFloat(@floor(x)); - const px_y: i32 = @intFromFloat(@floor(y)); + const px_x = @as(i32, @intFromFloat(@floor(x))) - @as(i32, @intCast(canvas_padding)); + const px_y = @as(i32, @intFromFloat(@floor(y))) - @as(i32, @intCast(canvas_padding)); // We keep track of the fractional part of the pixel bearings, which // we will add as an offset when rasterizing to make sure we get the @@ -413,9 +408,9 @@ pub const Face = struct { // Add the fractional pixel to the width and height and take // the ceiling to get a canvas size that will definitely fit - // our drawn glyph, including the fractional offset. - const px_width: u32 = @intFromFloat(@ceil(width + frac_x)); - const px_height: u32 = @intFromFloat(@ceil(height + frac_y)); + // our drawn glyph, including the fractional offset and font smoothing. + const px_width = @as(u32, @intFromFloat(@ceil(width + frac_x))) + (2 * canvas_padding); + const px_height = @as(u32, @intFromFloat(@ceil(height + frac_y))) + (2 * canvas_padding); // Settings that are specific to if we are rendering text or emoji. const color: struct { @@ -526,8 +521,8 @@ pub const Face = struct { // `drawGlyphs`, we pass the negated bearings. context.translateCTM( ctx, - frac_x, - frac_y, + frac_x + @as(f64, @floatFromInt(canvas_padding)), + frac_y + @as(f64, @floatFromInt(canvas_padding)), ); // Scale the drawing context so that when we draw diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 8be7647e5..55fac7a9d 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -429,6 +429,17 @@ pub const Face = struct { try self.face.loadGlyph(glyph_index, self.glyphLoadFlags(opts.constraint.doesAnything())); const glyph = self.face.handle.*.glyph; + // For synthetic bold, we embolden the glyph. + if (self.synthetic.bold) { + // We need to scale the embolden amount based on the font size. + // This is a heuristic I found worked well across a variety of + // founts: 1 pixel per 64 units of height. + const font_height: f64 = @floatFromInt(self.face.handle.*.size.*.metrics.height); + const ratio: f64 = 64.0 / 2048.0; + const amount = @ceil(font_height * ratio); + _ = freetype.c.FT_Outline_Embolden(&glyph.*.outline, @intFromFloat(amount)); + } + // We get a rect that represents the position // and size of the glyph before any changes. const rect = getGlyphSize(glyph); @@ -447,17 +458,6 @@ pub const Face = struct { .atlas_y = 0, }; - // For synthetic bold, we embolden the glyph. - if (self.synthetic.bold) { - // We need to scale the embolden amount based on the font size. - // This is a heuristic I found worked well across a variety of - // founts: 1 pixel per 64 units of height. - const font_height: f64 = @floatFromInt(self.face.handle.*.size.*.metrics.height); - const ratio: f64 = 64.0 / 2048.0; - const amount = @ceil(font_height * ratio); - _ = freetype.c.FT_Outline_Embolden(&glyph.*.outline, @intFromFloat(amount)); - } - const metrics = opts.grid_metrics; const cell_width: f64 = @floatFromInt(metrics.cell_width); const cell_height: f64 = @floatFromInt(metrics.cell_height); From f245574087dfc4a8f261d28dc1025b8aad40127a Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Thu, 2 Oct 2025 18:38:39 -0700 Subject: [PATCH 3/3] Fix comment --- src/font/face/freetype.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 55fac7a9d..0d2ddc366 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -441,7 +441,7 @@ pub const Face = struct { } // We get a rect that represents the position - // and size of the glyph before any changes. + // and size of the glyph before constraints. const rect = getGlyphSize(glyph); // If our glyph is smaller than a quarter pixel in either axis