diff --git a/src/font/Metrics.zig b/src/font/Metrics.zig index a3d3f54f7..9f6df9dc3 100644 --- a/src/font/Metrics.zig +++ b/src/font/Metrics.zig @@ -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 diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index bd7e16e0d..cb9993cbf 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -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, }; } diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 4fb82c502..82cf107c8 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -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, }; }