Fix font handling for bitmap and non-sfnt fonts (#8512)
Fixes #8483, fixes #2991 With this change, `font.face.getMetrics` is now infallible, and real bitmap fonts are properly handled and can be configured as the primary font (or used as fallbacks), as long as the backend (FreeType or CoreText) knows how to interpret them, since we now fall back on the backend for any metrics we can't extract from sfnt tables (which means we don't need any to be present in the first place). Also, doing this uncovered a double-free issue in our FreeType `renderGlyph` code, which thankfully wasn't too hard to track down and fix. > [!NOTE] > We should vendor a true bitmap font in each of the native formats supported by each backend and add tests for the metrics being computed right and the glyphs being rendered correctly. Idk if we wanna block this PR on that or not.pull/8526/head
commit
e2504d9cbf
|
|
@ -92,7 +92,6 @@ pub const AddOptions = struct {
|
||||||
|
|
||||||
pub const AddError =
|
pub const AddError =
|
||||||
Allocator.Error ||
|
Allocator.Error ||
|
||||||
Face.GetMetricsError ||
|
|
||||||
error{
|
error{
|
||||||
/// There's no more room in the collection.
|
/// There's no more room in the collection.
|
||||||
CollectionFull,
|
CollectionFull,
|
||||||
|
|
@ -127,7 +126,7 @@ pub fn add(
|
||||||
|
|
||||||
// Scale factor to adjust the size of the added face.
|
// Scale factor to adjust the size of the added face.
|
||||||
const scale_factor = self.scaleFactor(
|
const scale_factor = self.scaleFactor(
|
||||||
try owned_face.getMetrics(),
|
owned_face.getMetrics(),
|
||||||
opts.size_adjustment,
|
opts.size_adjustment,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -225,7 +224,7 @@ fn getFaceFromEntry(
|
||||||
// entry now that we have a loaded face.
|
// entry now that we have a loaded face.
|
||||||
entry.scale_factor = .{
|
entry.scale_factor = .{
|
||||||
.scale = self.scaleFactor(
|
.scale = self.scaleFactor(
|
||||||
try face.getMetrics(),
|
face.getMetrics(),
|
||||||
entry.scale_factor.adjustment,
|
entry.scale_factor.adjustment,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
@ -592,7 +591,7 @@ fn scaleFactor(
|
||||||
@branchHint(.unlikely);
|
@branchHint(.unlikely);
|
||||||
// If we can't load the primary face, just use 1.0 as the scale factor.
|
// If we can't load the primary face, just use 1.0 as the scale factor.
|
||||||
const primary_face = self.getFace(.{ .idx = 0 }) catch return 1.0;
|
const primary_face = self.getFace(.{ .idx = 0 }) catch return 1.0;
|
||||||
self.primary_face_metrics = primary_face.getMetrics() catch return 1.0;
|
self.primary_face_metrics = primary_face.getMetrics();
|
||||||
}
|
}
|
||||||
|
|
||||||
const primary_metrics = self.primary_face_metrics.?;
|
const primary_metrics = self.primary_face_metrics.?;
|
||||||
|
|
@ -652,7 +651,7 @@ fn scaleFactor(
|
||||||
return primary_metric / face_metric;
|
return primary_metric / face_metric;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UpdateMetricsError = font.Face.GetMetricsError || error{
|
const UpdateMetricsError = error{
|
||||||
CannotLoadPrimaryFont,
|
CannotLoadPrimaryFont,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -663,7 +662,7 @@ const UpdateMetricsError = font.Face.GetMetricsError || error{
|
||||||
pub fn updateMetrics(self: *Collection) UpdateMetricsError!void {
|
pub fn updateMetrics(self: *Collection) UpdateMetricsError!void {
|
||||||
const primary_face = self.getFace(.{ .idx = 0 }) catch return error.CannotLoadPrimaryFont;
|
const primary_face = self.getFace(.{ .idx = 0 }) catch return error.CannotLoadPrimaryFont;
|
||||||
|
|
||||||
self.primary_face_metrics = try primary_face.getMetrics();
|
self.primary_face_metrics = primary_face.getMetrics();
|
||||||
|
|
||||||
var metrics = Metrics.calc(self.primary_face_metrics.?);
|
var metrics = Metrics.calc(self.primary_face_metrics.?);
|
||||||
|
|
||||||
|
|
@ -1288,8 +1287,8 @@ test "adjusted sizes" {
|
||||||
|
|
||||||
// The chosen metric should match.
|
// The chosen metric should match.
|
||||||
{
|
{
|
||||||
const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics();
|
const primary_metrics = (try c.getFace(.{ .idx = 0 })).getMetrics();
|
||||||
const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics();
|
const fallback_metrics = (try c.getFace(fallback_idx)).getMetrics();
|
||||||
|
|
||||||
try std.testing.expectApproxEqAbs(
|
try std.testing.expectApproxEqAbs(
|
||||||
@field(primary_metrics, metric).?,
|
@field(primary_metrics, metric).?,
|
||||||
|
|
@ -1302,8 +1301,8 @@ test "adjusted sizes" {
|
||||||
// Resize should keep that relationship.
|
// Resize should keep that relationship.
|
||||||
try c.setSize(.{ .points = 37, .xdpi = 96, .ydpi = 96 });
|
try c.setSize(.{ .points = 37, .xdpi = 96, .ydpi = 96 });
|
||||||
{
|
{
|
||||||
const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics();
|
const primary_metrics = (try c.getFace(.{ .idx = 0 })).getMetrics();
|
||||||
const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics();
|
const fallback_metrics = (try c.getFace(fallback_idx)).getMetrics();
|
||||||
|
|
||||||
try std.testing.expectApproxEqAbs(
|
try std.testing.expectApproxEqAbs(
|
||||||
@field(primary_metrics, metric).?,
|
@field(primary_metrics, metric).?,
|
||||||
|
|
@ -1359,8 +1358,8 @@ test "adjusted sizes" {
|
||||||
|
|
||||||
// Test fallback to lineHeight() (ex_height and cap_height not defined in symbols font).
|
// Test fallback to lineHeight() (ex_height and cap_height not defined in symbols font).
|
||||||
{
|
{
|
||||||
const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics();
|
const primary_metrics = (try c.getFace(.{ .idx = 0 })).getMetrics();
|
||||||
const symbol_metrics = try (try c.getFace(symbol_idx)).getMetrics();
|
const symbol_metrics = (try c.getFace(symbol_idx)).getMetrics();
|
||||||
|
|
||||||
try std.testing.expectApproxEqAbs(
|
try std.testing.expectApproxEqAbs(
|
||||||
primary_metrics.lineHeight(),
|
primary_metrics.lineHeight(),
|
||||||
|
|
|
||||||
|
|
@ -574,19 +574,12 @@ pub const Face = struct {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const GetMetricsError = error{
|
|
||||||
CopyTableError,
|
|
||||||
InvalidHeadTable,
|
|
||||||
InvalidPostTable,
|
|
||||||
InvalidHheaTable,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Get the `FaceMetrics` for this face.
|
/// Get the `FaceMetrics` for this face.
|
||||||
pub fn getMetrics(self: *Face) GetMetricsError!font.Metrics.FaceMetrics {
|
pub fn getMetrics(self: *Face) font.Metrics.FaceMetrics {
|
||||||
const ct_font = self.font;
|
const ct_font = self.font;
|
||||||
|
|
||||||
// Read the 'head' table out of the font data.
|
// Read the 'head' table out of the font data.
|
||||||
const head: opentype.Head = head: {
|
const head_: ?opentype.Head = head: {
|
||||||
// macOS bitmap-only fonts use a 'bhed' tag rather than 'head', but
|
// macOS bitmap-only fonts use a 'bhed' tag rather than 'head', but
|
||||||
// the table format is byte-identical to the 'head' table, so if we
|
// the table format is byte-identical to the 'head' table, so if we
|
||||||
// can't find 'head' we try 'bhed' instead before failing.
|
// can't find 'head' we try 'bhed' instead before failing.
|
||||||
|
|
@ -597,29 +590,26 @@ pub const Face = struct {
|
||||||
const data =
|
const data =
|
||||||
ct_font.copyTable(head_tag) orelse
|
ct_font.copyTable(head_tag) orelse
|
||||||
ct_font.copyTable(bhed_tag) orelse
|
ct_font.copyTable(bhed_tag) orelse
|
||||||
return error.CopyTableError;
|
break :head null;
|
||||||
defer data.release();
|
defer data.release();
|
||||||
const ptr = data.getPointer();
|
const ptr = data.getPointer();
|
||||||
const len = data.getLength();
|
const len = data.getLength();
|
||||||
break :head opentype.Head.init(ptr[0..len]) catch |err| {
|
break :head opentype.Head.init(ptr[0..len]) catch |err| {
|
||||||
return switch (err) {
|
log.warn("error parsing head table: {}", .{err});
|
||||||
error.EndOfStream,
|
break :head null;
|
||||||
=> error.InvalidHeadTable,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Read the 'post' table out of the font data.
|
// Read the 'post' table out of the font data.
|
||||||
const post: opentype.Post = post: {
|
const post_: ?opentype.Post = post: {
|
||||||
const tag = macos.text.FontTableTag.init("post");
|
const tag = macos.text.FontTableTag.init("post");
|
||||||
const data = ct_font.copyTable(tag) orelse return error.CopyTableError;
|
const data = ct_font.copyTable(tag) orelse break :post null;
|
||||||
defer data.release();
|
defer data.release();
|
||||||
const ptr = data.getPointer();
|
const ptr = data.getPointer();
|
||||||
const len = data.getLength();
|
const len = data.getLength();
|
||||||
break :post opentype.Post.init(ptr[0..len]) catch |err| {
|
break :post opentype.Post.init(ptr[0..len]) catch |err| {
|
||||||
return switch (err) {
|
log.warn("error parsing post table: {}", .{err});
|
||||||
error.EndOfStream => error.InvalidPostTable,
|
break :post null;
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -637,96 +627,110 @@ pub const Face = struct {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Read the 'hhea' table out of the font data.
|
// Read the 'hhea' table out of the font data.
|
||||||
const hhea: opentype.Hhea = hhea: {
|
const hhea_: ?opentype.Hhea = hhea: {
|
||||||
const tag = macos.text.FontTableTag.init("hhea");
|
const tag = macos.text.FontTableTag.init("hhea");
|
||||||
const data = ct_font.copyTable(tag) orelse return error.CopyTableError;
|
const data = ct_font.copyTable(tag) orelse break :hhea null;
|
||||||
defer data.release();
|
defer data.release();
|
||||||
const ptr = data.getPointer();
|
const ptr = data.getPointer();
|
||||||
const len = data.getLength();
|
const len = data.getLength();
|
||||||
break :hhea opentype.Hhea.init(ptr[0..len]) catch |err| {
|
break :hhea opentype.Hhea.init(ptr[0..len]) catch |err| {
|
||||||
return switch (err) {
|
log.warn("error parsing hhea table: {}", .{err});
|
||||||
error.EndOfStream => error.InvalidHheaTable,
|
break :hhea null;
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const units_per_em: f64 = @floatFromInt(head.unitsPerEm);
|
const units_per_em: f64 =
|
||||||
|
if (head_) |head|
|
||||||
|
@floatFromInt(head.unitsPerEm)
|
||||||
|
else
|
||||||
|
@floatFromInt(self.font.getUnitsPerEm());
|
||||||
const px_per_em: f64 = ct_font.getSize();
|
const px_per_em: f64 = ct_font.getSize();
|
||||||
const px_per_unit: f64 = px_per_em / units_per_em;
|
const px_per_unit: f64 = px_per_em / 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: {
|
||||||
|
// If we couldn't get the hhea table, rely on metrics from CoreText.
|
||||||
|
const hhea = hhea_ orelse break :vertical_metrics .{
|
||||||
|
self.font.getAscent(),
|
||||||
|
-self.font.getDescent(),
|
||||||
|
self.font.getLeading(),
|
||||||
|
};
|
||||||
|
|
||||||
const hhea_ascent: f64 = @floatFromInt(hhea.ascender);
|
const hhea_ascent: f64 = @floatFromInt(hhea.ascender);
|
||||||
const hhea_descent: f64 = @floatFromInt(hhea.descender);
|
const hhea_descent: f64 = @floatFromInt(hhea.descender);
|
||||||
const hhea_line_gap: f64 = @floatFromInt(hhea.lineGap);
|
const hhea_line_gap: f64 = @floatFromInt(hhea.lineGap);
|
||||||
|
|
||||||
if (os2_) |os2| {
|
|
||||||
const os2_ascent: f64 = @floatFromInt(os2.sTypoAscender);
|
|
||||||
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.
|
|
||||||
if (os2.fsSelection.use_typo_metrics) 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 .{
|
|
||||||
win_ascent * px_per_unit,
|
|
||||||
// usWinDescent is *positive* -> down unlike sTypoDescender
|
|
||||||
// and hhea.Descender, so we flip its sign to fix this.
|
|
||||||
-win_descent * px_per_unit,
|
|
||||||
0.0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// If our font has no OS/2 table, then we just
|
// If our font has no OS/2 table, then we just
|
||||||
// blindly use the metrics from the hhea table.
|
// blindly use the metrics from the hhea table.
|
||||||
break :vertical_metrics .{
|
const os2 = os2_ orelse break :vertical_metrics .{
|
||||||
hhea_ascent * px_per_unit,
|
hhea_ascent * px_per_unit,
|
||||||
hhea_descent * px_per_unit,
|
hhea_descent * px_per_unit,
|
||||||
hhea_line_gap * px_per_unit,
|
hhea_line_gap * px_per_unit,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const os2_ascent: f64 = @floatFromInt(os2.sTypoAscender);
|
||||||
|
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.
|
||||||
|
if (os2.fsSelection.use_typo_metrics) 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 .{
|
||||||
|
win_ascent * px_per_unit,
|
||||||
|
// usWinDescent is *positive* -> down unlike sTypoDescender
|
||||||
|
// and hhea.Descender, so we flip its sign to fix this.
|
||||||
|
-win_descent * px_per_unit,
|
||||||
|
0.0,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Some fonts have degenerate 'post' tables where the underline
|
const underline_position, const underline_thickness = ul: {
|
||||||
// thickness (and often position) are 0. We consider them null
|
const post = post_ orelse break :ul .{ null, null };
|
||||||
// if this is the case and use our own fallbacks when we calculate.
|
|
||||||
const has_broken_underline = post.underlineThickness == 0;
|
|
||||||
|
|
||||||
// If the underline position isn't 0 then we do use it,
|
// Some fonts have degenerate 'post' tables where the underline
|
||||||
// even if the thickness is't properly specified.
|
// thickness (and often position) are 0. We consider them null
|
||||||
const underline_position: ?f64 = if (has_broken_underline and post.underlinePosition == 0)
|
// if this is the case and use our own fallbacks when we calculate.
|
||||||
null
|
const has_broken_underline = post.underlineThickness == 0;
|
||||||
else
|
|
||||||
@as(f64, @floatFromInt(post.underlinePosition)) * px_per_unit;
|
|
||||||
|
|
||||||
const underline_thickness = if (has_broken_underline)
|
// If the underline position isn't 0 then we do use it,
|
||||||
null
|
// even if the thickness is't properly specified.
|
||||||
else
|
const pos: ?f64 = if (has_broken_underline and post.underlinePosition == 0)
|
||||||
@as(f64, @floatFromInt(post.underlineThickness)) * px_per_unit;
|
null
|
||||||
|
else
|
||||||
|
@as(f64, @floatFromInt(post.underlinePosition)) * px_per_unit;
|
||||||
|
|
||||||
|
const thick: ?f64 = if (has_broken_underline)
|
||||||
|
null
|
||||||
|
else
|
||||||
|
@as(f64, @floatFromInt(post.underlineThickness)) * px_per_unit;
|
||||||
|
|
||||||
|
break :ul .{ pos, thick };
|
||||||
|
};
|
||||||
|
|
||||||
// Similar logic to the underline above.
|
// Similar logic to the underline above.
|
||||||
const strikethrough_position, const strikethrough_thickness = st: {
|
const strikethrough_position, const strikethrough_thickness = st: {
|
||||||
|
|
@ -989,7 +993,7 @@ test {
|
||||||
alloc,
|
alloc,
|
||||||
&atlas,
|
&atlas,
|
||||||
face.glyphIndex(i).?,
|
face.glyphIndex(i).?,
|
||||||
.{ .grid_metrics = font.Metrics.calc(try face.getMetrics()) },
|
.{ .grid_metrics = font.Metrics.calc(face.getMetrics()) },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1054,7 +1058,7 @@ test "in-memory" {
|
||||||
alloc,
|
alloc,
|
||||||
&atlas,
|
&atlas,
|
||||||
face.glyphIndex(i).?,
|
face.glyphIndex(i).?,
|
||||||
.{ .grid_metrics = font.Metrics.calc(try face.getMetrics()) },
|
.{ .grid_metrics = font.Metrics.calc(face.getMetrics()) },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1081,7 +1085,7 @@ test "variable" {
|
||||||
alloc,
|
alloc,
|
||||||
&atlas,
|
&atlas,
|
||||||
face.glyphIndex(i).?,
|
face.glyphIndex(i).?,
|
||||||
.{ .grid_metrics = font.Metrics.calc(try face.getMetrics()) },
|
.{ .grid_metrics = font.Metrics.calc(face.getMetrics()) },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1112,7 +1116,7 @@ test "variable set variation" {
|
||||||
alloc,
|
alloc,
|
||||||
&atlas,
|
&atlas,
|
||||||
face.glyphIndex(i).?,
|
face.glyphIndex(i).?,
|
||||||
.{ .grid_metrics = font.Metrics.calc(try face.getMetrics()) },
|
.{ .grid_metrics = font.Metrics.calc(face.getMetrics()) },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -594,6 +594,12 @@ pub const Face = struct {
|
||||||
freetype.c.FT_PIXEL_MODE_GRAY,
|
freetype.c.FT_PIXEL_MODE_GRAY,
|
||||||
=> {},
|
=> {},
|
||||||
else => {
|
else => {
|
||||||
|
// Make sure the slot owns its bitmap,
|
||||||
|
// since we'll be modifying it here.
|
||||||
|
if (freetype.c.FT_GlyphSlot_Own_Bitmap(glyph) != 0) {
|
||||||
|
return error.BitmapHandlingError;
|
||||||
|
}
|
||||||
|
|
||||||
var converted: freetype.c.FT_Bitmap = undefined;
|
var converted: freetype.c.FT_Bitmap = undefined;
|
||||||
freetype.c.FT_Bitmap_Init(&converted);
|
freetype.c.FT_Bitmap_Init(&converted);
|
||||||
if (freetype.c.FT_Bitmap_Convert(
|
if (freetype.c.FT_Bitmap_Convert(
|
||||||
|
|
@ -784,12 +790,8 @@ pub const Face = struct {
|
||||||
return @as(F26Dot6, @bitCast(@as(i32, @intCast(v)))).to(f64);
|
return @as(F26Dot6, @bitCast(@as(i32, @intCast(v)))).to(f64);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const GetMetricsError = error{
|
|
||||||
CopyTableError,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Get the `FaceMetrics` for this face.
|
/// Get the `FaceMetrics` for this face.
|
||||||
pub fn getMetrics(self: *Face) GetMetricsError!font.Metrics.FaceMetrics {
|
pub fn getMetrics(self: *Face) font.Metrics.FaceMetrics {
|
||||||
const face = self.face;
|
const face = self.face;
|
||||||
|
|
||||||
const size_metrics = face.handle.*.size.*.metrics;
|
const size_metrics = face.handle.*.size.*.metrics;
|
||||||
|
|
@ -799,10 +801,10 @@ pub const Face = struct {
|
||||||
assert(size_metrics.x_ppem == size_metrics.y_ppem);
|
assert(size_metrics.x_ppem == size_metrics.y_ppem);
|
||||||
|
|
||||||
// Read the 'head' table out of the font data.
|
// Read the 'head' table out of the font data.
|
||||||
const head = face.getSfntTable(.head) orelse return error.CopyTableError;
|
const head_ = face.getSfntTable(.head);
|
||||||
|
|
||||||
// Read the 'post' table out of the font data.
|
// Read the 'post' table out of the font data.
|
||||||
const post = face.getSfntTable(.post) orelse return error.CopyTableError;
|
const post_ = face.getSfntTable(.post);
|
||||||
|
|
||||||
// Read the 'OS/2' table out of the font data.
|
// Read the 'OS/2' table out of the font data.
|
||||||
const os2_: ?*freetype.c.TT_OS2 = os2: {
|
const os2_: ?*freetype.c.TT_OS2 = os2: {
|
||||||
|
|
@ -812,92 +814,128 @@ pub const Face = struct {
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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);
|
||||||
|
|
||||||
const units_per_em = head.Units_Per_EM;
|
// Whether the font is in a scalable format. We need to know this
|
||||||
|
// because many of the metrics provided by FreeType are invalid for
|
||||||
|
// non-scalable fonts.
|
||||||
|
const is_scalable = face.handle.*.face_flags & freetype.c.FT_FACE_FLAG_SCALABLE != 0;
|
||||||
|
|
||||||
|
// We get the UPM from the head table.
|
||||||
|
//
|
||||||
|
// If we have no head, but it is a scalable face, take the UPM from
|
||||||
|
// FreeType's units_per_EM, otherwise we'll assume that UPM == PPEM.
|
||||||
|
const units_per_em: freetype.c.FT_UShort =
|
||||||
|
if (head_) |head|
|
||||||
|
head.Units_Per_EM
|
||||||
|
else if (is_scalable)
|
||||||
|
face.handle.*.units_per_EM
|
||||||
|
else
|
||||||
|
size_metrics.y_ppem;
|
||||||
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 hhea = hhea_ orelse {
|
||||||
|
// If we couldn't get the hhea table, rely on metrics from FreeType.
|
||||||
|
const ascender = f26dot6ToF64(size_metrics.ascender);
|
||||||
|
const descender = f26dot6ToF64(size_metrics.descender);
|
||||||
|
const height = f26dot6ToF64(size_metrics.height);
|
||||||
|
break :vertical_metrics .{
|
||||||
|
ascender,
|
||||||
|
descender,
|
||||||
|
// We compute the line gap by adding the (negative) descender
|
||||||
|
// and subtracting the (positive) ascender from the line height
|
||||||
|
// to get the remaining gap size.
|
||||||
|
//
|
||||||
|
// NOTE: This might always be 0... but it doesn't hurt to do.
|
||||||
|
height + descender - ascender,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const hhea_ascent: f64 = @floatFromInt(hhea.Ascender);
|
const hhea_ascent: f64 = @floatFromInt(hhea.Ascender);
|
||||||
const hhea_descent: f64 = @floatFromInt(hhea.Descender);
|
const hhea_descent: f64 = @floatFromInt(hhea.Descender);
|
||||||
const hhea_line_gap: f64 = @floatFromInt(hhea.Line_Gap);
|
const hhea_line_gap: f64 = @floatFromInt(hhea.Line_Gap);
|
||||||
|
|
||||||
if (os2_) |os2| {
|
|
||||||
const os2_ascent: f64 = @floatFromInt(os2.sTypoAscender);
|
|
||||||
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 .{
|
|
||||||
win_ascent * px_per_unit,
|
|
||||||
// usWinDescent is *positive* -> down unlike sTypoDescender
|
|
||||||
// and hhea.Descender, so we flip its sign to fix this.
|
|
||||||
-win_descent * px_per_unit,
|
|
||||||
0.0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// If our font has no OS/2 table, then we just
|
// If our font has no OS/2 table, then we just
|
||||||
// blindly use the metrics from the hhea table.
|
// blindly use the metrics from the hhea table.
|
||||||
break :vertical_metrics .{
|
const os2 = os2_ orelse break :vertical_metrics .{
|
||||||
hhea_ascent * px_per_unit,
|
hhea_ascent * px_per_unit,
|
||||||
hhea_descent * px_per_unit,
|
hhea_descent * px_per_unit,
|
||||||
hhea_line_gap * px_per_unit,
|
hhea_line_gap * px_per_unit,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const os2_ascent: f64 = @floatFromInt(os2.sTypoAscender);
|
||||||
|
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 .{
|
||||||
|
win_ascent * px_per_unit,
|
||||||
|
// usWinDescent is *positive* -> down unlike sTypoDescender
|
||||||
|
// and hhea.Descender, so we flip its sign to fix this.
|
||||||
|
-win_descent * px_per_unit,
|
||||||
|
0.0,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Some fonts have degenerate 'post' tables where the underline
|
const underline_position: ?f64, const underline_thickness: ?f64 = ul: {
|
||||||
// thickness (and often position) are 0. We consider them null
|
const post = post_ orelse break :ul .{ null, null };
|
||||||
// if this is the case and use our own fallbacks when we calculate.
|
|
||||||
const has_broken_underline = post.underlineThickness == 0;
|
|
||||||
|
|
||||||
// If the underline position isn't 0 then we do use it,
|
// Some fonts have degenerate 'post' tables where the underline
|
||||||
// even if the thickness is't properly specified.
|
// thickness (and often position) are 0. We consider them null
|
||||||
const underline_position = if (has_broken_underline and post.underlinePosition == 0)
|
// if this is the case and use our own fallbacks when we calculate.
|
||||||
null
|
const has_broken_underline = post.underlineThickness == 0;
|
||||||
else
|
|
||||||
@as(f64, @floatFromInt(post.underlinePosition)) * px_per_unit;
|
|
||||||
|
|
||||||
const underline_thickness = if (has_broken_underline)
|
// If the underline position isn't 0 then we do use it,
|
||||||
null
|
// even if the thickness is't properly specified.
|
||||||
else
|
const pos: ?f64 = if (has_broken_underline and post.underlinePosition == 0)
|
||||||
@as(f64, @floatFromInt(post.underlineThickness)) * px_per_unit;
|
null
|
||||||
|
else
|
||||||
|
@as(f64, @floatFromInt(post.underlinePosition)) * px_per_unit;
|
||||||
|
|
||||||
|
const thick: ?f64 = if (has_broken_underline)
|
||||||
|
null
|
||||||
|
else
|
||||||
|
@as(f64, @floatFromInt(post.underlineThickness)) * px_per_unit;
|
||||||
|
|
||||||
|
break :ul .{ pos, thick };
|
||||||
|
};
|
||||||
|
|
||||||
// Similar logic to the underline above.
|
// Similar logic to the underline above.
|
||||||
const strikethrough_position, const strikethrough_thickness = st: {
|
const strikethrough_position, const strikethrough_thickness = st: {
|
||||||
|
|
@ -1085,7 +1123,7 @@ test {
|
||||||
alloc,
|
alloc,
|
||||||
&atlas,
|
&atlas,
|
||||||
ft_font.glyphIndex(i).?,
|
ft_font.glyphIndex(i).?,
|
||||||
.{ .grid_metrics = font.Metrics.calc(try ft_font.getMetrics()) },
|
.{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1095,7 +1133,7 @@ test {
|
||||||
alloc,
|
alloc,
|
||||||
&atlas,
|
&atlas,
|
||||||
ft_font.glyphIndex('A').?,
|
ft_font.glyphIndex('A').?,
|
||||||
.{ .grid_metrics = font.Metrics.calc(try ft_font.getMetrics()) },
|
.{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) },
|
||||||
);
|
);
|
||||||
try testing.expectEqual(@as(u32, 11), g1.height);
|
try testing.expectEqual(@as(u32, 11), g1.height);
|
||||||
|
|
||||||
|
|
@ -1104,7 +1142,7 @@ test {
|
||||||
alloc,
|
alloc,
|
||||||
&atlas,
|
&atlas,
|
||||||
ft_font.glyphIndex('A').?,
|
ft_font.glyphIndex('A').?,
|
||||||
.{ .grid_metrics = font.Metrics.calc(try ft_font.getMetrics()) },
|
.{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) },
|
||||||
);
|
);
|
||||||
try testing.expectEqual(@as(u32, 20), g2.height);
|
try testing.expectEqual(@as(u32, 20), g2.height);
|
||||||
}
|
}
|
||||||
|
|
@ -1131,7 +1169,7 @@ test "color emoji" {
|
||||||
alloc,
|
alloc,
|
||||||
&atlas,
|
&atlas,
|
||||||
ft_font.glyphIndex('🥸').?,
|
ft_font.glyphIndex('🥸').?,
|
||||||
.{ .grid_metrics = font.Metrics.calc(try ft_font.getMetrics()) },
|
.{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Make sure this glyph has color
|
// Make sure this glyph has color
|
||||||
|
|
@ -1191,7 +1229,7 @@ test "mono to bgra" {
|
||||||
alloc,
|
alloc,
|
||||||
&atlas,
|
&atlas,
|
||||||
3,
|
3,
|
||||||
.{ .grid_metrics = font.Metrics.calc(try ft_font.getMetrics()) },
|
.{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1255,7 +1293,7 @@ test "bitmap glyph" {
|
||||||
alloc,
|
alloc,
|
||||||
&atlas,
|
&atlas,
|
||||||
77,
|
77,
|
||||||
.{ .grid_metrics = font.Metrics.calc(try ft_font.getMetrics()) },
|
.{ .grid_metrics = font.Metrics.calc(ft_font.getMetrics()) },
|
||||||
);
|
);
|
||||||
|
|
||||||
// should render crisp
|
// should render crisp
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue