font: Measure ascii height and use to upper bound ic_width (#8720)
Measure the ~actual height between the ascender and descender lines as the height of the~ overall bounding box of the font's ASCII characters[^1], and use this to upper bound the IC width estimate. This ensures that the scaling of fallback CJK fonts to a wide-aspect primary font doesn't result in oversized CJK glyphs. Fixes #8709. I'd appreciate feedback from Chinese speakers on the suitability of this metric across a variety of primary fonts. Screenshots using an empty Ghostty config on macOS and the test file from #8651. The font loaded as fallback for CJK is PingFang SC. **1.2.0** <img width="592" height="301" alt="Screenshot 2025-09-17 at 09 51 43" src="https://github.com/user-attachments/assets/e553885d-009a-4205-88c9-24747b195211" /> **This PR** <img width="592" height="301" alt="Screenshot 2025-09-17 at 09 51 25" src="https://github.com/user-attachments/assets/3a8e8d95-ec0a-4d23-a5f0-85b2f47253e3" /> [^1]: Note that this may be different from the difference between the nominal ascent - descent, as non-letter ASCII characters often exceed the ascender and descnder lines, and fonts often bake the line gap into the ascent/descent and set `line_gap` to zero, as per official recommendations like those from Google Fonts: https://simoncozens.github.io/gf-docs/metrics.htmlpull/8749/head
commit
1efde5caba
|
|
@ -117,6 +117,16 @@ pub const FaceMetrics = struct {
|
|||
/// lowercase x glyph.
|
||||
ex_height: ?f64 = null,
|
||||
|
||||
/// The measured height of the bounding box containing all printable
|
||||
/// ASCII characters. This can be different from ascent - descent for
|
||||
/// two reasons: non-letter symbols like @ and $ often exceed the
|
||||
/// the ascender and descender lines; and fonts often bake the line
|
||||
/// gap into the ascent and descent metrics (as per, e.g., the Google
|
||||
/// Fonts guidelines: https://simoncozens.github.io/gf-docs/metrics.html).
|
||||
///
|
||||
/// Positive value in px
|
||||
ascii_height: ?f64 = null,
|
||||
|
||||
/// The width of the character "水" (CJK water ideograph, U+6C34),
|
||||
/// if present. This is used for font size adjustment, to normalize
|
||||
/// the width of CJK fonts mixed with latin fonts.
|
||||
|
|
@ -144,11 +154,20 @@ pub const FaceMetrics = struct {
|
|||
return 0.75 * self.capHeight();
|
||||
}
|
||||
|
||||
/// Convenience function for getting the ASCII height. If we
|
||||
/// couldn't measure this, we use 1.5 * cap_height as our
|
||||
/// estimator, based on measurements across programming fonts.
|
||||
pub inline fn asciiHeight(self: FaceMetrics) f64 {
|
||||
if (self.ascii_height) |value| if (value > 0) return value;
|
||||
return 1.5 * self.capHeight();
|
||||
}
|
||||
|
||||
/// Convenience function for getting the ideograph width. If this is
|
||||
/// not defined in the font, we estimate it as two cell widths.
|
||||
/// not defined in the font, we estimate it as the minimum of the
|
||||
/// ascii height and two cell widths.
|
||||
pub inline fn icWidth(self: FaceMetrics) f64 {
|
||||
if (self.ic_width) |value| if (value > 0) return value;
|
||||
return 2 * self.cell_width;
|
||||
return @min(self.asciiHeight(), 2 * self.cell_width);
|
||||
}
|
||||
|
||||
/// Convenience function for getting the underline thickness. If
|
||||
|
|
|
|||
|
|
@ -775,7 +775,10 @@ pub const Face = struct {
|
|||
// Cell width is calculated by calculating the widest width of the
|
||||
// visible ASCII characters. Usually 'M' is widest but we just take
|
||||
// whatever is widest.
|
||||
const cell_width: f64 = cell_width: {
|
||||
//
|
||||
// ASCII height is calculated as the height of the overall bounding
|
||||
// box of the same characters.
|
||||
const cell_width: f64, const ascii_height: f64 = measurements: {
|
||||
// Build a comptime array of all the ASCII chars
|
||||
const unichars = comptime unichars: {
|
||||
const len = 127 - 32;
|
||||
|
|
@ -803,7 +806,10 @@ pub const Face = struct {
|
|||
max = @max(advances[i].width, max);
|
||||
}
|
||||
|
||||
break :cell_width max;
|
||||
// Get the overall bounding rect for the glyphs
|
||||
const rect = ct_font.getBoundingRectsForGlyphs(.horizontal, &glyphs, null);
|
||||
|
||||
break :measurements .{ max, rect.size.height };
|
||||
};
|
||||
|
||||
// Measure "水" (CJK water ideograph, U+6C34) for our ic width.
|
||||
|
|
@ -864,6 +870,7 @@ pub const Face = struct {
|
|||
|
||||
.cap_height = cap_height,
|
||||
.ex_height = ex_height,
|
||||
.ascii_height = ascii_height,
|
||||
.ic_width = ic_width,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -960,13 +960,19 @@ pub const Face = struct {
|
|||
// visible ASCII characters. Usually 'M' is widest but we just take
|
||||
// whatever is widest.
|
||||
//
|
||||
// ASCII height is calculated as the height of the overall bounding
|
||||
// box of the same characters.
|
||||
//
|
||||
// If we fail to load any visible ASCII we just use max_advance from
|
||||
// the metrics provided by FreeType.
|
||||
const cell_width: f64 = cell_width: {
|
||||
// the metrics provided by FreeType, and set ascii_height to null as
|
||||
// it's optional.
|
||||
const cell_width: f64, const ascii_height: ?f64 = measurements: {
|
||||
self.ft_mutex.lock();
|
||||
defer self.ft_mutex.unlock();
|
||||
|
||||
var max: f64 = 0.0;
|
||||
var top: f64 = 0.0;
|
||||
var bottom: f64 = 0.0;
|
||||
var c: u8 = ' ';
|
||||
while (c < 127) : (c += 1) {
|
||||
if (face.getCharIndex(c)) |glyph_index| {
|
||||
|
|
@ -974,20 +980,37 @@ pub const Face = struct {
|
|||
.render = false,
|
||||
.no_svg = true,
|
||||
})) {
|
||||
const glyph = face.handle.*.glyph;
|
||||
max = @max(
|
||||
f26dot6ToF64(face.handle.*.glyph.*.advance.x),
|
||||
f26dot6ToF64(glyph.*.advance.x),
|
||||
max,
|
||||
);
|
||||
top = @max(
|
||||
f26dot6ToF64(glyph.*.metrics.horiBearingY),
|
||||
top,
|
||||
);
|
||||
bottom = @min(
|
||||
f26dot6ToF64(glyph.*.metrics.horiBearingY - glyph.*.metrics.height),
|
||||
bottom,
|
||||
);
|
||||
} else |_| {}
|
||||
}
|
||||
}
|
||||
|
||||
// If we couldn't get any widths, just use FreeType's max_advance.
|
||||
// If we couldn't get valid measurements, just use
|
||||
// FreeType's max_advance and null, respectively.
|
||||
if (max == 0.0) {
|
||||
break :cell_width f26dot6ToF64(size_metrics.max_advance);
|
||||
max = f26dot6ToF64(size_metrics.max_advance);
|
||||
}
|
||||
const rect_height: ?f64 = rect_height: {
|
||||
const estimate = top - bottom;
|
||||
if (estimate <= 0.0) {
|
||||
break :rect_height null;
|
||||
}
|
||||
break :rect_height estimate;
|
||||
};
|
||||
|
||||
break :cell_width max;
|
||||
break :measurements .{ max, rect_height };
|
||||
};
|
||||
|
||||
// We use the cap and ex heights specified by the font if they're
|
||||
|
|
@ -1089,6 +1112,7 @@ pub const Face = struct {
|
|||
|
||||
.cap_height = cap_height,
|
||||
.ex_height = ex_height,
|
||||
.ascii_height = ascii_height,
|
||||
.ic_width = ic_width,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue