font(freetype): tolerate fonts without OS/2 tables

This is more common than anticipated, so proper fallback logic has been
added. Why must fonts be like this? -.-
This commit is contained in:
Qwerasd
2024-12-16 14:52:56 -05:00
parent 5cd214066d
commit 13e4861dff

View File

@ -600,7 +600,6 @@ pub const Face = struct {
const CalcMetricsError = error{ const CalcMetricsError = error{
CopyTableError, CopyTableError,
MissingOS2Table,
}; };
/// Calculate the metrics associated with a face. This is not public because /// Calculate the metrics associated with a face. This is not public because
@ -629,70 +628,80 @@ pub const Face = struct {
const post = face.getSfntTable(.post) orelse return error.CopyTableError; const post = face.getSfntTable(.post) orelse return error.CopyTableError;
// Read the 'OS/2' table out of the font data. // Read the 'OS/2' table out of the font data.
const os2 = face.getSfntTable(.os2) orelse return error.CopyTableError; const maybe_os2: ?*freetype.c.TT_OS2 = os2: {
const os2 = face.getSfntTable(.os2) orelse break :os2 null;
if (os2.version == 0xFFFF) break :os2 null;
break :os2 os2;
};
// Read the 'hhea' table out of the font data. // Read the 'hhea' table out of the font data.
const hhea = face.getSfntTable(.hhea) orelse return error.CopyTableError; const hhea = face.getSfntTable(.hhea) orelse return error.CopyTableError;
// Some fonts don't actually have an OS/2 table, which
// we need in order to do the metrics calculations, in
// such cases FreeType sets the version to 0xFFFF
if (os2.version == 0xFFFF) return error.MissingOS2Table;
const units_per_em = head.Units_Per_EM; const units_per_em = head.Units_Per_EM;
const px_per_em: f64 = @floatFromInt(size_metrics.y_ppem); const px_per_em: f64 = @floatFromInt(size_metrics.y_ppem);
const px_per_unit = px_per_em / @as(f64, @floatFromInt(units_per_em)); const px_per_unit = px_per_em / @as(f64, @floatFromInt(units_per_em));
const ascent: f64, const descent: f64, const line_gap: f64 = vertical_metrics: { const ascent: f64, const descent: f64, const line_gap: f64 = vertical_metrics: {
const os2_ascent: f64 = @floatFromInt(os2.sTypoAscender); const hhea_ascent: f64 = @floatFromInt(hhea.Ascender);
const os2_descent: f64 = @floatFromInt(os2.sTypoDescender); const hhea_descent: f64 = @floatFromInt(hhea.Descender);
const os2_line_gap: f64 = @floatFromInt(os2.sTypoLineGap); const hhea_line_gap: f64 = @floatFromInt(hhea.Line_Gap);
// If the font says to use typo metrics, trust it. if (maybe_os2) |os2| {
// (The USE_TYPO_METRICS bit is bit 7) const os2_ascent: f64 = @floatFromInt(os2.sTypoAscender);
if (os2.fsSelection & (1 << 7) != 0) { const os2_descent: f64 = @floatFromInt(os2.sTypoDescender);
const os2_line_gap: f64 = @floatFromInt(os2.sTypoLineGap);
// If the font says to use typo metrics, trust it.
// (The USE_TYPO_METRICS bit is bit 7)
if (os2.fsSelection & (1 << 7) != 0) {
break :vertical_metrics .{
os2_ascent * px_per_unit,
os2_descent * px_per_unit,
os2_line_gap * px_per_unit,
};
}
// Otherwise we prefer the height metrics from 'hhea' if they
// are available, or else OS/2 sTypo* metrics, and if all else
// fails then we use OS/2 usWin* metrics.
//
// This is not "standard" behavior, but it's our best bet to
// account for fonts being... just weird. It's pretty much what
// FreeType does to get its generic ascent and descent metrics.
if (hhea.Ascender != 0 or hhea.Descender != 0) {
break :vertical_metrics .{
hhea_ascent * px_per_unit,
hhea_descent * px_per_unit,
hhea_line_gap * px_per_unit,
};
}
if (os2_ascent != 0 or os2_descent != 0) {
break :vertical_metrics .{
os2_ascent * px_per_unit,
os2_descent * px_per_unit,
os2_line_gap * px_per_unit,
};
}
const win_ascent: f64 = @floatFromInt(os2.usWinAscent);
const win_descent: f64 = @floatFromInt(os2.usWinDescent);
break :vertical_metrics .{ break :vertical_metrics .{
os2_ascent * px_per_unit, win_ascent * px_per_unit,
os2_descent * px_per_unit, // usWinDescent is *positive* -> down unlike sTypoDescender
os2_line_gap * px_per_unit, // and hhea.Descender, so we flip its sign to fix this.
-win_descent * px_per_unit,
0.0,
}; };
} }
// Otherwise we prefer the height metrics from 'hhea' if they // If our font has no OS/2 table, then we just
// are available, or else OS/2 sTypo* metrics, and if all else // blindly use the metrics from the hhea table.
// fails then we use OS/2 usWin* metrics.
//
// This is not "standard" behavior, but it's our best bet to
// account for fonts being... just weird. It's pretty much what
// FreeType does to get its generic ascent and descent metrics.
if (hhea.Ascender != 0 or hhea.Descender != 0) {
const hhea_ascent: f64 = @floatFromInt(hhea.Ascender);
const hhea_descent: f64 = @floatFromInt(hhea.Descender);
const hhea_line_gap: f64 = @floatFromInt(hhea.Line_Gap);
break :vertical_metrics .{
hhea_ascent * px_per_unit,
hhea_descent * px_per_unit,
hhea_line_gap * px_per_unit,
};
}
if (os2_ascent != 0 or os2_descent != 0) {
break :vertical_metrics .{
os2_ascent * px_per_unit,
os2_descent * px_per_unit,
os2_line_gap * px_per_unit,
};
}
const win_ascent: f64 = @floatFromInt(os2.usWinAscent);
const win_descent: f64 = @floatFromInt(os2.usWinDescent);
break :vertical_metrics .{ break :vertical_metrics .{
win_ascent * px_per_unit, hhea_ascent * px_per_unit,
// usWinDescent is *positive* -> down unlike sTypoDescender hhea_descent * px_per_unit,
// and hhea.Descender, so we flip its sign to fix this. hhea_line_gap * px_per_unit,
-win_descent * px_per_unit,
0.0,
}; };
}; };
@ -714,17 +723,25 @@ pub const Face = struct {
@as(f64, @floatFromInt(post.underlineThickness)) * px_per_unit; @as(f64, @floatFromInt(post.underlineThickness)) * px_per_unit;
// Similar logic to the underline above. // Similar logic to the underline above.
const has_broken_strikethrough = os2.yStrikeoutSize == 0; const strikethrough_position, const strikethrough_thickness = st: {
if (maybe_os2) |os2| {
const has_broken_strikethrough = os2.yStrikeoutSize == 0;
const strikethrough_position = if (has_broken_strikethrough and os2.yStrikeoutPosition == 0) const pos: ?f64 = if (has_broken_strikethrough and os2.yStrikeoutPosition == 0)
null null
else else
@as(f64, @floatFromInt(os2.yStrikeoutPosition)) * px_per_unit; @as(f64, @floatFromInt(os2.yStrikeoutPosition)) * px_per_unit;
const strikethrough_thickness = if (has_broken_strikethrough) const thick: ?f64 = if (has_broken_strikethrough)
null null
else else
@as(f64, @floatFromInt(os2.yStrikeoutSize)) * px_per_unit; @as(f64, @floatFromInt(os2.yStrikeoutSize)) * px_per_unit;
break :st .{ pos, thick };
}
break :st .{ null, null };
};
// Cell width is calculated by calculating the widest width of the // Cell width is calculated by calculating the widest width of the
// visible ASCII characters. Usually 'M' is widest but we just take // visible ASCII characters. Usually 'M' is widest but we just take
@ -754,37 +771,33 @@ pub const Face = struct {
break :cell_width max; break :cell_width max;
}; };
// The OS/2 table does not include sCapHeight or sxHeight in version 1. // We use the cap and ex heights specified by the font if they're
const has_os2_height_metrics = os2.version >= 2; // available, otherwise we try to measure the `H` and `x` glyphs.
const cap_height: ?f64, const ex_height: ?f64 = heights: {
// We use the cap height specified by the font if it's if (maybe_os2) |os2| {
// available, otherwise we try to measure the `H` glyph. break :heights .{
const cap_height: ?f64 = cap_height: { @as(f64, @floatFromInt(os2.sCapHeight)) * px_per_unit,
if (has_os2_height_metrics) { @as(f64, @floatFromInt(os2.sxHeight)) * px_per_unit,
break :cap_height @as(f64, @floatFromInt(os2.sCapHeight)) * px_per_unit; };
} }
if (face.getCharIndex('H')) |glyph_index| { break :heights .{
if (face.loadGlyph(glyph_index, .{ .render = true })) { cap: {
break :cap_height f26dot6ToF64(face.handle.*.glyph.*.metrics.height); if (face.getCharIndex('H')) |glyph_index| {
} else |_| {} if (face.loadGlyph(glyph_index, .{ .render = true })) {
} break :cap f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
} else |_| {}
break :cap_height null; }
}; break :cap null;
},
// We use the ex height specified by the font if it's ex: {
// available, otherwise we try to measure the `x` glyph. if (face.getCharIndex('x')) |glyph_index| {
const ex_height: ?f64 = ex_height: { if (face.loadGlyph(glyph_index, .{ .render = true })) {
if (has_os2_height_metrics) { break :ex f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
break :ex_height @as(f64, @floatFromInt(os2.sxHeight)) * px_per_unit; } else |_| {}
} }
if (face.getCharIndex('x')) |glyph_index| { break :ex null;
if (face.loadGlyph(glyph_index, .{ .render = true })) { },
break :ex_height f26dot6ToF64(face.handle.*.glyph.*.metrics.height); };
} else |_| {}
}
break :ex_height null;
}; };
var result = font.face.Metrics.calc(.{ var result = font.face.Metrics.calc(.{