fix(font): make `face.getMetrics()` infallible
Before we had a bad day if we tried to get the metrics of a bitmap font, which would happen if we ever used one as fallback because we started doing it for all fonts when we added fallback font scaling. This is a pretty easy fix and finally allows users to configure true bitmap fonts as their primary font as long as FreeType/CoreText can handle it.pull/8512/head
parent
c3e7857a2c
commit
5c1d87fda6
|
|
@ -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,114 @@ 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: {
|
||||||
const hhea_ascent: f64 = @floatFromInt(hhea.ascender);
|
if (hhea_) |hhea| {
|
||||||
const hhea_descent: f64 = @floatFromInt(hhea.descender);
|
const hhea_ascent: f64 = @floatFromInt(hhea.ascender);
|
||||||
const hhea_line_gap: f64 = @floatFromInt(hhea.lineGap);
|
const hhea_descent: f64 = @floatFromInt(hhea.descender);
|
||||||
|
const hhea_line_gap: f64 = @floatFromInt(hhea.lineGap);
|
||||||
|
|
||||||
if (os2_) |os2| {
|
if (os2_) |os2| {
|
||||||
const os2_ascent: f64 = @floatFromInt(os2.sTypoAscender);
|
const os2_ascent: f64 = @floatFromInt(os2.sTypoAscender);
|
||||||
const os2_descent: f64 = @floatFromInt(os2.sTypoDescender);
|
const os2_descent: f64 = @floatFromInt(os2.sTypoDescender);
|
||||||
const os2_line_gap: f64 = @floatFromInt(os2.sTypoLineGap);
|
const os2_line_gap: f64 = @floatFromInt(os2.sTypoLineGap);
|
||||||
|
|
||||||
// If the font says to use typo metrics, trust it.
|
// If the font says to use typo metrics, trust it.
|
||||||
if (os2.fsSelection.use_typo_metrics) break :vertical_metrics .{
|
if (os2.fsSelection.use_typo_metrics) break :vertical_metrics .{
|
||||||
os2_ascent * px_per_unit,
|
os2_ascent * px_per_unit,
|
||||||
os2_descent * px_per_unit,
|
os2_descent * px_per_unit,
|
||||||
os2_line_gap * px_per_unit,
|
os2_line_gap * px_per_unit,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Otherwise we prefer the height metrics from 'hhea' if they
|
// Otherwise we prefer the height metrics from 'hhea' if they
|
||||||
// are available, or else OS/2 sTypo* metrics, and if all else
|
// are available, or else OS/2 sTypo* metrics, and if all else
|
||||||
// fails then we use OS/2 usWin* metrics.
|
// fails then we use OS/2 usWin* metrics.
|
||||||
//
|
//
|
||||||
// This is not "standard" behavior, but it's our best bet to
|
// This is not "standard" behavior, but it's our best bet to
|
||||||
// account for fonts being... just weird. It's pretty much what
|
// account for fonts being... just weird. It's pretty much what
|
||||||
// FreeType does to get its generic ascent and descent metrics.
|
// FreeType does to get its generic ascent and descent metrics.
|
||||||
|
|
||||||
if (hhea.ascender != 0 or hhea.descender != 0) break :vertical_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
|
||||||
|
// blindly use the metrics from the hhea table.
|
||||||
|
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,
|
||||||
};
|
};
|
||||||
|
|
||||||
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 we couldn't get the hhea table, rely on metrics from CoreText.
|
||||||
// blindly use the metrics from the hhea table.
|
|
||||||
break :vertical_metrics .{
|
break :vertical_metrics .{
|
||||||
hhea_ascent * px_per_unit,
|
self.font.getAscent(),
|
||||||
hhea_descent * px_per_unit,
|
-self.font.getDescent(),
|
||||||
hhea_line_gap * px_per_unit,
|
self.font.getLeading(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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 +997,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 +1062,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 +1089,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 +1120,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()) },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -784,12 +784,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 +795,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 +808,130 @@ 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_ascent: f64 = @floatFromInt(hhea.Ascender);
|
if (hhea_) |hhea| {
|
||||||
const hhea_descent: f64 = @floatFromInt(hhea.Descender);
|
const hhea_ascent: f64 = @floatFromInt(hhea.Ascender);
|
||||||
const hhea_line_gap: f64 = @floatFromInt(hhea.Line_Gap);
|
const hhea_descent: f64 = @floatFromInt(hhea.Descender);
|
||||||
|
const hhea_line_gap: f64 = @floatFromInt(hhea.Line_Gap);
|
||||||
|
|
||||||
if (os2_) |os2| {
|
if (os2_) |os2| {
|
||||||
const os2_ascent: f64 = @floatFromInt(os2.sTypoAscender);
|
const os2_ascent: f64 = @floatFromInt(os2.sTypoAscender);
|
||||||
const os2_descent: f64 = @floatFromInt(os2.sTypoDescender);
|
const os2_descent: f64 = @floatFromInt(os2.sTypoDescender);
|
||||||
const os2_line_gap: f64 = @floatFromInt(os2.sTypoLineGap);
|
const os2_line_gap: f64 = @floatFromInt(os2.sTypoLineGap);
|
||||||
|
|
||||||
// If the font says to use typo metrics, trust it.
|
// If the font says to use typo metrics, trust it.
|
||||||
// (The USE_TYPO_METRICS bit is bit 7)
|
// (The USE_TYPO_METRICS bit is bit 7)
|
||||||
if (os2.fsSelection & (1 << 7) != 0) {
|
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) {
|
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// If our font has no OS/2 table, then we just
|
// If we couldn't get the hhea table, rely on metrics from FreeType.
|
||||||
// blindly use the metrics from the hhea table.
|
const ascender = f26dot6ToF64(size_metrics.ascender);
|
||||||
|
const descender = f26dot6ToF64(size_metrics.descender);
|
||||||
|
const height = f26dot6ToF64(size_metrics.height);
|
||||||
break :vertical_metrics .{
|
break :vertical_metrics .{
|
||||||
hhea_ascent * px_per_unit,
|
ascender,
|
||||||
hhea_descent * px_per_unit,
|
descender,
|
||||||
hhea_line_gap * px_per_unit,
|
// 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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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 +1119,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 +1129,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 +1138,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 +1165,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 +1225,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 +1289,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