Skip to content

Commit 0bddaed

Browse files
authored
fix(font): Improve FreeType glyph measurements and add unit tests for face metrics (#8738)
Follow-up to #8720 adding * Two improvements to FreeType glyph measurements: - Ensuring that glyphs are measured with the same hinting as they are rendered, ref [#8720#issuecomment-3305408157](#8720 (comment)); - For outline glyphs, using the outline bbox instead of the built-in metrics, like `renderGlyph()`. * Basic unit tests for face metrics and their estimators, using the narrowest and widest fonts from the resource directory, Cozette Vector and Geist Mono. --- I also made one unrelated change to `freetype.zig`, replacing `@alignCast(@ptrCast(...))` with `@ptrCast(@alignCast(...))` on line 173. Autoformatting has been making this change on every save for weeks, and reverting the hunk before each commit is getting old, so I hope it's OK that I use this PR to upstream this decree from the formatter.
2 parents b643d30 + 52ef17d commit 0bddaed

File tree

4 files changed

+227
-89
lines changed

4 files changed

+227
-89
lines changed

pkg/freetype/main.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub const Library = @import("Library.zig");
99

1010
pub const Error = errors.Error;
1111
pub const Face = face.Face;
12+
pub const LoadFlags = face.LoadFlags;
1213
pub const Tag = tag.Tag;
1314
pub const mulFix = computations.mulFix;
1415

src/font/Collection.zig

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1378,3 +1378,155 @@ test "adjusted sizes" {
13781378
);
13791379
}
13801380
}
1381+
1382+
test "face metrics" {
1383+
// The web canvas backend doesn't calculate face metrics, only cell metrics
1384+
if (options.backend != .web_canvas) return error.SkipZigTest;
1385+
1386+
const testing = std.testing;
1387+
const alloc = testing.allocator;
1388+
const narrowFont = font.embedded.cozette;
1389+
const wideFont = font.embedded.geist_mono;
1390+
1391+
var lib = try Library.init(alloc);
1392+
defer lib.deinit();
1393+
1394+
var c = init();
1395+
defer c.deinit(alloc);
1396+
const size: DesiredSize = .{ .points = 12, .xdpi = 96, .ydpi = 96 };
1397+
c.load_options = .{ .library = lib, .size = size };
1398+
1399+
const narrowIndex = try c.add(alloc, try .init(
1400+
lib,
1401+
narrowFont,
1402+
.{ .size = size },
1403+
), .{
1404+
.style = .regular,
1405+
.fallback = false,
1406+
.size_adjustment = .none,
1407+
});
1408+
const wideIndex = try c.add(alloc, try .init(
1409+
lib,
1410+
wideFont,
1411+
.{ .size = size },
1412+
), .{
1413+
.style = .regular,
1414+
.fallback = false,
1415+
.size_adjustment = .none,
1416+
});
1417+
1418+
const narrowMetrics: font.Metrics.FaceMetrics = (try c.getFace(narrowIndex)).getMetrics();
1419+
const wideMetrics: font.Metrics.FaceMetrics = (try c.getFace(wideIndex)).getMetrics();
1420+
1421+
// Verify provided/measured metrics. Measured
1422+
// values are backend-dependent due to hinting.
1423+
const narrowMetricsExpected = font.Metrics.FaceMetrics{
1424+
.px_per_em = 16.0,
1425+
.cell_width = switch (options.backend) {
1426+
.freetype,
1427+
.fontconfig_freetype,
1428+
.coretext_freetype,
1429+
=> 8.0,
1430+
.coretext,
1431+
.coretext_harfbuzz,
1432+
.coretext_noshape,
1433+
=> 7.3828125,
1434+
.web_canvas => unreachable,
1435+
},
1436+
.ascent = 12.3046875,
1437+
.descent = -3.6953125,
1438+
.line_gap = 0.0,
1439+
.underline_position = -1.2265625,
1440+
.underline_thickness = 1.2265625,
1441+
.strikethrough_position = 6.15625,
1442+
.strikethrough_thickness = 1.234375,
1443+
.cap_height = 9.84375,
1444+
.ex_height = 7.3828125,
1445+
.ascii_height = switch (options.backend) {
1446+
.freetype,
1447+
.fontconfig_freetype,
1448+
.coretext_freetype,
1449+
=> 18.0625,
1450+
.coretext,
1451+
.coretext_harfbuzz,
1452+
.coretext_noshape,
1453+
=> 16.0,
1454+
.web_canvas => unreachable,
1455+
},
1456+
};
1457+
const wideMetricsExpected = font.Metrics.FaceMetrics{
1458+
.px_per_em = 16.0,
1459+
.cell_width = switch (options.backend) {
1460+
.freetype,
1461+
.fontconfig_freetype,
1462+
.coretext_freetype,
1463+
=> 10.0,
1464+
.coretext,
1465+
.coretext_harfbuzz,
1466+
.coretext_noshape,
1467+
=> 9.6,
1468+
.web_canvas => unreachable,
1469+
},
1470+
.ascent = 14.72,
1471+
.descent = -3.52,
1472+
.line_gap = 1.6,
1473+
.underline_position = -1.6,
1474+
.underline_thickness = 0.8,
1475+
.strikethrough_position = 4.24,
1476+
.strikethrough_thickness = 0.8,
1477+
.cap_height = 11.36,
1478+
.ex_height = 8.48,
1479+
.ascii_height = switch (options.backend) {
1480+
.freetype,
1481+
.fontconfig_freetype,
1482+
.coretext_freetype,
1483+
=> 16.0,
1484+
.coretext,
1485+
.coretext_harfbuzz,
1486+
.coretext_noshape,
1487+
=> 15.472000000000001,
1488+
.web_canvas => unreachable,
1489+
},
1490+
};
1491+
1492+
inline for (
1493+
.{ narrowMetricsExpected, wideMetricsExpected },
1494+
.{ narrowMetrics, wideMetrics },
1495+
) |metricsExpected, metricsActual| {
1496+
inline for (@typeInfo(font.Metrics.FaceMetrics).@"struct".fields) |field| {
1497+
const expected = @field(metricsExpected, field.name);
1498+
const actual = @field(metricsActual, field.name);
1499+
// Unwrap optional fields
1500+
const expectedValue, const actualValue = unwrap: switch (@typeInfo(field.type)) {
1501+
.optional => {
1502+
if (expected) |expectedValue| if (actual) |actualValue| {
1503+
break :unwrap .{ expectedValue, actualValue };
1504+
};
1505+
// Null values can be compared directly
1506+
try std.testing.expectEqual(expected, actual);
1507+
continue;
1508+
},
1509+
else => break :unwrap .{ expected, actual },
1510+
};
1511+
// All non-null values are floats
1512+
const eps = std.math.floatEps(@TypeOf(actualValue - expectedValue));
1513+
try std.testing.expectApproxEqRel(
1514+
expectedValue,
1515+
actualValue,
1516+
std.math.sqrt(eps),
1517+
);
1518+
}
1519+
}
1520+
1521+
// Verify estimated metrics. icWidth() should equal the smaller of
1522+
// 2 * cell_width and ascii_height. For a narrow (wide) font, the
1523+
// smaller quantity is the former (latter).
1524+
try std.testing.expectEqual(
1525+
2 * narrowMetrics.cell_width,
1526+
narrowMetrics.icWidth(),
1527+
);
1528+
try std.testing.expectEqual(
1529+
wideMetrics.ascii_height,
1530+
wideMetrics.icWidth(),
1531+
);
1532+
}

src/font/face.zig

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,14 @@ pub const Variation = struct {
9393
};
9494
};
9595

96+
/// The size and position of a glyph.
97+
pub const GlyphSize = struct {
98+
width: f64,
99+
height: f64,
100+
x: f64,
101+
y: f64,
102+
};
103+
96104
/// Additional options for rendering glyphs.
97105
pub const RenderOptions = struct {
98106
/// The metrics that are defining the grid layout. These are usually
@@ -216,14 +224,6 @@ pub const RenderOptions = struct {
216224
icon,
217225
};
218226

219-
/// The size and position of a glyph.
220-
pub const GlyphSize = struct {
221-
width: f64,
222-
height: f64,
223-
x: f64,
224-
y: f64,
225-
};
226-
227227
/// Returns true if the constraint does anything. If it doesn't,
228228
/// because it neither sizes nor positions the glyph, then this
229229
/// returns false.

0 commit comments

Comments
 (0)