From 80e52aa6f02a1d3c7ddf69fb55d736875aa76d33 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Tue, 15 Jul 2025 13:04:17 -0700 Subject: [PATCH] Make normalization reference metric customizable per font --- src/font/Collection.zig | 49 ++++++++++++++++---------------------- src/font/face.zig | 20 ++++++++++++++++ src/font/face/coretext.zig | 5 ++++ src/font/face/freetype.zig | 5 ++++ 4 files changed, 51 insertions(+), 28 deletions(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index ac36be36f..108ccb6b7 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -124,8 +124,7 @@ pub const AdjustSizeError = font.Face.GetMetricsError; // Calculate a size for the provided face that will match it with the primary // font, metrically, to improve consistency with fallback fonts. Right now we -// match the font based on the ex height, or the ideograph width if the font -// has ideographs in it. +// match the font based on the a preferred metric specified per fallback font. // // This returns null if load options is null or if self.load_options is null. // @@ -134,7 +133,7 @@ pub const AdjustSizeError = font.Face.GetMetricsError; // // TODO: In the future, provide config options that allow the user to select // which metric should be matched for fallback fonts, instead of hard -// coding it as ex height. +// coding it in the font init code. pub fn adjustedSize( self: *Collection, face: *Face, @@ -151,26 +150,26 @@ pub fn adjustedSize( const primary_metrics = try primary_face.getMetrics(); const face_metrics = try face.getMetrics(); - // We use the ex height to match our font sizes, so that the height of - // lower-case letters matches between all fonts in the fallback chain. - // - // We estimate ex height as 0.75 * cap height if it's not specifically - // provided, and we estimate cap height as 0.75 * ascent in the same case. - // - // If the fallback font has an ic_width we prefer that, for normalization - // of CJK font sizes when mixed with latin fonts. - // - // We estimate the ic_width as twice the cell width if it isn't provided. - const primary_ex = primary_metrics.exHeight(); - const primary_ic = primary_metrics.icWidth(); - - const face_ex = face_metrics.exHeight(); - const face_ic = face_metrics.icWidth(); + // The preferred metric to normalize by is specified by + // face.reference_metric, however we don't want to normalize by a + // metric not explicitly defined in `face`, so if needed we fall + // back through the other possible reference metrics in the order + // shown in the switch statement below. If the reference metric is + // not defined in the primary font, we use the default estimate from + // the face metrics. + const line_height_scale = primary_metrics.lineHeight() / face_metrics.lineHeight(); + const scale: f64 = normalize_by: switch (face.reference_metric) { + .ic_width => if (face_metrics.ic_width) |value| primary_metrics.icWidth() / value else continue :normalize_by .ex_height, + .ex_height => if (face_metrics.ex_height) |value| primary_metrics.exHeight() / value else continue :normalize_by .cap_height, + .cap_height => if (face_metrics.cap_height) |value| primary_metrics.capHeight() / value else continue :normalize_by .line_height, + .line_height => line_height_scale, + .em_size => 1.0, + }; // If the line height of the scaled font would be larger than // the line height of the primary font, we don't want that, so - // we take the minimum between matching the ic/ex and the line - // height. + // we take the minimum between matching the reference metric + // and keeping the line heights within some margin. // // NOTE: We actually allow the line height to be up to 1.2 // times the primary line height because empirically @@ -178,19 +177,13 @@ pub fn adjustedSize( // // TODO: We should probably provide a config option that lets // the user pick what metric to use for size adjustment. - const scale = @min( - 1.2 * primary_metrics.lineHeight() / face_metrics.lineHeight(), - if (face_metrics.ic_width != null) - primary_ic / face_ic - else - primary_ex / face_ex, - ); + const capped_scale = @min(scale, 1.2 * line_height_scale); // Make a copy of our load options, set the size to the size of // the provided face, and then multiply that by our scaling factor. var opts = load_options; opts.size = face.size; - opts.size.points *= @as(f32, @floatCast(scale)); + opts.size.points *= @as(f32, @floatCast(capped_scale)); return opts; } diff --git a/src/font/face.zig b/src/font/face.zig index fc5118c3d..1642095ce 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -39,6 +39,12 @@ pub const freetype_load_flags_default: FreetypeLoadFlags = if (FreetypeLoadFlags pub const Options = struct { size: DesiredSize, freetype_load_flags: FreetypeLoadFlags = freetype_load_flags_default, + + // reference_metric defaults to ic_width to ensure appropriate + // normalization of CJK font sizes when mixed with latin fonts. See + // the implementation of Collections.adjustedSize() for fallback + // rules when the font does not define the specified metric. + reference_metric: ReferenceMetric = .ic_width, }; /// The desired size for loading a font. @@ -57,6 +63,20 @@ pub const DesiredSize = struct { } }; +pub const ReferenceMetric = enum { + // The font's ideograph width + ic_width, + // The font's ex height + ex_height, + // The font's cap height + cap_height, + // The font's line height + line_height, + // The font's em size (i.e., what point sizes like 12 refer to). + // Usually equivalent to line height, but that's just convention. + em_size, +}; + /// A font variation setting. The best documentation for this I know of /// is actually the CSS font-variation-settings property on MDN: /// https://developer.mozilla.org/en-US/docs/Web/CSS/font-variation-settings diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index bb9a472d2..51956a55c 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -34,6 +34,10 @@ pub const Face = struct { /// The current size this font is set to. size: font.face.DesiredSize, + // The preferred font metric to use when normalizing the size of a + // fallback font to the primary font. + reference_metric: font.face.ReferenceMetric, + /// True if our build is using Harfbuzz. If we're not, we can avoid /// some Harfbuzz-specific code paths. const harfbuzz_shaper = font.options.backend.hasHarfbuzz(); @@ -110,6 +114,7 @@ pub const Face = struct { .hb_font = hb_font, .color = color, .size = opts.size, + .reference_metric = opts.reference_metric, }; result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result); diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index f42868e5c..4eb3a091a 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -62,6 +62,10 @@ pub const Face = struct { /// The current size this font is set to. size: font.face.DesiredSize, + // The preferred font metric to use when normalizing the size of a + // fallback font to the primary font. + reference_metric: font.face.ReferenceMetric, + /// Initialize a new font face with the given source in-memory. pub fn initFile( lib: Library, @@ -111,6 +115,7 @@ pub const Face = struct { .ft_mutex = ft_mutex, .load_flags = opts.freetype_load_flags, .size = opts.size, + .reference_metric = opts.reference_metric, }; result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);