mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 08:46:08 +03:00
Unified font metrics (#2948)
This is a big one, I kind of over scoped a touch. I don't have it in me to do a clean-up pass on this code before PR-ing it, so apologies for any weirdness; I'll happily resolve any feedback, but trying to review my own code right now is not something my brain wants to do. #### Summary - Font metric calculations reworked to be based primarily on data from font tables, and now the majority of the logic is shared between CoreText and FreeType, and we have more control over the specifics. - Sprite font metrics for the position and thickness of underlines, strikethroughs, and overlines are now separate, as well as box drawing thickness, allowing for individual adjustments. - Minimums are applied to font metrics to avoid *completely* broken results from fonts with degenerate values in them. ### Unified Metrics Calculations I added a solid foundation for parsing as many SFNT font tables as we need, and added parsing for `head`, `hhea` (unused), `post`, and `OS/2`. I didn't make a strong effort to account for variable fonts, so if a variable font changes vertical metrics significantly it could cause issues -- luckily, width, which is the most likely metric to change, is not a problem, since we get the width by having our backends measure the advances for us, which does account for variations correctly. ### Separated Sprite Metrics I reworked the sprite renderer to just get a copy of the metrics that it can use directly, instead of being given its own metrics (width, height, thickness) -- so that the different thickness metrics can be used for their intended purposes, and so that offsets for "unadjusted" characters can be handled in the Box renderer itself. ### Minimums Prevent degenerate fonts and bad `adjust` configs from creating, e.g. zero-thickness underlines by applying a minimum to certain fields after calculating metrics and after applying modifiers. ### Misc. Fixed `init` for CoreText faces selecting the first predefined instance of a variable font rather than the default.
This commit is contained in:
@ -215,6 +215,8 @@ pub const SfntTag = enum(c_int) {
|
|||||||
pub fn DataType(comptime self: SfntTag) type {
|
pub fn DataType(comptime self: SfntTag) type {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.os2 => c.TT_OS2,
|
.os2 => c.TT_OS2,
|
||||||
|
.head => c.TT_Header,
|
||||||
|
.post => c.TT_Postscript,
|
||||||
else => unreachable, // As-needed...
|
else => unreachable, // As-needed...
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ pub const FontVariationAxisKey = font_descriptor.FontVariationAxisKey;
|
|||||||
pub const FontSymbolicTraits = font_descriptor.FontSymbolicTraits;
|
pub const FontSymbolicTraits = font_descriptor.FontSymbolicTraits;
|
||||||
pub const createFontDescriptorsFromURL = font_manager.createFontDescriptorsFromURL;
|
pub const createFontDescriptorsFromURL = font_manager.createFontDescriptorsFromURL;
|
||||||
pub const createFontDescriptorsFromData = font_manager.createFontDescriptorsFromData;
|
pub const createFontDescriptorsFromData = font_manager.createFontDescriptorsFromData;
|
||||||
|
pub const createFontDescriptorFromData = font_manager.createFontDescriptorFromData;
|
||||||
pub const Frame = frame.Frame;
|
pub const Frame = frame.Frame;
|
||||||
pub const Framesetter = framesetter.Framesetter;
|
pub const Framesetter = framesetter.Framesetter;
|
||||||
pub const Line = line.Line;
|
pub const Line = line.Line;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const foundation = @import("../foundation.zig");
|
const foundation = @import("../foundation.zig");
|
||||||
|
const FontDescriptor = @import("./font_descriptor.zig").FontDescriptor;
|
||||||
const c = @import("c.zig").c;
|
const c = @import("c.zig").c;
|
||||||
|
|
||||||
pub fn createFontDescriptorsFromURL(url: *foundation.URL) ?*foundation.Array {
|
pub fn createFontDescriptorsFromURL(url: *foundation.URL) ?*foundation.Array {
|
||||||
@ -14,3 +15,9 @@ pub fn createFontDescriptorsFromData(data: *foundation.Data) ?*foundation.Array
|
|||||||
@ptrCast(data),
|
@ptrCast(data),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn createFontDescriptorFromData(data: *foundation.Data) ?*FontDescriptor {
|
||||||
|
return @ptrFromInt(@intFromPtr(c.CTFontManagerCreateFontDescriptorFromData(
|
||||||
|
@ptrCast(data),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
@ -520,7 +520,14 @@ test "getIndex box glyph" {
|
|||||||
|
|
||||||
var r: CodepointResolver = .{
|
var r: CodepointResolver = .{
|
||||||
.collection = c,
|
.collection = c,
|
||||||
.sprite = .{ .width = 18, .height = 36, .thickness = 2 },
|
.sprite = .{
|
||||||
|
.metrics = font.Metrics.calc(.{
|
||||||
|
.cell_width = 18.0,
|
||||||
|
.ascent = 30.0,
|
||||||
|
.descent = -6.0,
|
||||||
|
.line_gap = 0.0,
|
||||||
|
}),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
defer r.deinit(alloc);
|
defer r.deinit(alloc);
|
||||||
|
|
||||||
|
@ -122,13 +122,7 @@ fn reloadMetrics(self: *SharedGrid) !void {
|
|||||||
self.metrics = face.metrics;
|
self.metrics = face.metrics;
|
||||||
|
|
||||||
// Setup our sprite font.
|
// Setup our sprite font.
|
||||||
self.resolver.sprite = .{
|
self.resolver.sprite = .{ .metrics = self.metrics };
|
||||||
.width = self.metrics.cell_width,
|
|
||||||
.height = self.metrics.cell_height,
|
|
||||||
.thickness = self.metrics.underline_thickness,
|
|
||||||
.underline_position = self.metrics.underline_position,
|
|
||||||
.strikethrough_position = self.metrics.strikethrough_position,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the grid cell size.
|
/// Returns the grid cell size.
|
||||||
|
@ -6,21 +6,28 @@ const std = @import("std");
|
|||||||
cell_width: u32,
|
cell_width: u32,
|
||||||
cell_height: u32,
|
cell_height: u32,
|
||||||
|
|
||||||
/// For monospace grids, the recommended y-value from the bottom to set
|
/// Distance in pixels from the bottom of the cell to the text baseline.
|
||||||
/// the baseline for font rendering. This is chosen so that things such
|
|
||||||
/// as the bottom of a "g" or "y" do not drop below the cell.
|
|
||||||
cell_baseline: u32,
|
cell_baseline: u32,
|
||||||
|
|
||||||
/// The position of the underline from the top of the cell and the
|
/// Distance in pixels from the top of the cell to the top of the underline.
|
||||||
/// thickness in pixels.
|
|
||||||
underline_position: u32,
|
underline_position: u32,
|
||||||
|
/// Thickness in pixels of the underline.
|
||||||
underline_thickness: u32,
|
underline_thickness: u32,
|
||||||
|
|
||||||
/// The position and thickness of a strikethrough. Same units/style
|
/// Distance in pixels from the top of the cell to the top of the strikethrough.
|
||||||
/// as the underline fields.
|
|
||||||
strikethrough_position: u32,
|
strikethrough_position: u32,
|
||||||
|
/// Thickness in pixels of the strikethrough.
|
||||||
strikethrough_thickness: u32,
|
strikethrough_thickness: u32,
|
||||||
|
|
||||||
|
/// Distance in pixels from the top of the cell to the top of the overline.
|
||||||
|
/// Can be negative to adjust the position above the top of the cell.
|
||||||
|
overline_position: i32,
|
||||||
|
/// Thickness in pixels of the overline.
|
||||||
|
overline_thickness: u32,
|
||||||
|
|
||||||
|
/// Thickness in pixels of box drawing characters.
|
||||||
|
box_thickness: u32,
|
||||||
|
|
||||||
/// The thickness in pixels of the cursor sprite. This has a default value
|
/// The thickness in pixels of the cursor sprite. This has a default value
|
||||||
/// because it is not determined by fonts but rather by user configuration.
|
/// because it is not determined by fonts but rather by user configuration.
|
||||||
cursor_thickness: u32 = 1,
|
cursor_thickness: u32 = 1,
|
||||||
@ -30,6 +37,146 @@ cursor_thickness: u32 = 1,
|
|||||||
original_cell_width: ?u32 = null,
|
original_cell_width: ?u32 = null,
|
||||||
original_cell_height: ?u32 = null,
|
original_cell_height: ?u32 = null,
|
||||||
|
|
||||||
|
/// Minimum acceptable values for some fields to prevent modifiers
|
||||||
|
/// from being able to, for example, cause 0-thickness underlines.
|
||||||
|
const Minimums = struct {
|
||||||
|
const cell_width = 1;
|
||||||
|
const cell_height = 1;
|
||||||
|
const underline_thickness = 1;
|
||||||
|
const strikethrough_thickness = 1;
|
||||||
|
const overline_thickness = 1;
|
||||||
|
const box_thickness = 1;
|
||||||
|
const cursor_thickness = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CalcOpts = struct {
|
||||||
|
cell_width: f64,
|
||||||
|
|
||||||
|
/// The typographic ascent metric from the font.
|
||||||
|
/// This represents the maximum vertical position of the highest ascender.
|
||||||
|
///
|
||||||
|
/// Relative to the baseline, in px, +Y=up
|
||||||
|
ascent: f64,
|
||||||
|
|
||||||
|
/// The typographic descent metric from the font.
|
||||||
|
/// This represents the minimum vertical position of the lowest descender.
|
||||||
|
///
|
||||||
|
/// Relative to the baseline, in px, +Y=up
|
||||||
|
///
|
||||||
|
/// Note:
|
||||||
|
/// As this value is generally below the baseline, it is typically negative.
|
||||||
|
descent: f64,
|
||||||
|
|
||||||
|
/// The typographic line gap (aka "leading") metric from the font.
|
||||||
|
/// This represents the additional space to be added between lines in
|
||||||
|
/// addition to the space defined by the ascent and descent metrics.
|
||||||
|
///
|
||||||
|
/// Positive value in px
|
||||||
|
line_gap: f64,
|
||||||
|
|
||||||
|
/// The TOP of the underline stroke.
|
||||||
|
///
|
||||||
|
/// Relative to the baseline, in px, +Y=up
|
||||||
|
underline_position: ?f64 = null,
|
||||||
|
|
||||||
|
/// The thickness of the underline stroke in px.
|
||||||
|
underline_thickness: ?f64 = null,
|
||||||
|
|
||||||
|
/// The TOP of the strikethrough stroke.
|
||||||
|
///
|
||||||
|
/// Relative to the baseline, in px, +Y=up
|
||||||
|
strikethrough_position: ?f64 = null,
|
||||||
|
|
||||||
|
/// The thickness of the strikethrough stroke in px.
|
||||||
|
strikethrough_thickness: ?f64 = null,
|
||||||
|
|
||||||
|
/// The height of capital letters in the font, either derived from
|
||||||
|
/// a provided cap height metric or measured from the height of the
|
||||||
|
/// capital H glyph.
|
||||||
|
cap_height: ?f64 = null,
|
||||||
|
|
||||||
|
/// The height of lowercase letters in the font, either derived from
|
||||||
|
/// a provided ex height metric or measured from the height of the
|
||||||
|
/// lowercase x glyph.
|
||||||
|
ex_height: ?f64 = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Calculate our metrics based on values extracted from a font.
|
||||||
|
///
|
||||||
|
/// Try to pass values with as much precision as possible,
|
||||||
|
/// do not round them before using them for this function.
|
||||||
|
///
|
||||||
|
/// For any nullable options that are not provided, estimates will be used.
|
||||||
|
pub fn calc(opts: CalcOpts) Metrics {
|
||||||
|
// We use the ceiling of the provided cell width and height to ensure
|
||||||
|
// that the cell is large enough for the provided size, since we cast
|
||||||
|
// it to an integer later.
|
||||||
|
const cell_width = @ceil(opts.cell_width);
|
||||||
|
const cell_height = @ceil(opts.ascent - opts.descent + opts.line_gap);
|
||||||
|
|
||||||
|
// We split our line gap in two parts, and put half of it on the top
|
||||||
|
// of the cell and the other half on the bottom, so that our text never
|
||||||
|
// bumps up against either edge of the cell vertically.
|
||||||
|
const half_line_gap = opts.line_gap / 2;
|
||||||
|
|
||||||
|
// Unlike all our other metrics, `cell_baseline` is relative to the
|
||||||
|
// BOTTOM of the cell.
|
||||||
|
const cell_baseline = @round(half_line_gap - opts.descent);
|
||||||
|
|
||||||
|
// We calculate a top_to_baseline to make following calculations simpler.
|
||||||
|
const top_to_baseline = cell_height - cell_baseline;
|
||||||
|
|
||||||
|
// If we don't have a provided cap height,
|
||||||
|
// we estimate it as 75% of the ascent.
|
||||||
|
const cap_height = opts.cap_height orelse opts.ascent * 0.75;
|
||||||
|
|
||||||
|
// If we don't have a provided ex height,
|
||||||
|
// we estimate it as 75% of the cap height.
|
||||||
|
const ex_height = opts.ex_height orelse cap_height * 0.75;
|
||||||
|
|
||||||
|
// If we don't have a provided underline thickness,
|
||||||
|
// we estimate it as 15% of the ex height.
|
||||||
|
const underline_thickness = @max(1, @ceil(opts.underline_thickness orelse 0.15 * ex_height));
|
||||||
|
|
||||||
|
// If we don't have a provided strikethrough thickness
|
||||||
|
// then we just use the underline thickness for it.
|
||||||
|
const strikethrough_thickness = @max(1, @ceil(opts.strikethrough_thickness orelse underline_thickness));
|
||||||
|
|
||||||
|
// If we don't have a provided underline position then
|
||||||
|
// we place it 1 underline-thickness below the baseline.
|
||||||
|
const underline_position = @round(top_to_baseline -
|
||||||
|
(opts.underline_position orelse
|
||||||
|
-underline_thickness));
|
||||||
|
|
||||||
|
// If we don't have a provided strikethrough position
|
||||||
|
// then we center the strikethrough stroke at half the
|
||||||
|
// ex height, so that it's perfectly centered on lower
|
||||||
|
// case text.
|
||||||
|
const strikethrough_position = @round(top_to_baseline -
|
||||||
|
(opts.strikethrough_position orelse
|
||||||
|
ex_height * 0.5 + strikethrough_thickness * 0.5));
|
||||||
|
|
||||||
|
var result: Metrics = .{
|
||||||
|
.cell_width = @intFromFloat(cell_width),
|
||||||
|
.cell_height = @intFromFloat(cell_height),
|
||||||
|
.cell_baseline = @intFromFloat(cell_baseline),
|
||||||
|
.underline_position = @intFromFloat(underline_position),
|
||||||
|
.underline_thickness = @intFromFloat(underline_thickness),
|
||||||
|
.strikethrough_position = @intFromFloat(strikethrough_position),
|
||||||
|
.strikethrough_thickness = @intFromFloat(strikethrough_thickness),
|
||||||
|
.overline_position = 0,
|
||||||
|
.overline_thickness = @intFromFloat(underline_thickness),
|
||||||
|
.box_thickness = @intFromFloat(underline_thickness),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ensure all metrics are within their allowable range.
|
||||||
|
result.clamp();
|
||||||
|
|
||||||
|
// std.log.debug("metrics={}", .{result});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/// Apply a set of modifiers.
|
/// Apply a set of modifiers.
|
||||||
pub fn apply(self: *Metrics, mods: ModifierSet) void {
|
pub fn apply(self: *Metrics, mods: ModifierSet) void {
|
||||||
var it = mods.iterator();
|
var it = mods.iterator();
|
||||||
@ -80,6 +227,21 @@ pub fn apply(self: *Metrics, mods: ModifierSet) void {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent modifiers from pushing metrics out of their allowable range.
|
||||||
|
self.clamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clamp all metrics to their allowable range.
|
||||||
|
fn clamp(self: *Metrics) void {
|
||||||
|
inline for (std.meta.fields(Metrics)) |field| {
|
||||||
|
if (@hasDecl(Minimums, field.name)) {
|
||||||
|
@field(self, field.name) = @max(
|
||||||
|
@field(self, field.name),
|
||||||
|
@field(Minimums, field.name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A set of modifiers to apply to metrics. We use a hash map because
|
/// A set of modifiers to apply to metrics. We use a hash map because
|
||||||
@ -152,23 +314,26 @@ pub const Modifier = union(enum) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Apply a modifier to a numeric value.
|
/// Apply a modifier to a numeric value.
|
||||||
pub fn apply(self: Modifier, v: u32) u32 {
|
pub fn apply(self: Modifier, v: anytype) @TypeOf(v) {
|
||||||
|
const T = @TypeOf(v);
|
||||||
|
const signed = @typeInfo(T).Int.signedness == .signed;
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.percent => |p| percent: {
|
.percent => |p| percent: {
|
||||||
const p_clamped: f64 = @max(0, p);
|
const p_clamped: f64 = @max(0, p);
|
||||||
const v_f64: f64 = @floatFromInt(v);
|
const v_f64: f64 = @floatFromInt(v);
|
||||||
const applied_f64: f64 = @round(v_f64 * p_clamped);
|
const applied_f64: f64 = @round(v_f64 * p_clamped);
|
||||||
const applied_u32: u32 = @intFromFloat(applied_f64);
|
const applied_T: T = @intFromFloat(applied_f64);
|
||||||
break :percent applied_u32;
|
break :percent applied_T;
|
||||||
},
|
},
|
||||||
|
|
||||||
.absolute => |abs| absolute: {
|
.absolute => |abs| absolute: {
|
||||||
const v_i64: i64 = @intCast(v);
|
const v_i64: i64 = @intCast(v);
|
||||||
const abs_i64: i64 = @intCast(abs);
|
const abs_i64: i64 = @intCast(abs);
|
||||||
const applied_i64: i64 = @max(0, v_i64 +| abs_i64);
|
const applied_i64: i64 = v_i64 +| abs_i64;
|
||||||
const applied_u32: u32 = std.math.cast(u32, applied_i64) orelse
|
const clamped_i64: i64 = if (signed) applied_i64 else @max(0, applied_i64);
|
||||||
std.math.maxInt(u32);
|
const applied_T: T = std.math.cast(T, clamped_i64) orelse
|
||||||
break :absolute applied_u32;
|
std.math.maxInt(T) * @as(T, @intCast(std.math.sign(clamped_i64)));
|
||||||
|
break :absolute applied_T;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -215,7 +380,7 @@ pub const Key = key: {
|
|||||||
var enumFields: [field_infos.len]std.builtin.Type.EnumField = undefined;
|
var enumFields: [field_infos.len]std.builtin.Type.EnumField = undefined;
|
||||||
var count: usize = 0;
|
var count: usize = 0;
|
||||||
for (field_infos, 0..) |field, i| {
|
for (field_infos, 0..) |field, i| {
|
||||||
if (field.type != u32) continue;
|
if (field.type != u32 and field.type != i32) continue;
|
||||||
enumFields[i] = .{ .name = field.name, .value = i };
|
enumFields[i] = .{ .name = field.name, .value = i };
|
||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
@ -242,6 +407,9 @@ fn init() Metrics {
|
|||||||
.underline_thickness = 0,
|
.underline_thickness = 0,
|
||||||
.strikethrough_position = 0,
|
.strikethrough_position = 0,
|
||||||
.strikethrough_thickness = 0,
|
.strikethrough_thickness = 0,
|
||||||
|
.overline_position = 0,
|
||||||
|
.overline_thickness = 0,
|
||||||
|
.box_thickness = 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -337,12 +505,12 @@ test "Modifier: percent" {
|
|||||||
|
|
||||||
{
|
{
|
||||||
const m: Modifier = .{ .percent = 0.8 };
|
const m: Modifier = .{ .percent = 0.8 };
|
||||||
const v: u32 = m.apply(100);
|
const v: u32 = m.apply(@as(u32, 100));
|
||||||
try testing.expectEqual(@as(u32, 80), v);
|
try testing.expectEqual(@as(u32, 80), v);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const m: Modifier = .{ .percent = 1.8 };
|
const m: Modifier = .{ .percent = 1.8 };
|
||||||
const v: u32 = m.apply(100);
|
const v: u32 = m.apply(@as(u32, 100));
|
||||||
try testing.expectEqual(@as(u32, 180), v);
|
try testing.expectEqual(@as(u32, 180), v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -352,17 +520,17 @@ test "Modifier: absolute" {
|
|||||||
|
|
||||||
{
|
{
|
||||||
const m: Modifier = .{ .absolute = -100 };
|
const m: Modifier = .{ .absolute = -100 };
|
||||||
const v: u32 = m.apply(100);
|
const v: u32 = m.apply(@as(u32, 100));
|
||||||
try testing.expectEqual(@as(u32, 0), v);
|
try testing.expectEqual(@as(u32, 0), v);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const m: Modifier = .{ .absolute = -120 };
|
const m: Modifier = .{ .absolute = -120 };
|
||||||
const v: u32 = m.apply(100);
|
const v: u32 = m.apply(@as(u32, 100));
|
||||||
try testing.expectEqual(@as(u32, 0), v);
|
try testing.expectEqual(@as(u32, 0), v);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const m: Modifier = .{ .absolute = 100 };
|
const m: Modifier = .{ .absolute = 100 };
|
||||||
const v: u32 = m.apply(100);
|
const v: u32 = m.apply(@as(u32, 100));
|
||||||
try testing.expectEqual(@as(u32, 200), v);
|
try testing.expectEqual(@as(u32, 200), v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,12 +55,10 @@ pub const Face = struct {
|
|||||||
const data = try macos.foundation.Data.createWithBytesNoCopy(source);
|
const data = try macos.foundation.Data.createWithBytesNoCopy(source);
|
||||||
defer data.release();
|
defer data.release();
|
||||||
|
|
||||||
const arr = macos.text.createFontDescriptorsFromData(data) orelse
|
const desc = macos.text.createFontDescriptorFromData(data) orelse
|
||||||
return error.FontInitFailure;
|
return error.FontInitFailure;
|
||||||
defer arr.release();
|
defer desc.release();
|
||||||
if (arr.getCount() == 0) return error.FontInitFailure;
|
|
||||||
|
|
||||||
const desc = arr.getValueAtIndex(macos.text.FontDescriptor, 0);
|
|
||||||
const ct_font = try macos.text.Font.createWithFontDescriptor(desc, 12);
|
const ct_font = try macos.text.Font.createWithFontDescriptor(desc, 12);
|
||||||
defer ct_font.release();
|
defer ct_font.release();
|
||||||
|
|
||||||
@ -532,11 +530,114 @@ pub const Face = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calcMetrics(ct_font: *macos.text.Font) !font.face.Metrics {
|
const CalcMetricsError = error{
|
||||||
|
CopyTableError,
|
||||||
|
InvalidHeadTable,
|
||||||
|
InvalidPostTable,
|
||||||
|
InvalidOS2Table,
|
||||||
|
OS2VersionNotSupported,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn calcMetrics(ct_font: *macos.text.Font) CalcMetricsError!font.face.Metrics {
|
||||||
|
// Read the 'head' table out of the font data.
|
||||||
|
const head: opentype.Head = head: {
|
||||||
|
const tag = macos.text.FontTableTag.init("head");
|
||||||
|
const data = ct_font.copyTable(tag) orelse return error.CopyTableError;
|
||||||
|
defer data.release();
|
||||||
|
const ptr = data.getPointer();
|
||||||
|
const len = data.getLength();
|
||||||
|
break :head opentype.Head.init(ptr[0..len]) catch |err| {
|
||||||
|
return switch (err) {
|
||||||
|
error.EndOfStream,
|
||||||
|
=> error.InvalidHeadTable,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read the 'post' table out of the font data.
|
||||||
|
const post: opentype.Post = post: {
|
||||||
|
const tag = macos.text.FontTableTag.init("post");
|
||||||
|
const data = ct_font.copyTable(tag) orelse return error.CopyTableError;
|
||||||
|
defer data.release();
|
||||||
|
const ptr = data.getPointer();
|
||||||
|
const len = data.getLength();
|
||||||
|
break :post opentype.Post.init(ptr[0..len]) catch |err| {
|
||||||
|
return switch (err) {
|
||||||
|
error.EndOfStream => error.InvalidOS2Table,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read the 'OS/2' table out of the font data.
|
||||||
|
const os2: opentype.OS2 = os2: {
|
||||||
|
const tag = macos.text.FontTableTag.init("OS/2");
|
||||||
|
const data = ct_font.copyTable(tag) orelse return error.CopyTableError;
|
||||||
|
defer data.release();
|
||||||
|
const ptr = data.getPointer();
|
||||||
|
const len = data.getLength();
|
||||||
|
break :os2 opentype.OS2.init(ptr[0..len]) catch |err| {
|
||||||
|
return switch (err) {
|
||||||
|
error.EndOfStream => error.InvalidOS2Table,
|
||||||
|
error.OS2VersionNotSupported => error.OS2VersionNotSupported,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const units_per_em: f64 = @floatFromInt(head.unitsPerEm);
|
||||||
|
const px_per_em: f64 = ct_font.getSize();
|
||||||
|
const px_per_unit: f64 = px_per_em / units_per_em;
|
||||||
|
|
||||||
|
const ascent = @as(f64, @floatFromInt(os2.sTypoAscender)) * px_per_unit;
|
||||||
|
const descent = @as(f64, @floatFromInt(os2.sTypoDescender)) * px_per_unit;
|
||||||
|
const line_gap = @as(f64, @floatFromInt(os2.sTypoLineGap)) * px_per_unit;
|
||||||
|
|
||||||
|
// Some fonts have degenerate 'post' tables where the underline
|
||||||
|
// thickness (and often position) are 0. We consider them 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,
|
||||||
|
// even if the thickness is't properly specified.
|
||||||
|
const underline_position: ?f64 = if (has_broken_underline and post.underlinePosition == 0)
|
||||||
|
null
|
||||||
|
else
|
||||||
|
@as(f64, @floatFromInt(post.underlinePosition)) * px_per_unit;
|
||||||
|
|
||||||
|
const underline_thickness = if (has_broken_underline)
|
||||||
|
null
|
||||||
|
else
|
||||||
|
@as(f64, @floatFromInt(post.underlineThickness)) * px_per_unit;
|
||||||
|
|
||||||
|
// Similar logic to the underline above.
|
||||||
|
const has_broken_strikethrough = os2.yStrikeoutSize == 0;
|
||||||
|
|
||||||
|
const strikethrough_position: ?f64 = if (has_broken_strikethrough and os2.yStrikeoutPosition == 0)
|
||||||
|
null
|
||||||
|
else
|
||||||
|
@as(f64, @floatFromInt(os2.yStrikeoutPosition)) * px_per_unit;
|
||||||
|
|
||||||
|
const strikethrough_thickness: ?f64 = if (has_broken_strikethrough)
|
||||||
|
null
|
||||||
|
else
|
||||||
|
@as(f64, @floatFromInt(os2.yStrikeoutSize)) * px_per_unit;
|
||||||
|
|
||||||
|
// We fall back to whatever CoreText does if
|
||||||
|
// the OS/2 table doesn't specify a cap height.
|
||||||
|
const cap_height: f64 = if (os2.sCapHeight) |sCapHeight|
|
||||||
|
@as(f64, @floatFromInt(sCapHeight)) * px_per_unit
|
||||||
|
else
|
||||||
|
ct_font.getCapHeight();
|
||||||
|
|
||||||
|
// Ditto for ex height.
|
||||||
|
const ex_height: f64 = if (os2.sxHeight) |sxHeight|
|
||||||
|
@as(f64, @floatFromInt(sxHeight)) * px_per_unit
|
||||||
|
else
|
||||||
|
ct_font.getXHeight();
|
||||||
|
|
||||||
// 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
|
||||||
// whatever is widest.
|
// whatever is widest.
|
||||||
const cell_width: f32 = cell_width: {
|
const cell_width: f64 = cell_width: {
|
||||||
// Build a comptime array of all the ASCII chars
|
// Build a comptime array of all the ASCII chars
|
||||||
const unichars = comptime unichars: {
|
const unichars = comptime unichars: {
|
||||||
const len = 127 - 32;
|
const len = 127 - 32;
|
||||||
@ -564,93 +665,29 @@ pub const Face = struct {
|
|||||||
max = @max(advances[i].width, max);
|
max = @max(advances[i].width, max);
|
||||||
}
|
}
|
||||||
|
|
||||||
break :cell_width @floatCast(@ceil(max));
|
break :cell_width max;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate the layout metrics for height/ascent by just asking
|
return font.face.Metrics.calc(.{
|
||||||
// the font. I also tried Kitty's approach at one point which is to
|
.cell_width = cell_width,
|
||||||
// use the CoreText layout engine but this led to some glyphs being
|
.ascent = ascent,
|
||||||
// set incorrectly.
|
.descent = descent,
|
||||||
const layout_metrics: struct {
|
.line_gap = line_gap,
|
||||||
height: f32,
|
.underline_position = underline_position,
|
||||||
ascent: f32,
|
.underline_thickness = underline_thickness,
|
||||||
leading: f32,
|
.strikethrough_position = strikethrough_position,
|
||||||
} = metrics: {
|
.strikethrough_thickness = strikethrough_thickness,
|
||||||
const ascent = ct_font.getAscent();
|
.cap_height = cap_height,
|
||||||
const descent = ct_font.getDescent();
|
.ex_height = ex_height,
|
||||||
|
});
|
||||||
// Leading is the value between lines at the TOP of a line.
|
|
||||||
// Because we are rendering a fixed size terminal grid, we
|
|
||||||
// want the leading to be split equally between the top and bottom.
|
|
||||||
const leading = ct_font.getLeading();
|
|
||||||
|
|
||||||
// We ceil the metrics below because we don't want to cut off any
|
|
||||||
// potential used pixels. This tends to only make a one pixel
|
|
||||||
// difference but at small font sizes this can be noticeable.
|
|
||||||
break :metrics .{
|
|
||||||
.height = @floatCast(@ceil(ascent + descent + leading)),
|
|
||||||
.ascent = @floatCast(@ceil(ascent + (leading / 2))),
|
|
||||||
.leading = @floatCast(leading),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// All of these metrics are based on our layout above.
|
|
||||||
const cell_height = @ceil(layout_metrics.height);
|
|
||||||
const cell_baseline = @ceil(layout_metrics.height - layout_metrics.ascent);
|
|
||||||
|
|
||||||
const underline_thickness = @ceil(@as(f32, @floatCast(ct_font.getUnderlineThickness())));
|
|
||||||
const strikethrough_thickness = underline_thickness;
|
|
||||||
|
|
||||||
const strikethrough_position = strikethrough_position: {
|
|
||||||
// This is the height of lower case letters in our font.
|
|
||||||
const ex_height = ct_font.getXHeight();
|
|
||||||
|
|
||||||
// We want to position the strikethrough so that it's
|
|
||||||
// vertically centered on any lower case text. This is
|
|
||||||
// a fairly standard choice for strikethrough positioning.
|
|
||||||
//
|
|
||||||
// Because our `strikethrough_position` is relative to the
|
|
||||||
// top of the cell we start with the ascent metric, which
|
|
||||||
// is the distance from the top down to the baseline, then
|
|
||||||
// we subtract half of the ex height to go back up to the
|
|
||||||
// correct height that should evenly split lowercase text.
|
|
||||||
const pos = layout_metrics.ascent -
|
|
||||||
ex_height * 0.5 -
|
|
||||||
strikethrough_thickness * 0.5;
|
|
||||||
|
|
||||||
break :strikethrough_position @ceil(pos);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Underline position reported is usually something like "-1" to
|
|
||||||
// represent the amount under the baseline. We add this to our real
|
|
||||||
// baseline to get the actual value from the bottom (+y is up).
|
|
||||||
// The final underline position is +y from the TOP (confusing)
|
|
||||||
// so we have to subtract from the cell height.
|
|
||||||
const underline_position = @ceil(layout_metrics.ascent -
|
|
||||||
@as(f32, @floatCast(ct_font.getUnderlinePosition())));
|
|
||||||
|
|
||||||
// Note: is this useful?
|
|
||||||
// const units_per_em = ct_font.getUnitsPerEm();
|
|
||||||
// const units_per_point = @intToFloat(f64, units_per_em) / ct_font.getSize();
|
|
||||||
|
|
||||||
const result = font.face.Metrics{
|
|
||||||
.cell_width = @intFromFloat(cell_width),
|
|
||||||
.cell_height = @intFromFloat(cell_height),
|
|
||||||
.cell_baseline = @intFromFloat(cell_baseline),
|
|
||||||
.underline_position = @intFromFloat(underline_position),
|
|
||||||
.underline_thickness = @intFromFloat(underline_thickness),
|
|
||||||
.strikethrough_position = @intFromFloat(strikethrough_position),
|
|
||||||
.strikethrough_thickness = @intFromFloat(strikethrough_thickness),
|
|
||||||
};
|
|
||||||
|
|
||||||
// std.log.warn("font size size={d}", .{ct_font.getSize()});
|
|
||||||
// std.log.warn("font metrics={}", .{result});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Copy the font table data for the given tag.
|
/// Copy the font table data for the given tag.
|
||||||
pub fn copyTable(self: Face, alloc: Allocator, tag: *const [4]u8) !?[]u8 {
|
pub fn copyTable(
|
||||||
|
self: Face,
|
||||||
|
alloc: Allocator,
|
||||||
|
tag: *const [4]u8,
|
||||||
|
) Allocator.Error!?[]u8 {
|
||||||
const data = self.font.copyTable(macos.text.FontTableTag.init(tag)) orelse
|
const data = self.font.copyTable(macos.text.FontTableTag.init(tag)) orelse
|
||||||
return null;
|
return null;
|
||||||
defer data.release();
|
defer data.release();
|
||||||
@ -678,7 +715,9 @@ const ColorState = struct {
|
|||||||
svg: ?opentype.SVG,
|
svg: ?opentype.SVG,
|
||||||
svg_data: ?*macos.foundation.Data,
|
svg_data: ?*macos.foundation.Data,
|
||||||
|
|
||||||
pub fn init(f: *macos.text.Font) !ColorState {
|
pub const Error = error{InvalidSVGTable};
|
||||||
|
|
||||||
|
pub fn init(f: *macos.text.Font) Error!ColorState {
|
||||||
// sbix is true if the table exists in the font data at all.
|
// sbix is true if the table exists in the font data at all.
|
||||||
// In the future we probably want to actually parse it and
|
// In the future we probably want to actually parse it and
|
||||||
// check for glyphs.
|
// check for glyphs.
|
||||||
@ -699,8 +738,16 @@ const ColorState = struct {
|
|||||||
errdefer data.release();
|
errdefer data.release();
|
||||||
const ptr = data.getPointer();
|
const ptr = data.getPointer();
|
||||||
const len = data.getLength();
|
const len = data.getLength();
|
||||||
|
const svg = opentype.SVG.init(ptr[0..len]) catch |err| {
|
||||||
|
return switch (err) {
|
||||||
|
error.EndOfStream,
|
||||||
|
error.SVGVersionNotSupported,
|
||||||
|
=> error.InvalidSVGTable,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
break :svg .{
|
break :svg .{
|
||||||
.svg = try opentype.SVG.init(ptr[0..len]),
|
.svg = svg,
|
||||||
.data = data,
|
.data = data,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -907,3 +954,58 @@ test "glyphIndex colored vs text" {
|
|||||||
try testing.expect(face.isColorGlyph(glyph));
|
try testing.expect(face.isColorGlyph(glyph));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "coretext: metrics" {
|
||||||
|
const testFont = font.embedded.inconsolata;
|
||||||
|
const alloc = std.testing.allocator;
|
||||||
|
|
||||||
|
var atlas = try font.Atlas.init(alloc, 512, .grayscale);
|
||||||
|
defer atlas.deinit(alloc);
|
||||||
|
|
||||||
|
var ct_font = try Face.init(
|
||||||
|
undefined,
|
||||||
|
testFont,
|
||||||
|
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
|
||||||
|
);
|
||||||
|
defer ct_font.deinit();
|
||||||
|
|
||||||
|
try std.testing.expectEqual(font.face.Metrics{
|
||||||
|
.cell_width = 8,
|
||||||
|
// The cell height is 17 px because the calculation is
|
||||||
|
//
|
||||||
|
// ascender - descender + gap
|
||||||
|
//
|
||||||
|
// which, for inconsolata is
|
||||||
|
//
|
||||||
|
// 859 - -190 + 0
|
||||||
|
//
|
||||||
|
// font units, at 1000 units per em that works out to 1.049 em,
|
||||||
|
// and 1em should be the point size * dpi scale, so 12 * (96/72)
|
||||||
|
// which is 16, and 16 * 1.049 = 16.784, which finally is rounded
|
||||||
|
// to 17.
|
||||||
|
.cell_height = 17,
|
||||||
|
.cell_baseline = 3,
|
||||||
|
.underline_position = 17,
|
||||||
|
.underline_thickness = 1,
|
||||||
|
.strikethrough_position = 10,
|
||||||
|
.strikethrough_thickness = 1,
|
||||||
|
.overline_position = 0,
|
||||||
|
.overline_thickness = 1,
|
||||||
|
.box_thickness = 1,
|
||||||
|
}, ct_font.metrics);
|
||||||
|
|
||||||
|
// Resize should change metrics
|
||||||
|
try ct_font.setSize(.{ .size = .{ .points = 24, .xdpi = 96, .ydpi = 96 } });
|
||||||
|
try std.testing.expectEqual(font.face.Metrics{
|
||||||
|
.cell_width = 16,
|
||||||
|
.cell_height = 34,
|
||||||
|
.cell_baseline = 6,
|
||||||
|
.underline_position = 34,
|
||||||
|
.underline_thickness = 2,
|
||||||
|
.strikethrough_position = 19,
|
||||||
|
.strikethrough_thickness = 2,
|
||||||
|
.overline_position = 0,
|
||||||
|
.overline_thickness = 2,
|
||||||
|
.box_thickness = 2,
|
||||||
|
}, ct_font.metrics);
|
||||||
|
}
|
||||||
|
@ -16,6 +16,7 @@ const font = @import("../main.zig");
|
|||||||
const Glyph = font.Glyph;
|
const Glyph = font.Glyph;
|
||||||
const Library = font.Library;
|
const Library = font.Library;
|
||||||
const convert = @import("freetype_convert.zig");
|
const convert = @import("freetype_convert.zig");
|
||||||
|
const opentype = @import("../opentype.zig");
|
||||||
const fastmem = @import("../../fastmem.zig");
|
const fastmem = @import("../../fastmem.zig");
|
||||||
const quirks = @import("../../quirks.zig");
|
const quirks = @import("../../quirks.zig");
|
||||||
const config = @import("../../config.zig");
|
const config = @import("../../config.zig");
|
||||||
@ -85,7 +86,7 @@ pub const Face = struct {
|
|||||||
.lib = lib.lib,
|
.lib = lib.lib,
|
||||||
.face = face,
|
.face = face,
|
||||||
.hb_font = hb_font,
|
.hb_font = hb_font,
|
||||||
.metrics = calcMetrics(face, opts.metric_modifiers),
|
.metrics = try calcMetrics(face, opts.metric_modifiers),
|
||||||
.load_flags = opts.freetype_load_flags,
|
.load_flags = opts.freetype_load_flags,
|
||||||
};
|
};
|
||||||
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
|
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
|
||||||
@ -185,7 +186,7 @@ pub const Face = struct {
|
|||||||
/// for clearing any glyph caches, font atlas data, etc.
|
/// for clearing any glyph caches, font atlas data, etc.
|
||||||
pub fn setSize(self: *Face, opts: font.face.Options) !void {
|
pub fn setSize(self: *Face, opts: font.face.Options) !void {
|
||||||
try setSize_(self.face, opts.size);
|
try setSize_(self.face, opts.size);
|
||||||
self.metrics = calcMetrics(self.face, opts.metric_modifiers);
|
self.metrics = try calcMetrics(self.face, opts.metric_modifiers);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setSize_(face: freetype.Face, size: font.face.DesiredSize) !void {
|
fn setSize_(face: freetype.Face, size: font.face.DesiredSize) !void {
|
||||||
@ -258,7 +259,7 @@ pub const Face = struct {
|
|||||||
try self.face.setVarDesignCoordinates(coords);
|
try self.face.setVarDesignCoordinates(coords);
|
||||||
|
|
||||||
// We need to recalculate font metrics which may have changed.
|
// We need to recalculate font metrics which may have changed.
|
||||||
self.metrics = calcMetrics(self.face, opts.metric_modifiers);
|
self.metrics = try calcMetrics(self.face, opts.metric_modifiers);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the glyph index for the given Unicode code point. If this
|
/// Returns the glyph index for the given Unicode code point. If this
|
||||||
@ -593,6 +594,15 @@ pub const Face = struct {
|
|||||||
return @floatFromInt(v >> 6);
|
return @floatFromInt(v >> 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn f26dot6ToF64(v: freetype.c.FT_F26Dot6) f64 {
|
||||||
|
return @as(opentype.sfnt.F26Dot6, @bitCast(@as(u32, @intCast(v)))).to(f64);
|
||||||
|
}
|
||||||
|
|
||||||
|
const CalcMetricsError = error{
|
||||||
|
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
|
||||||
/// the metrics are calculated for every face and cached since they're
|
/// the metrics are calculated for every face and cached since they're
|
||||||
/// frequently required for renderers and take up next to little memory space
|
/// frequently required for renderers and take up next to little memory space
|
||||||
@ -605,138 +615,143 @@ pub const Face = struct {
|
|||||||
fn calcMetrics(
|
fn calcMetrics(
|
||||||
face: freetype.Face,
|
face: freetype.Face,
|
||||||
modifiers: ?*const font.face.Metrics.ModifierSet,
|
modifiers: ?*const font.face.Metrics.ModifierSet,
|
||||||
) font.face.Metrics {
|
) CalcMetricsError!font.face.Metrics {
|
||||||
const size_metrics = face.handle.*.size.*.metrics;
|
const size_metrics = face.handle.*.size.*.metrics;
|
||||||
|
|
||||||
// Cell width is calculated by preferring to use 'M' as the width of a
|
// This code relies on this assumption, and it should always be
|
||||||
// cell since 'M' is generally the widest ASCII character. If loading 'M'
|
// true since we don't do any non-uniform scaling on the font ever.
|
||||||
// fails then we use the max advance of the font face size metrics.
|
assert(size_metrics.x_ppem == size_metrics.y_ppem);
|
||||||
const cell_width: f32 = cell_width: {
|
|
||||||
if (face.getCharIndex('M')) |glyph_index| {
|
// Read the 'head' table out of the font data.
|
||||||
if (face.loadGlyph(glyph_index, .{ .render = true })) {
|
const head = face.getSfntTable(.head) orelse return error.CopyTableError;
|
||||||
break :cell_width f26dot6ToFloat(face.handle.*.glyph.*.advance.x);
|
|
||||||
} else |_| {
|
// Read the 'post' table out of the font data.
|
||||||
// Ignore the error since we just fall back to max_advance below
|
const post = face.getSfntTable(.post) orelse return error.CopyTableError;
|
||||||
|
|
||||||
|
// Read the 'OS/2' table out of the font data.
|
||||||
|
const os2 = face.getSfntTable(.os2) 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 px_per_em: f64 = @floatFromInt(size_metrics.y_ppem);
|
||||||
|
const px_per_unit = px_per_em / @as(f64, @floatFromInt(units_per_em));
|
||||||
|
|
||||||
|
const ascent = @as(f64, @floatFromInt(os2.sTypoAscender)) * px_per_unit;
|
||||||
|
const descent = @as(f64, @floatFromInt(os2.sTypoDescender)) * px_per_unit;
|
||||||
|
const line_gap = @as(f64, @floatFromInt(os2.sTypoLineGap)) * px_per_unit;
|
||||||
|
|
||||||
|
// Some fonts have degenerate 'post' tables where the underline
|
||||||
|
// thickness (and often position) are 0. We consider them 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,
|
||||||
|
// even if the thickness is't properly specified.
|
||||||
|
const underline_position = if (has_broken_underline and post.underlinePosition == 0)
|
||||||
|
null
|
||||||
|
else
|
||||||
|
@as(f64, @floatFromInt(post.underlinePosition)) * px_per_unit;
|
||||||
|
|
||||||
|
const underline_thickness = if (has_broken_underline)
|
||||||
|
null
|
||||||
|
else
|
||||||
|
@as(f64, @floatFromInt(post.underlineThickness)) * px_per_unit;
|
||||||
|
|
||||||
|
// Similar logic to the underline above.
|
||||||
|
const has_broken_strikethrough = os2.yStrikeoutSize == 0;
|
||||||
|
|
||||||
|
const strikethrough_position = if (has_broken_strikethrough and os2.yStrikeoutPosition == 0)
|
||||||
|
null
|
||||||
|
else
|
||||||
|
@as(f64, @floatFromInt(os2.yStrikeoutPosition)) * px_per_unit;
|
||||||
|
|
||||||
|
const strikethrough_thickness = if (has_broken_strikethrough)
|
||||||
|
null
|
||||||
|
else
|
||||||
|
@as(f64, @floatFromInt(os2.yStrikeoutSize)) * px_per_unit;
|
||||||
|
|
||||||
|
// Cell width is calculated by calculating the widest width of the
|
||||||
|
// visible ASCII characters. Usually 'M' is widest but we just take
|
||||||
|
// whatever is widest.
|
||||||
|
//
|
||||||
|
// If we fail to load any visible ASCII we just use max_advance from
|
||||||
|
// the metrics provided by FreeType.
|
||||||
|
const cell_width: f64 = cell_width: {
|
||||||
|
var max: f64 = 0.0;
|
||||||
|
var c: u8 = ' ';
|
||||||
|
while (c < 127) : (c += 1) {
|
||||||
|
if (face.getCharIndex(c)) |glyph_index| {
|
||||||
|
if (face.loadGlyph(glyph_index, .{ .render = true })) {
|
||||||
|
max = @max(
|
||||||
|
f26dot6ToF64(face.handle.*.glyph.*.advance.x),
|
||||||
|
max,
|
||||||
|
);
|
||||||
|
} else |_| {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break :cell_width f26dot6ToFloat(size_metrics.max_advance);
|
// If we couldn't get any widths, just use FreeType's max_advance.
|
||||||
|
if (max == 0.0) {
|
||||||
|
break :cell_width f26dot6ToF64(size_metrics.max_advance);
|
||||||
|
}
|
||||||
|
|
||||||
|
break :cell_width max;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ex height is calculated by measuring the height of the `x` glyph.
|
// The OS/2 table does not include sCapHeight or sxHeight in version 1.
|
||||||
// If that fails then we just pretend it's 65% of the ascent height.
|
const has_os2_height_metrics = os2.version >= 2;
|
||||||
const ex_height: f32 = ex_height: {
|
|
||||||
|
// We use the cap height specified by the font if it's
|
||||||
|
// available, otherwise we try to measure the `H` glyph.
|
||||||
|
const cap_height: ?f64 = cap_height: {
|
||||||
|
if (has_os2_height_metrics) {
|
||||||
|
break :cap_height @as(f64, @floatFromInt(os2.sCapHeight)) * px_per_unit;
|
||||||
|
}
|
||||||
|
if (face.getCharIndex('H')) |glyph_index| {
|
||||||
|
if (face.loadGlyph(glyph_index, .{ .render = true })) {
|
||||||
|
break :cap_height f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
|
||||||
|
} else |_| {}
|
||||||
|
}
|
||||||
|
|
||||||
|
break :cap_height null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// We use the ex height specified by the font if it's
|
||||||
|
// available, otherwise we try to measure the `x` glyph.
|
||||||
|
const ex_height: ?f64 = ex_height: {
|
||||||
|
if (has_os2_height_metrics) {
|
||||||
|
break :ex_height @as(f64, @floatFromInt(os2.sxHeight)) * px_per_unit;
|
||||||
|
}
|
||||||
if (face.getCharIndex('x')) |glyph_index| {
|
if (face.getCharIndex('x')) |glyph_index| {
|
||||||
if (face.loadGlyph(glyph_index, .{ .render = true })) {
|
if (face.loadGlyph(glyph_index, .{ .render = true })) {
|
||||||
break :ex_height f26dot6ToFloat(face.handle.*.glyph.*.metrics.height);
|
break :ex_height f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
|
||||||
} else |_| {
|
} else |_| {}
|
||||||
// Ignore the error since we just fall back to 65% of the ascent below
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break :ex_height f26dot6ToFloat(size_metrics.ascender) * 0.65;
|
break :ex_height null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Cell height is calculated as the maximum of multiple things in order
|
var result = font.face.Metrics.calc(.{
|
||||||
// to handle edge cases in fonts: (1) the height as reported in metadata
|
.cell_width = cell_width,
|
||||||
// by the font designer (2) the maximum glyph height as measured in the
|
|
||||||
// font and (3) the height from the ascender to an underscore.
|
|
||||||
const cell_height: f32 = cell_height: {
|
|
||||||
// The height as reported by the font designer.
|
|
||||||
const face_height = f26dot6ToFloat(size_metrics.height);
|
|
||||||
|
|
||||||
// The maximum height a glyph can take in the font
|
.ascent = ascent,
|
||||||
const max_glyph_height = f26dot6ToFloat(size_metrics.ascender) -
|
.descent = descent,
|
||||||
f26dot6ToFloat(size_metrics.descender);
|
.line_gap = line_gap,
|
||||||
|
|
||||||
// The height of the underscore character
|
.underline_position = underline_position,
|
||||||
const underscore_height = underscore: {
|
.underline_thickness = underline_thickness,
|
||||||
if (face.getCharIndex('_')) |glyph_index| {
|
|
||||||
if (face.loadGlyph(glyph_index, .{ .render = true })) {
|
|
||||||
var res: f32 = f26dot6ToFloat(size_metrics.ascender);
|
|
||||||
res -= @floatFromInt(face.handle.*.glyph.*.bitmap_top);
|
|
||||||
res += @floatFromInt(face.handle.*.glyph.*.bitmap.rows);
|
|
||||||
break :underscore res;
|
|
||||||
} else |_| {
|
|
||||||
// Ignore the error since we just fall back below
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break :underscore 0;
|
.strikethrough_position = strikethrough_position,
|
||||||
};
|
.strikethrough_thickness = strikethrough_thickness,
|
||||||
|
|
||||||
break :cell_height @max(
|
.cap_height = cap_height,
|
||||||
face_height,
|
.ex_height = ex_height,
|
||||||
@max(max_glyph_height, underscore_height),
|
});
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// The baseline is the descender amount for the font. This is the maximum
|
|
||||||
// that a font may go down. We switch signs because our coordinate system
|
|
||||||
// is reversed.
|
|
||||||
const cell_baseline = -1 * f26dot6ToFloat(size_metrics.descender);
|
|
||||||
|
|
||||||
const underline_thickness = @max(@as(f32, 1), fontUnitsToPxY(
|
|
||||||
face,
|
|
||||||
face.handle.*.underline_thickness,
|
|
||||||
));
|
|
||||||
|
|
||||||
// The underline position. This is a value from the top where the
|
|
||||||
// underline should go.
|
|
||||||
const underline_position: f32 = underline_pos: {
|
|
||||||
// From the FreeType docs:
|
|
||||||
// > `underline_position`
|
|
||||||
// > The position, in font units, of the underline line for
|
|
||||||
// > this face. It is the center of the underlining stem.
|
|
||||||
|
|
||||||
const declared_px = @as(f32, @floatFromInt(freetype.mulFix(
|
|
||||||
face.handle.*.underline_position,
|
|
||||||
@intCast(face.handle.*.size.*.metrics.y_scale),
|
|
||||||
))) / 64;
|
|
||||||
|
|
||||||
// We use the declared underline position if its available.
|
|
||||||
const declared = @ceil(cell_height - cell_baseline - declared_px - underline_thickness * 0.5);
|
|
||||||
if (declared > 0)
|
|
||||||
break :underline_pos declared;
|
|
||||||
|
|
||||||
// If we have no declared underline position, we go slightly under the
|
|
||||||
// cell height (mainly: non-scalable fonts, i.e. emoji)
|
|
||||||
break :underline_pos cell_height - 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
// The strikethrough position. We use the position provided by the
|
|
||||||
// font if it exists otherwise we calculate a best guess.
|
|
||||||
const strikethrough: struct {
|
|
||||||
pos: f32,
|
|
||||||
thickness: f32,
|
|
||||||
} = if (face.getSfntTable(.os2)) |os2| st: {
|
|
||||||
const thickness = @max(@as(f32, 1), fontUnitsToPxY(face, os2.yStrikeoutSize));
|
|
||||||
|
|
||||||
const pos = @as(f32, @floatFromInt(freetype.mulFix(
|
|
||||||
os2.yStrikeoutPosition,
|
|
||||||
@as(i32, @intCast(face.handle.*.size.*.metrics.y_scale)),
|
|
||||||
))) / 64;
|
|
||||||
|
|
||||||
break :st .{
|
|
||||||
.pos = @ceil(cell_height - cell_baseline - pos),
|
|
||||||
.thickness = thickness,
|
|
||||||
};
|
|
||||||
} else .{
|
|
||||||
// Exactly 50% of the ex height so that our strikethrough is
|
|
||||||
// centered through lowercase text. This is a common choice.
|
|
||||||
.pos = @ceil(cell_height - cell_baseline - ex_height * 0.5 - underline_thickness * 0.5),
|
|
||||||
.thickness = underline_thickness,
|
|
||||||
};
|
|
||||||
|
|
||||||
var result = font.face.Metrics{
|
|
||||||
.cell_width = @intFromFloat(cell_width),
|
|
||||||
.cell_height = @intFromFloat(cell_height),
|
|
||||||
.cell_baseline = @intFromFloat(cell_baseline),
|
|
||||||
.underline_position = @intFromFloat(underline_position),
|
|
||||||
.underline_thickness = @intFromFloat(underline_thickness),
|
|
||||||
.strikethrough_position = @intFromFloat(strikethrough.pos),
|
|
||||||
.strikethrough_thickness = @intFromFloat(strikethrough.thickness),
|
|
||||||
};
|
|
||||||
if (modifiers) |m| result.apply(m.*);
|
if (modifiers) |m| result.apply(m.*);
|
||||||
|
|
||||||
// std.log.warn("font metrics={}", .{result});
|
// std.log.warn("font metrics={}", .{result});
|
||||||
@ -744,13 +759,6 @@ pub const Face = struct {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert freetype "font units" to pixels using the Y scale.
|
|
||||||
fn fontUnitsToPxY(face: freetype.Face, x: i32) f32 {
|
|
||||||
const mul = freetype.mulFix(x, @as(i32, @intCast(face.handle.*.size.*.metrics.y_scale)));
|
|
||||||
const div = @as(f32, @floatFromInt(mul)) / 64;
|
|
||||||
return @ceil(div);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Copy the font table data for the given tag.
|
/// Copy the font table data for the given tag.
|
||||||
pub fn copyTable(self: Face, alloc: Allocator, tag: *const [4]u8) !?[]u8 {
|
pub fn copyTable(self: Face, alloc: Allocator, tag: *const [4]u8) !?[]u8 {
|
||||||
return try self.face.loadSfntTable(alloc, freetype.Tag.init(tag));
|
return try self.face.loadSfntTable(alloc, freetype.Tag.init(tag));
|
||||||
@ -828,6 +836,9 @@ test "color emoji" {
|
|||||||
.underline_thickness = 0,
|
.underline_thickness = 0,
|
||||||
.strikethrough_position = 0,
|
.strikethrough_position = 0,
|
||||||
.strikethrough_thickness = 0,
|
.strikethrough_thickness = 0,
|
||||||
|
.overline_position = 0,
|
||||||
|
.overline_thickness = 0,
|
||||||
|
.box_thickness = 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
try testing.expectEqual(@as(u32, 24), glyph.height);
|
try testing.expectEqual(@as(u32, 24), glyph.height);
|
||||||
@ -853,24 +864,42 @@ test "metrics" {
|
|||||||
|
|
||||||
try testing.expectEqual(font.face.Metrics{
|
try testing.expectEqual(font.face.Metrics{
|
||||||
.cell_width = 8,
|
.cell_width = 8,
|
||||||
.cell_height = 1.8e1,
|
// The cell height is 17 px because the calculation is
|
||||||
.cell_baseline = 4,
|
//
|
||||||
.underline_position = 18,
|
// ascender - descender + gap
|
||||||
|
//
|
||||||
|
// which, for inconsolata is
|
||||||
|
//
|
||||||
|
// 859 - -190 + 0
|
||||||
|
//
|
||||||
|
// font units, at 1000 units per em that works out to 1.049 em,
|
||||||
|
// and 1em should be the point size * dpi scale, so 12 * (96/72)
|
||||||
|
// which is 16, and 16 * 1.049 = 16.784, which finally is rounded
|
||||||
|
// to 17.
|
||||||
|
.cell_height = 17,
|
||||||
|
.cell_baseline = 3,
|
||||||
|
.underline_position = 17,
|
||||||
.underline_thickness = 1,
|
.underline_thickness = 1,
|
||||||
.strikethrough_position = 10,
|
.strikethrough_position = 10,
|
||||||
.strikethrough_thickness = 1,
|
.strikethrough_thickness = 1,
|
||||||
|
.overline_position = 0,
|
||||||
|
.overline_thickness = 1,
|
||||||
|
.box_thickness = 1,
|
||||||
}, ft_font.metrics);
|
}, ft_font.metrics);
|
||||||
|
|
||||||
// Resize should change metrics
|
// Resize should change metrics
|
||||||
try ft_font.setSize(.{ .size = .{ .points = 24, .xdpi = 96, .ydpi = 96 } });
|
try ft_font.setSize(.{ .size = .{ .points = 24, .xdpi = 96, .ydpi = 96 } });
|
||||||
try testing.expectEqual(font.face.Metrics{
|
try testing.expectEqual(font.face.Metrics{
|
||||||
.cell_width = 16,
|
.cell_width = 16,
|
||||||
.cell_height = 35,
|
.cell_height = 34,
|
||||||
.cell_baseline = 7,
|
.cell_baseline = 6,
|
||||||
.underline_position = 35,
|
.underline_position = 34,
|
||||||
.underline_thickness = 2,
|
.underline_thickness = 2,
|
||||||
.strikethrough_position = 20,
|
.strikethrough_position = 19,
|
||||||
.strikethrough_thickness = 2,
|
.strikethrough_thickness = 2,
|
||||||
|
.overline_position = 0,
|
||||||
|
.overline_thickness = 2,
|
||||||
|
.box_thickness = 2,
|
||||||
}, ft_font.metrics);
|
}, ft_font.metrics);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,16 @@
|
|||||||
|
pub const sfnt = @import("opentype/sfnt.zig");
|
||||||
|
|
||||||
const svg = @import("opentype/svg.zig");
|
const svg = @import("opentype/svg.zig");
|
||||||
|
const os2 = @import("opentype/os2.zig");
|
||||||
|
const post = @import("opentype/post.zig");
|
||||||
|
const hhea = @import("opentype/hhea.zig");
|
||||||
|
const head = @import("opentype/head.zig");
|
||||||
|
|
||||||
pub const SVG = svg.SVG;
|
pub const SVG = svg.SVG;
|
||||||
|
pub const OS2 = os2.OS2;
|
||||||
|
pub const Post = post.Post;
|
||||||
|
pub const Hhea = hhea.Hhea;
|
||||||
|
pub const Head = head.Head;
|
||||||
|
|
||||||
test {
|
test {
|
||||||
@import("std").testing.refAllDecls(@This());
|
@import("std").testing.refAllDecls(@This());
|
||||||
|
180
src/font/opentype/head.zig
Normal file
180
src/font/opentype/head.zig
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const sfnt = @import("sfnt.zig");
|
||||||
|
|
||||||
|
/// Font Header Table
|
||||||
|
///
|
||||||
|
/// References:
|
||||||
|
/// - https://learn.microsoft.com/en-us/typography/opentype/spec/head
|
||||||
|
///
|
||||||
|
/// Field names are in camelCase to match names in spec.
|
||||||
|
pub const Head = extern struct {
|
||||||
|
/// Major version number of the font header table — set to 1.
|
||||||
|
majorVersion: sfnt.uint16 align(1),
|
||||||
|
|
||||||
|
/// Minor version number of the font header table — set to 0.
|
||||||
|
minorVersion: sfnt.uint16 align(1),
|
||||||
|
|
||||||
|
/// Set by font manufacturer.
|
||||||
|
fontRevision: sfnt.Fixed align(1),
|
||||||
|
|
||||||
|
/// To compute: set it to 0, sum the entire font as uint32, then store
|
||||||
|
/// 0xB1B0AFBA - sum. If the font is used as a component in a font
|
||||||
|
/// collection file, the value of this field will be invalidated by
|
||||||
|
/// changes to the file structure and font table directory, and must
|
||||||
|
/// be ignored.
|
||||||
|
checksumAdjustment: sfnt.uint32 align(1),
|
||||||
|
|
||||||
|
/// Set to 0x5F0F3CF5.
|
||||||
|
magicNumber: sfnt.uint32 align(1),
|
||||||
|
|
||||||
|
/// Bit 0: Baseline for font at y=0.
|
||||||
|
///
|
||||||
|
/// Bit 1: Left sidebearing point at x=0
|
||||||
|
/// (relevant only for TrueType rasterizers)
|
||||||
|
///
|
||||||
|
/// Bit 2: Instructions may depend on point size.
|
||||||
|
///
|
||||||
|
/// Bit 3: Force ppem to integer values for all internal scaler math; may
|
||||||
|
/// use fractional ppem sizes if this bit is clear. It is strongly
|
||||||
|
/// recommended that this be set in hinted fonts.
|
||||||
|
///
|
||||||
|
/// Bit 4: Instructions may alter advance width
|
||||||
|
/// (the advance widths might not scale linearly).
|
||||||
|
///
|
||||||
|
/// Bit 5: This bit is not used in OpenType, and should not be set in order
|
||||||
|
/// to ensure compatible behavior on all platforms. If set, it may
|
||||||
|
/// result in different behavior for vertical layout in some
|
||||||
|
/// platforms.
|
||||||
|
///
|
||||||
|
/// (See Apple’s specification for details
|
||||||
|
/// regarding behavior in Apple platforms.)
|
||||||
|
///
|
||||||
|
/// Bits 6 – 10: These bits are not used in OpenType and should always be
|
||||||
|
/// cleared.
|
||||||
|
///
|
||||||
|
/// (See Apple’s specification for details
|
||||||
|
/// regarding legacy use in Apple platforms.)
|
||||||
|
///
|
||||||
|
/// Bit 11: Font data is “lossless” as a result of having been
|
||||||
|
/// subjected to optimizing transformation and/or compression
|
||||||
|
/// (such as compression mechanisms defined by ISO/IEC 14496-18,
|
||||||
|
/// MicroType® Express, WOFF 2.0, or similar) where the original
|
||||||
|
/// font functionality and features are retained but the binary
|
||||||
|
/// compatibility between input and output font files is not
|
||||||
|
/// guaranteed. As a result of the applied transform, the DSIG
|
||||||
|
/// table may also be invalidated.
|
||||||
|
///
|
||||||
|
/// Bit 12: Font converted (produce compatible metrics).
|
||||||
|
///
|
||||||
|
/// Bit 13: Font optimized for ClearType®. Note, fonts that rely on embedded
|
||||||
|
/// bitmaps (EBDT) for rendering should not be considered optimized
|
||||||
|
/// for ClearType, and therefore should keep this bit cleared.
|
||||||
|
///
|
||||||
|
/// Bit 14: Last Resort font. If set, indicates that the glyphs encoded in
|
||||||
|
/// the 'cmap' subtables are simply generic symbolic representations
|
||||||
|
/// of code point ranges and do not truly represent support for
|
||||||
|
/// those code points. If unset, indicates that the glyphs encoded
|
||||||
|
/// in the 'cmap' subtables represent proper support for those code
|
||||||
|
/// points.
|
||||||
|
///
|
||||||
|
/// Bit 15: Reserved, set to 0.
|
||||||
|
flags: sfnt.uint16 align(1),
|
||||||
|
|
||||||
|
/// Set to a value from 16 to 16384. Any value in this range is valid.
|
||||||
|
///
|
||||||
|
/// In fonts that have TrueType outlines, a power of 2 is recommended
|
||||||
|
/// as this allows performance optimization in some rasterizers.
|
||||||
|
unitsPerEm: sfnt.uint16 align(1),
|
||||||
|
|
||||||
|
/// Number of seconds since 12:00 midnight that started
|
||||||
|
/// January 1st, 1904, in GMT/UTC time zone.
|
||||||
|
created: sfnt.LONGDATETIME align(1),
|
||||||
|
|
||||||
|
/// Number of seconds since 12:00 midnight that started
|
||||||
|
/// January 1st, 1904, in GMT/UTC time zone.
|
||||||
|
modified: sfnt.LONGDATETIME align(1),
|
||||||
|
|
||||||
|
/// Minimum x coordinate across all glyph bounding boxes.
|
||||||
|
xMin: sfnt.int16 align(1),
|
||||||
|
|
||||||
|
/// Minimum y coordinate across all glyph bounding boxes.
|
||||||
|
yMin: sfnt.int16 align(1),
|
||||||
|
|
||||||
|
/// Maximum x coordinate across all glyph bounding boxes.
|
||||||
|
xMax: sfnt.int16 align(1),
|
||||||
|
|
||||||
|
/// Maximum y coordinate across all glyph bounding boxes.
|
||||||
|
yMax: sfnt.int16 align(1),
|
||||||
|
|
||||||
|
/// Bit 0: Bold (if set to 1);
|
||||||
|
/// Bit 1: Italic (if set to 1)
|
||||||
|
/// Bit 2: Underline (if set to 1)
|
||||||
|
/// Bit 3: Outline (if set to 1)
|
||||||
|
/// Bit 4: Shadow (if set to 1)
|
||||||
|
/// Bit 5: Condensed (if set to 1)
|
||||||
|
/// Bit 6: Extended (if set to 1)
|
||||||
|
/// Bits 7 – 15: Reserved (set to 0).
|
||||||
|
macStyle: sfnt.uint16 align(1),
|
||||||
|
|
||||||
|
/// Smallest readable size in pixels.
|
||||||
|
lowestRecPPEM: sfnt.uint16 align(1),
|
||||||
|
|
||||||
|
/// Deprecated (Set to 2).
|
||||||
|
/// 0: Fully mixed directional glyphs;
|
||||||
|
/// 1: Only strongly left to right;
|
||||||
|
/// 2: Like 1 but also contains neutrals;
|
||||||
|
/// -1: Only strongly right to left;
|
||||||
|
/// -2: Like -1 but also contains neutrals.
|
||||||
|
fontDirectionHint: sfnt.int16 align(1),
|
||||||
|
|
||||||
|
/// 0 for short offsets (Offset16), 1 for long (Offset32).
|
||||||
|
indexToLocFormat: sfnt.int16 align(1),
|
||||||
|
|
||||||
|
/// 0 for current format.
|
||||||
|
glyphDataFormat: sfnt.int16 align(1),
|
||||||
|
|
||||||
|
/// Parse the table from raw data.
|
||||||
|
pub fn init(data: []const u8) error{EndOfStream}!Head {
|
||||||
|
var fbs = std.io.fixedBufferStream(data);
|
||||||
|
const reader = fbs.reader();
|
||||||
|
return try reader.readStructEndian(Head, .big);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test "head" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
const test_font = @import("../embedded.zig").julia_mono;
|
||||||
|
|
||||||
|
const font = try sfnt.SFNT.init(test_font, alloc);
|
||||||
|
defer font.deinit(alloc);
|
||||||
|
|
||||||
|
const table = font.getTable("head").?;
|
||||||
|
|
||||||
|
const head = try Head.init(table);
|
||||||
|
|
||||||
|
try testing.expectEqualDeep(
|
||||||
|
Head{
|
||||||
|
.majorVersion = 1,
|
||||||
|
.minorVersion = 0,
|
||||||
|
.fontRevision = sfnt.Fixed.from(0.05499267578125),
|
||||||
|
.checksumAdjustment = 1007668681,
|
||||||
|
.magicNumber = 1594834165,
|
||||||
|
.flags = 7,
|
||||||
|
.unitsPerEm = 2000,
|
||||||
|
.created = 3797757830,
|
||||||
|
.modified = 3797760444,
|
||||||
|
.xMin = -1000,
|
||||||
|
.yMin = -1058,
|
||||||
|
.xMax = 3089,
|
||||||
|
.yMax = 2400,
|
||||||
|
.macStyle = 0,
|
||||||
|
.lowestRecPPEM = 7,
|
||||||
|
.fontDirectionHint = 2,
|
||||||
|
.indexToLocFormat = 1,
|
||||||
|
.glyphDataFormat = 0,
|
||||||
|
},
|
||||||
|
head,
|
||||||
|
);
|
||||||
|
}
|
117
src/font/opentype/hhea.zig
Normal file
117
src/font/opentype/hhea.zig
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const sfnt = @import("sfnt.zig");
|
||||||
|
|
||||||
|
/// Horizontal Header Table
|
||||||
|
///
|
||||||
|
/// References:
|
||||||
|
/// - https://learn.microsoft.com/en-us/typography/opentype/spec/hhea
|
||||||
|
///
|
||||||
|
/// Field names are in camelCase to match names in spec.
|
||||||
|
pub const Hhea = extern struct {
|
||||||
|
/// Major version number of the horizontal header table — set to 1.
|
||||||
|
majorVersion: sfnt.uint16 align(1),
|
||||||
|
|
||||||
|
/// Minor version number of the horizontal header table — set to 0.
|
||||||
|
minorVersion: sfnt.uint16 align(1),
|
||||||
|
|
||||||
|
/// Typographic ascent—see remarks below.
|
||||||
|
ascender: sfnt.FWORD align(1),
|
||||||
|
|
||||||
|
/// Typographic descent—see remarks below.
|
||||||
|
descender: sfnt.FWORD align(1),
|
||||||
|
|
||||||
|
/// Typographic line gap.
|
||||||
|
///
|
||||||
|
/// Negative lineGap values are treated as zero
|
||||||
|
/// in some legacy platform implementations.
|
||||||
|
lineGap: sfnt.FWORD align(1),
|
||||||
|
|
||||||
|
/// Maximum advance width value in 'hmtx' table.
|
||||||
|
advanceWidthMax: sfnt.UFWORD align(1),
|
||||||
|
|
||||||
|
/// Minimum left sidebearing value in 'hmtx' table for
|
||||||
|
/// glyphs with contours (empty glyphs should be ignored).
|
||||||
|
minLeftSideBearing: sfnt.FWORD align(1),
|
||||||
|
|
||||||
|
/// Minimum right sidebearing value; calculated as
|
||||||
|
/// min(aw - (lsb + xMax - xMin)) for glyphs with
|
||||||
|
/// contours (empty glyphs should be ignored).
|
||||||
|
minRightSideBearing: sfnt.FWORD align(1),
|
||||||
|
|
||||||
|
/// Max(lsb + (xMax - xMin)).
|
||||||
|
xMaxExtent: sfnt.FWORD align(1),
|
||||||
|
|
||||||
|
/// Used to calculate the slope of the cursor (rise/run); 1 for vertical.
|
||||||
|
caretSlopeRise: sfnt.int16 align(1),
|
||||||
|
|
||||||
|
/// 0 for vertical.
|
||||||
|
caretSlopeRun: sfnt.int16 align(1),
|
||||||
|
|
||||||
|
/// The amount by which a slanted highlight on a glyph needs to be shifted
|
||||||
|
/// to produce the best appearance. Set to 0 for non-slanted fonts
|
||||||
|
caretOffset: sfnt.int16 align(1),
|
||||||
|
|
||||||
|
/// set to 0
|
||||||
|
_reserved0: sfnt.int16 align(1),
|
||||||
|
|
||||||
|
/// set to 0
|
||||||
|
_reserved1: sfnt.int16 align(1),
|
||||||
|
|
||||||
|
/// set to 0
|
||||||
|
_reserved2: sfnt.int16 align(1),
|
||||||
|
|
||||||
|
/// set to 0
|
||||||
|
_reserved3: sfnt.int16 align(1),
|
||||||
|
|
||||||
|
/// 0 for current format.
|
||||||
|
metricDataFormat: sfnt.int16 align(1),
|
||||||
|
|
||||||
|
/// Number of hMetric entries in 'hmtx' table
|
||||||
|
numberOfHMetrics: sfnt.uint16 align(1),
|
||||||
|
|
||||||
|
/// Parse the table from raw data.
|
||||||
|
pub fn init(data: []const u8) !Hhea {
|
||||||
|
var fbs = std.io.fixedBufferStream(data);
|
||||||
|
const reader = fbs.reader();
|
||||||
|
|
||||||
|
return try reader.readStructEndian(Hhea, .big);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test "hhea" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
const test_font = @import("../embedded.zig").julia_mono;
|
||||||
|
|
||||||
|
const font = try sfnt.SFNT.init(test_font, alloc);
|
||||||
|
defer font.deinit(alloc);
|
||||||
|
|
||||||
|
const table = font.getTable("hhea").?;
|
||||||
|
|
||||||
|
const hhea = try Hhea.init(table);
|
||||||
|
|
||||||
|
try testing.expectEqualDeep(
|
||||||
|
Hhea{
|
||||||
|
.majorVersion = 1,
|
||||||
|
.minorVersion = 0,
|
||||||
|
.ascender = 1900,
|
||||||
|
.descender = -450,
|
||||||
|
.lineGap = 0,
|
||||||
|
.advanceWidthMax = 1200,
|
||||||
|
.minLeftSideBearing = -1000,
|
||||||
|
.minRightSideBearing = -1889,
|
||||||
|
.xMaxExtent = 3089,
|
||||||
|
.caretSlopeRise = 1,
|
||||||
|
.caretSlopeRun = 0,
|
||||||
|
.caretOffset = 0,
|
||||||
|
._reserved0 = 0,
|
||||||
|
._reserved1 = 0,
|
||||||
|
._reserved2 = 0,
|
||||||
|
._reserved3 = 0,
|
||||||
|
.metricDataFormat = 0,
|
||||||
|
.numberOfHMetrics = 2,
|
||||||
|
},
|
||||||
|
hhea,
|
||||||
|
);
|
||||||
|
}
|
584
src/font/opentype/os2.zig
Normal file
584
src/font/opentype/os2.zig
Normal file
@ -0,0 +1,584 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const sfnt = @import("sfnt.zig");
|
||||||
|
|
||||||
|
pub const FSSelection = packed struct(sfnt.uint16) {
|
||||||
|
/// Font contains italic or oblique glyphs, otherwise they are upright.
|
||||||
|
italic: bool = false,
|
||||||
|
|
||||||
|
/// Glyphs are underscored.
|
||||||
|
underscore: bool = false,
|
||||||
|
|
||||||
|
/// Glyphs have their foreground and background reversed.
|
||||||
|
negative: bool = false,
|
||||||
|
|
||||||
|
/// Outline (hollow) glyphs, otherwise they are solid.
|
||||||
|
outlined: bool = false,
|
||||||
|
|
||||||
|
/// Glyphs are overstruck.
|
||||||
|
strikeout: bool = false,
|
||||||
|
|
||||||
|
/// Glyphs are emboldened.
|
||||||
|
bold: bool = false,
|
||||||
|
|
||||||
|
/// Glyphs are in the standard weight/style for the font.
|
||||||
|
regular: bool = false,
|
||||||
|
|
||||||
|
/// If set, it is strongly recommended that applications use
|
||||||
|
/// OS/2.sTypoAscender - OS/2.sTypoDescender + OS/2.sTypoLineGap
|
||||||
|
/// as the default line spacing for this font.
|
||||||
|
use_typo_metrics: bool = false,
|
||||||
|
|
||||||
|
/// The font has 'name' table strings consistent with a weight/width/slope
|
||||||
|
/// family without requiring use of name IDs 21 and 22.
|
||||||
|
wws: bool = false,
|
||||||
|
|
||||||
|
/// Font contains oblique glyphs.
|
||||||
|
oblique: bool = false,
|
||||||
|
|
||||||
|
_reserved: u6 = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// OS/2 and Windows Metrics Table
|
||||||
|
///
|
||||||
|
/// References:
|
||||||
|
/// - https://learn.microsoft.com/en-us/typography/opentype/spec/os2
|
||||||
|
///
|
||||||
|
/// Field names are in camelCase to match names in spec.
|
||||||
|
pub const OS2v5 = extern struct {
|
||||||
|
version: sfnt.uint16 align(1),
|
||||||
|
xAvgCharWidth: sfnt.FWORD align(1),
|
||||||
|
usWeightClass: sfnt.uint16 align(1),
|
||||||
|
usWidthClass: sfnt.uint16 align(1),
|
||||||
|
fsType: sfnt.uint16 align(1),
|
||||||
|
ySubscriptXSize: sfnt.FWORD align(1),
|
||||||
|
ySubscriptYSize: sfnt.FWORD align(1),
|
||||||
|
ySubscriptXOffset: sfnt.FWORD align(1),
|
||||||
|
ySubscriptYOffset: sfnt.FWORD align(1),
|
||||||
|
ySuperscriptXSize: sfnt.FWORD align(1),
|
||||||
|
ySuperscriptYSize: sfnt.FWORD align(1),
|
||||||
|
ySuperscriptXOffset: sfnt.FWORD align(1),
|
||||||
|
ySuperscriptYOffset: sfnt.FWORD align(1),
|
||||||
|
yStrikeoutSize: sfnt.FWORD align(1),
|
||||||
|
yStrikeoutPosition: sfnt.FWORD align(1),
|
||||||
|
sFamilyClass: sfnt.int16 align(1),
|
||||||
|
panose: [10]sfnt.uint8 align(1),
|
||||||
|
ulUnicodeRange1: sfnt.uint32 align(1),
|
||||||
|
ulUnicodeRange2: sfnt.uint32 align(1),
|
||||||
|
ulUnicodeRange3: sfnt.uint32 align(1),
|
||||||
|
ulUnicodeRange4: sfnt.uint32 align(1),
|
||||||
|
achVendID: sfnt.Tag align(1),
|
||||||
|
fsSelection: FSSelection align(1),
|
||||||
|
usFirstCharIndex: sfnt.uint16 align(1),
|
||||||
|
usLastCharIndex: sfnt.uint16 align(1),
|
||||||
|
sTypoAscender: sfnt.FWORD align(1),
|
||||||
|
sTypoDescender: sfnt.FWORD align(1),
|
||||||
|
sTypoLineGap: sfnt.FWORD align(1),
|
||||||
|
usWinAscent: sfnt.UFWORD align(1),
|
||||||
|
usWinDescent: sfnt.UFWORD align(1),
|
||||||
|
ulCodePageRange1: sfnt.uint32 align(1),
|
||||||
|
ulCodePageRange2: sfnt.uint32 align(1),
|
||||||
|
sxHeight: sfnt.FWORD align(1),
|
||||||
|
sCapHeight: sfnt.FWORD align(1),
|
||||||
|
usDefaultChar: sfnt.uint16 align(1),
|
||||||
|
usBreakChar: sfnt.uint16 align(1),
|
||||||
|
usMaxContext: sfnt.uint16 align(1),
|
||||||
|
usLowerOpticalPointSize: sfnt.uint16 align(1),
|
||||||
|
usUpperOpticalPointSize: sfnt.uint16 align(1),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const OS2v4_3_2 = extern struct {
|
||||||
|
version: sfnt.uint16 align(1),
|
||||||
|
xAvgCharWidth: sfnt.FWORD align(1),
|
||||||
|
usWeightClass: sfnt.uint16 align(1),
|
||||||
|
usWidthClass: sfnt.uint16 align(1),
|
||||||
|
fsType: sfnt.uint16 align(1),
|
||||||
|
ySubscriptXSize: sfnt.FWORD align(1),
|
||||||
|
ySubscriptYSize: sfnt.FWORD align(1),
|
||||||
|
ySubscriptXOffset: sfnt.FWORD align(1),
|
||||||
|
ySubscriptYOffset: sfnt.FWORD align(1),
|
||||||
|
ySuperscriptXSize: sfnt.FWORD align(1),
|
||||||
|
ySuperscriptYSize: sfnt.FWORD align(1),
|
||||||
|
ySuperscriptXOffset: sfnt.FWORD align(1),
|
||||||
|
ySuperscriptYOffset: sfnt.FWORD align(1),
|
||||||
|
yStrikeoutSize: sfnt.FWORD align(1),
|
||||||
|
yStrikeoutPosition: sfnt.FWORD align(1),
|
||||||
|
sFamilyClass: sfnt.int16 align(1),
|
||||||
|
panose: [10]sfnt.uint8 align(1),
|
||||||
|
ulUnicodeRange1: sfnt.uint32 align(1),
|
||||||
|
ulUnicodeRange2: sfnt.uint32 align(1),
|
||||||
|
ulUnicodeRange3: sfnt.uint32 align(1),
|
||||||
|
ulUnicodeRange4: sfnt.uint32 align(1),
|
||||||
|
achVendID: sfnt.Tag align(1),
|
||||||
|
fsSelection: FSSelection align(1),
|
||||||
|
usFirstCharIndex: sfnt.uint16 align(1),
|
||||||
|
usLastCharIndex: sfnt.uint16 align(1),
|
||||||
|
sTypoAscender: sfnt.FWORD align(1),
|
||||||
|
sTypoDescender: sfnt.FWORD align(1),
|
||||||
|
sTypoLineGap: sfnt.FWORD align(1),
|
||||||
|
usWinAscent: sfnt.UFWORD align(1),
|
||||||
|
usWinDescent: sfnt.UFWORD align(1),
|
||||||
|
ulCodePageRange1: sfnt.uint32 align(1),
|
||||||
|
ulCodePageRange2: sfnt.uint32 align(1),
|
||||||
|
sxHeight: sfnt.FWORD align(1),
|
||||||
|
sCapHeight: sfnt.FWORD align(1),
|
||||||
|
usDefaultChar: sfnt.uint16 align(1),
|
||||||
|
usBreakChar: sfnt.uint16 align(1),
|
||||||
|
usMaxContext: sfnt.uint16 align(1),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const OS2v1 = extern struct {
|
||||||
|
version: sfnt.uint16 align(1),
|
||||||
|
xAvgCharWidth: sfnt.FWORD align(1),
|
||||||
|
usWeightClass: sfnt.uint16 align(1),
|
||||||
|
usWidthClass: sfnt.uint16 align(1),
|
||||||
|
fsType: sfnt.uint16 align(1),
|
||||||
|
ySubscriptXSize: sfnt.FWORD align(1),
|
||||||
|
ySubscriptYSize: sfnt.FWORD align(1),
|
||||||
|
ySubscriptXOffset: sfnt.FWORD align(1),
|
||||||
|
ySubscriptYOffset: sfnt.FWORD align(1),
|
||||||
|
ySuperscriptXSize: sfnt.FWORD align(1),
|
||||||
|
ySuperscriptYSize: sfnt.FWORD align(1),
|
||||||
|
ySuperscriptXOffset: sfnt.FWORD align(1),
|
||||||
|
ySuperscriptYOffset: sfnt.FWORD align(1),
|
||||||
|
yStrikeoutSize: sfnt.FWORD align(1),
|
||||||
|
yStrikeoutPosition: sfnt.FWORD align(1),
|
||||||
|
sFamilyClass: sfnt.int16 align(1),
|
||||||
|
panose: [10]sfnt.uint8 align(1),
|
||||||
|
ulUnicodeRange1: sfnt.uint32 align(1),
|
||||||
|
ulUnicodeRange2: sfnt.uint32 align(1),
|
||||||
|
ulUnicodeRange3: sfnt.uint32 align(1),
|
||||||
|
ulUnicodeRange4: sfnt.uint32 align(1),
|
||||||
|
achVendID: sfnt.Tag align(1),
|
||||||
|
fsSelection: FSSelection align(1),
|
||||||
|
usFirstCharIndex: sfnt.uint16 align(1),
|
||||||
|
usLastCharIndex: sfnt.uint16 align(1),
|
||||||
|
sTypoAscender: sfnt.FWORD align(1),
|
||||||
|
sTypoDescender: sfnt.FWORD align(1),
|
||||||
|
sTypoLineGap: sfnt.FWORD align(1),
|
||||||
|
usWinAscent: sfnt.UFWORD align(1),
|
||||||
|
usWinDescent: sfnt.UFWORD align(1),
|
||||||
|
ulCodePageRange1: sfnt.uint32 align(1),
|
||||||
|
ulCodePageRange2: sfnt.uint32 align(1),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const OS2v0 = extern struct {
|
||||||
|
version: sfnt.uint16 align(1),
|
||||||
|
xAvgCharWidth: sfnt.FWORD align(1),
|
||||||
|
usWeightClass: sfnt.uint16 align(1),
|
||||||
|
usWidthClass: sfnt.uint16 align(1),
|
||||||
|
fsType: sfnt.uint16 align(1),
|
||||||
|
ySubscriptXSize: sfnt.FWORD align(1),
|
||||||
|
ySubscriptYSize: sfnt.FWORD align(1),
|
||||||
|
ySubscriptXOffset: sfnt.FWORD align(1),
|
||||||
|
ySubscriptYOffset: sfnt.FWORD align(1),
|
||||||
|
ySuperscriptXSize: sfnt.FWORD align(1),
|
||||||
|
ySuperscriptYSize: sfnt.FWORD align(1),
|
||||||
|
ySuperscriptXOffset: sfnt.FWORD align(1),
|
||||||
|
ySuperscriptYOffset: sfnt.FWORD align(1),
|
||||||
|
yStrikeoutSize: sfnt.FWORD align(1),
|
||||||
|
yStrikeoutPosition: sfnt.FWORD align(1),
|
||||||
|
sFamilyClass: sfnt.int16 align(1),
|
||||||
|
panose: [10]sfnt.uint8 align(1),
|
||||||
|
ulUnicodeRange1: sfnt.uint32 align(1),
|
||||||
|
ulUnicodeRange2: sfnt.uint32 align(1),
|
||||||
|
ulUnicodeRange3: sfnt.uint32 align(1),
|
||||||
|
ulUnicodeRange4: sfnt.uint32 align(1),
|
||||||
|
achVendID: sfnt.Tag align(1),
|
||||||
|
fsSelection: FSSelection align(1),
|
||||||
|
usFirstCharIndex: sfnt.uint16 align(1),
|
||||||
|
usLastCharIndex: sfnt.uint16 align(1),
|
||||||
|
sTypoAscender: sfnt.FWORD align(1),
|
||||||
|
sTypoDescender: sfnt.FWORD align(1),
|
||||||
|
sTypoLineGap: sfnt.FWORD align(1),
|
||||||
|
usWinAscent: sfnt.UFWORD align(1),
|
||||||
|
usWinDescent: sfnt.UFWORD align(1),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Generic OS/2 table with optional fields
|
||||||
|
/// for those that don't exist in all versions.
|
||||||
|
///
|
||||||
|
/// References:
|
||||||
|
/// - https://learn.microsoft.com/en-us/typography/opentype/spec/os2
|
||||||
|
///
|
||||||
|
/// Field names are in camelCase to match names in spec.
|
||||||
|
pub const OS2 = struct {
|
||||||
|
/// The version number for the OS/2 table: 0x0000 to 0x0005.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2#version
|
||||||
|
version: u16,
|
||||||
|
/// The Average Character Width field specifies the arithmetic average of the escapement (width) of all non-zero width glyphs in the font.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#xavgcharwidth
|
||||||
|
xAvgCharWidth: i16,
|
||||||
|
/// Indicates the visual weight (degree of blackness or thickness of strokes) of the characters in the font. Values from 1 to 1000 are valid.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#usweightclass
|
||||||
|
usWeightClass: u16,
|
||||||
|
/// Indicates a relative change from the normal aspect ratio (width to height ratio) as specified by a font designer for the glyphs in a font.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#uswidthclass
|
||||||
|
usWidthClass: u16,
|
||||||
|
/// Indicates font embedding licensing rights for the font.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#fstype
|
||||||
|
fsType: u16,
|
||||||
|
/// The recommended horizontal size in font design units for subscripts for this font.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ysubscriptxsize
|
||||||
|
ySubscriptXSize: i16,
|
||||||
|
/// The recommended vertical size in font design units for subscripts for this font.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ysubscriptysize
|
||||||
|
ySubscriptYSize: i16,
|
||||||
|
/// The recommended horizontal offset in font design units for subscripts for this font.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ysubscriptxoffset
|
||||||
|
ySubscriptXOffset: i16,
|
||||||
|
/// The recommended vertical offset in font design units from the baseline for subscripts for this font.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ysubscriptyoffset
|
||||||
|
ySubscriptYOffset: i16,
|
||||||
|
/// The recommended horizontal size in font design units for superscripts for this font.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ysuperscriptxsize
|
||||||
|
ySuperscriptXSize: i16,
|
||||||
|
/// The recommended vertical size in font design units for superscripts for this font.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ysuperscriptysize
|
||||||
|
ySuperscriptYSize: i16,
|
||||||
|
/// The recommended horizontal offset in font design units for superscripts for this font.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ysuperscriptxoffset
|
||||||
|
ySuperscriptXOffset: i16,
|
||||||
|
/// The recommended vertical offset in font design units from the baseline for superscripts for this font.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ysuperscriptyoffset
|
||||||
|
ySuperscriptYOffset: i16,
|
||||||
|
/// Thickness of the strikeout stroke in font design units. Should be > 0.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ystrikeoutsize
|
||||||
|
yStrikeoutSize: i16,
|
||||||
|
/// The position of the top of the strikeout stroke relative to the baseline in font design units.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ystrikeoutposition
|
||||||
|
yStrikeoutPosition: i16,
|
||||||
|
/// This field provides a classification of font-family design.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#sfamilyclass
|
||||||
|
sFamilyClass: i16,
|
||||||
|
/// This 10-byte array of numbers is used to describe the visual characteristics of a given typeface.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#panose
|
||||||
|
panose: [10]u8,
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ulunicoderange
|
||||||
|
ulUnicodeRange1: u32,
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ulunicoderange
|
||||||
|
ulUnicodeRange2: u32,
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ulunicoderange
|
||||||
|
ulUnicodeRange3: u32,
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ulunicoderange
|
||||||
|
ulUnicodeRange4: u32,
|
||||||
|
/// The four character identifier for the vendor of the given type face.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#achvendid
|
||||||
|
achVendID: [4]u8,
|
||||||
|
/// Contains information concerning the nature of the font patterns.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#fsselection
|
||||||
|
fsSelection: FSSelection,
|
||||||
|
/// The minimum Unicode index (character code) in this font, according to the 'cmap' subtable for platform ID 3 and platform-specific encoding ID 0 or 1.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#usfirstcharindex
|
||||||
|
usFirstCharIndex: u16,
|
||||||
|
/// The maximum Unicode index (character code) in this font, according to the 'cmap' subtable for platform ID 3 and encoding ID 0 or 1.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#uslastcharindex
|
||||||
|
usLastCharIndex: u16,
|
||||||
|
/// The typographic ascender for this font. This field should be combined with the sTypoDescender and sTypoLineGap values to determine default line spacing.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#stypoascender
|
||||||
|
sTypoAscender: i16,
|
||||||
|
/// The typographic descender for this font. This field should be combined with the sTypoAscender and sTypoLineGap values to determine default line spacing.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#stypodescender
|
||||||
|
sTypoDescender: i16,
|
||||||
|
/// The typographic line gap for this font. This field should be combined with the sTypoAscender and sTypoDescender values to determine default line spacing.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#stypolinegap
|
||||||
|
sTypoLineGap: i16,
|
||||||
|
/// The “Windows ascender” metric. This should be used to specify the height above the baseline for a clipping region.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#uswinascent
|
||||||
|
usWinAscent: u16,
|
||||||
|
/// The “Windows descender” metric. This should be used to specify the vertical extent below the baseline for a clipping region.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#uswindescent
|
||||||
|
usWinDescent: u16,
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ulcodepagerange
|
||||||
|
ulCodePageRange1: ?u32 = null,
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ulcodepagerange
|
||||||
|
ulCodePageRange2: ?u32 = null,
|
||||||
|
/// This metric specifies the distance between the baseline and the approximate height of non-ascending lowercase letters measured in font design units.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#sxheight
|
||||||
|
sxHeight: ?i16 = null,
|
||||||
|
/// This metric specifies the distance between the baseline and the approximate height of uppercase letters measured in font design units.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#scapheight
|
||||||
|
sCapHeight: ?i16 = null,
|
||||||
|
/// This is the Unicode code point, in UTF-16 encoding, of a character that can be used for a default glyph if a requested character is not supported in the font.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#usdefaultchar
|
||||||
|
usDefaultChar: ?u16 = null,
|
||||||
|
/// This is the Unicode code point, in UTF-16 encoding, of a character that can be used as a default break character.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#usbreakchar
|
||||||
|
usBreakChar: ?u16 = null,
|
||||||
|
/// The maximum length of a target glyph context for any feature in this font. For example, a font which has only a pair kerning feature should set this field to 2.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#usmaxcontext
|
||||||
|
usMaxContext: ?u16 = null,
|
||||||
|
/// This field is used for fonts with multiple optical styles.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#usloweropticalpointsize
|
||||||
|
usLowerOpticalPointSize: ?u16 = null,
|
||||||
|
/// This field is used for fonts with multiple optical styles.
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#usupperopticalpointsize
|
||||||
|
usUpperOpticalPointSize: ?u16 = null,
|
||||||
|
|
||||||
|
/// Parse the table from raw data.
|
||||||
|
pub fn init(data: []const u8) error{
|
||||||
|
EndOfStream,
|
||||||
|
OS2VersionNotSupported,
|
||||||
|
}!OS2 {
|
||||||
|
var fbs = std.io.fixedBufferStream(data);
|
||||||
|
const reader = fbs.reader();
|
||||||
|
|
||||||
|
const version = try reader.readInt(sfnt.uint16, .big);
|
||||||
|
|
||||||
|
// Return to the start, cause the version is part of the struct.
|
||||||
|
try fbs.seekTo(0);
|
||||||
|
|
||||||
|
switch (version) {
|
||||||
|
5 => {
|
||||||
|
const table = try reader.readStructEndian(OS2v5, .big);
|
||||||
|
return .{
|
||||||
|
.version = table.version,
|
||||||
|
.xAvgCharWidth = table.xAvgCharWidth,
|
||||||
|
.usWeightClass = table.usWeightClass,
|
||||||
|
.usWidthClass = table.usWidthClass,
|
||||||
|
.fsType = table.fsType,
|
||||||
|
.ySubscriptXSize = table.ySubscriptXSize,
|
||||||
|
.ySubscriptYSize = table.ySubscriptYSize,
|
||||||
|
.ySubscriptXOffset = table.ySubscriptXOffset,
|
||||||
|
.ySubscriptYOffset = table.ySubscriptYOffset,
|
||||||
|
.ySuperscriptXSize = table.ySuperscriptXSize,
|
||||||
|
.ySuperscriptYSize = table.ySuperscriptYSize,
|
||||||
|
.ySuperscriptXOffset = table.ySuperscriptXOffset,
|
||||||
|
.ySuperscriptYOffset = table.ySuperscriptYOffset,
|
||||||
|
.yStrikeoutSize = table.yStrikeoutSize,
|
||||||
|
.yStrikeoutPosition = table.yStrikeoutPosition,
|
||||||
|
.sFamilyClass = table.sFamilyClass,
|
||||||
|
.panose = table.panose,
|
||||||
|
.ulUnicodeRange1 = table.ulUnicodeRange1,
|
||||||
|
.ulUnicodeRange2 = table.ulUnicodeRange2,
|
||||||
|
.ulUnicodeRange3 = table.ulUnicodeRange3,
|
||||||
|
.ulUnicodeRange4 = table.ulUnicodeRange4,
|
||||||
|
.achVendID = table.achVendID,
|
||||||
|
.fsSelection = table.fsSelection,
|
||||||
|
.usFirstCharIndex = table.usFirstCharIndex,
|
||||||
|
.usLastCharIndex = table.usLastCharIndex,
|
||||||
|
.sTypoAscender = table.sTypoAscender,
|
||||||
|
.sTypoDescender = table.sTypoDescender,
|
||||||
|
.sTypoLineGap = table.sTypoLineGap,
|
||||||
|
.usWinAscent = table.usWinAscent,
|
||||||
|
.usWinDescent = table.usWinDescent,
|
||||||
|
.ulCodePageRange1 = table.ulCodePageRange1,
|
||||||
|
.ulCodePageRange2 = table.ulCodePageRange2,
|
||||||
|
.sxHeight = table.sxHeight,
|
||||||
|
.sCapHeight = table.sCapHeight,
|
||||||
|
.usDefaultChar = table.usDefaultChar,
|
||||||
|
.usBreakChar = table.usBreakChar,
|
||||||
|
.usMaxContext = table.usMaxContext,
|
||||||
|
.usLowerOpticalPointSize = table.usLowerOpticalPointSize,
|
||||||
|
.usUpperOpticalPointSize = table.usUpperOpticalPointSize,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
4, 3, 2 => {
|
||||||
|
const table = try reader.readStructEndian(OS2v4_3_2, .big);
|
||||||
|
return .{
|
||||||
|
.version = table.version,
|
||||||
|
.xAvgCharWidth = table.xAvgCharWidth,
|
||||||
|
.usWeightClass = table.usWeightClass,
|
||||||
|
.usWidthClass = table.usWidthClass,
|
||||||
|
.fsType = table.fsType,
|
||||||
|
.ySubscriptXSize = table.ySubscriptXSize,
|
||||||
|
.ySubscriptYSize = table.ySubscriptYSize,
|
||||||
|
.ySubscriptXOffset = table.ySubscriptXOffset,
|
||||||
|
.ySubscriptYOffset = table.ySubscriptYOffset,
|
||||||
|
.ySuperscriptXSize = table.ySuperscriptXSize,
|
||||||
|
.ySuperscriptYSize = table.ySuperscriptYSize,
|
||||||
|
.ySuperscriptXOffset = table.ySuperscriptXOffset,
|
||||||
|
.ySuperscriptYOffset = table.ySuperscriptYOffset,
|
||||||
|
.yStrikeoutSize = table.yStrikeoutSize,
|
||||||
|
.yStrikeoutPosition = table.yStrikeoutPosition,
|
||||||
|
.sFamilyClass = table.sFamilyClass,
|
||||||
|
.panose = table.panose,
|
||||||
|
.ulUnicodeRange1 = table.ulUnicodeRange1,
|
||||||
|
.ulUnicodeRange2 = table.ulUnicodeRange2,
|
||||||
|
.ulUnicodeRange3 = table.ulUnicodeRange3,
|
||||||
|
.ulUnicodeRange4 = table.ulUnicodeRange4,
|
||||||
|
.achVendID = table.achVendID,
|
||||||
|
.fsSelection = table.fsSelection,
|
||||||
|
.usFirstCharIndex = table.usFirstCharIndex,
|
||||||
|
.usLastCharIndex = table.usLastCharIndex,
|
||||||
|
.sTypoAscender = table.sTypoAscender,
|
||||||
|
.sTypoDescender = table.sTypoDescender,
|
||||||
|
.sTypoLineGap = table.sTypoLineGap,
|
||||||
|
.usWinAscent = table.usWinAscent,
|
||||||
|
.usWinDescent = table.usWinDescent,
|
||||||
|
.ulCodePageRange1 = table.ulCodePageRange1,
|
||||||
|
.ulCodePageRange2 = table.ulCodePageRange2,
|
||||||
|
.sxHeight = table.sxHeight,
|
||||||
|
.sCapHeight = table.sCapHeight,
|
||||||
|
.usDefaultChar = table.usDefaultChar,
|
||||||
|
.usBreakChar = table.usBreakChar,
|
||||||
|
.usMaxContext = table.usMaxContext,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
1 => {
|
||||||
|
const table = try reader.readStructEndian(OS2v1, .big);
|
||||||
|
return .{
|
||||||
|
.version = table.version,
|
||||||
|
.xAvgCharWidth = table.xAvgCharWidth,
|
||||||
|
.usWeightClass = table.usWeightClass,
|
||||||
|
.usWidthClass = table.usWidthClass,
|
||||||
|
.fsType = table.fsType,
|
||||||
|
.ySubscriptXSize = table.ySubscriptXSize,
|
||||||
|
.ySubscriptYSize = table.ySubscriptYSize,
|
||||||
|
.ySubscriptXOffset = table.ySubscriptXOffset,
|
||||||
|
.ySubscriptYOffset = table.ySubscriptYOffset,
|
||||||
|
.ySuperscriptXSize = table.ySuperscriptXSize,
|
||||||
|
.ySuperscriptYSize = table.ySuperscriptYSize,
|
||||||
|
.ySuperscriptXOffset = table.ySuperscriptXOffset,
|
||||||
|
.ySuperscriptYOffset = table.ySuperscriptYOffset,
|
||||||
|
.yStrikeoutSize = table.yStrikeoutSize,
|
||||||
|
.yStrikeoutPosition = table.yStrikeoutPosition,
|
||||||
|
.sFamilyClass = table.sFamilyClass,
|
||||||
|
.panose = table.panose,
|
||||||
|
.ulUnicodeRange1 = table.ulUnicodeRange1,
|
||||||
|
.ulUnicodeRange2 = table.ulUnicodeRange2,
|
||||||
|
.ulUnicodeRange3 = table.ulUnicodeRange3,
|
||||||
|
.ulUnicodeRange4 = table.ulUnicodeRange4,
|
||||||
|
.achVendID = table.achVendID,
|
||||||
|
.fsSelection = table.fsSelection,
|
||||||
|
.usFirstCharIndex = table.usFirstCharIndex,
|
||||||
|
.usLastCharIndex = table.usLastCharIndex,
|
||||||
|
.sTypoAscender = table.sTypoAscender,
|
||||||
|
.sTypoDescender = table.sTypoDescender,
|
||||||
|
.sTypoLineGap = table.sTypoLineGap,
|
||||||
|
.usWinAscent = table.usWinAscent,
|
||||||
|
.usWinDescent = table.usWinDescent,
|
||||||
|
.ulCodePageRange1 = table.ulCodePageRange1,
|
||||||
|
.ulCodePageRange2 = table.ulCodePageRange2,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
0 => {
|
||||||
|
const table = try reader.readStructEndian(OS2v0, .big);
|
||||||
|
return .{
|
||||||
|
.version = table.version,
|
||||||
|
.xAvgCharWidth = table.xAvgCharWidth,
|
||||||
|
.usWeightClass = table.usWeightClass,
|
||||||
|
.usWidthClass = table.usWidthClass,
|
||||||
|
.fsType = table.fsType,
|
||||||
|
.ySubscriptXSize = table.ySubscriptXSize,
|
||||||
|
.ySubscriptYSize = table.ySubscriptYSize,
|
||||||
|
.ySubscriptXOffset = table.ySubscriptXOffset,
|
||||||
|
.ySubscriptYOffset = table.ySubscriptYOffset,
|
||||||
|
.ySuperscriptXSize = table.ySuperscriptXSize,
|
||||||
|
.ySuperscriptYSize = table.ySuperscriptYSize,
|
||||||
|
.ySuperscriptXOffset = table.ySuperscriptXOffset,
|
||||||
|
.ySuperscriptYOffset = table.ySuperscriptYOffset,
|
||||||
|
.yStrikeoutSize = table.yStrikeoutSize,
|
||||||
|
.yStrikeoutPosition = table.yStrikeoutPosition,
|
||||||
|
.sFamilyClass = table.sFamilyClass,
|
||||||
|
.panose = table.panose,
|
||||||
|
.ulUnicodeRange1 = table.ulUnicodeRange1,
|
||||||
|
.ulUnicodeRange2 = table.ulUnicodeRange2,
|
||||||
|
.ulUnicodeRange3 = table.ulUnicodeRange3,
|
||||||
|
.ulUnicodeRange4 = table.ulUnicodeRange4,
|
||||||
|
.achVendID = table.achVendID,
|
||||||
|
.fsSelection = table.fsSelection,
|
||||||
|
.usFirstCharIndex = table.usFirstCharIndex,
|
||||||
|
.usLastCharIndex = table.usLastCharIndex,
|
||||||
|
.sTypoAscender = table.sTypoAscender,
|
||||||
|
.sTypoDescender = table.sTypoDescender,
|
||||||
|
.sTypoLineGap = table.sTypoLineGap,
|
||||||
|
.usWinAscent = table.usWinAscent,
|
||||||
|
.usWinDescent = table.usWinDescent,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
else => return error.OS2VersionNotSupported,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test "OS/2" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
const test_font = @import("../embedded.zig").julia_mono;
|
||||||
|
|
||||||
|
const font = try sfnt.SFNT.init(test_font, alloc);
|
||||||
|
defer font.deinit(alloc);
|
||||||
|
|
||||||
|
const table = font.getTable("OS/2").?;
|
||||||
|
|
||||||
|
const os2 = try OS2.init(table);
|
||||||
|
|
||||||
|
try testing.expectEqualDeep(OS2{
|
||||||
|
.version = 4,
|
||||||
|
.xAvgCharWidth = 1200,
|
||||||
|
.usWeightClass = 400,
|
||||||
|
.usWidthClass = 5,
|
||||||
|
.fsType = 0,
|
||||||
|
.ySubscriptXSize = 1300,
|
||||||
|
.ySubscriptYSize = 1200,
|
||||||
|
.ySubscriptXOffset = 0,
|
||||||
|
.ySubscriptYOffset = 150,
|
||||||
|
.ySuperscriptXSize = 1300,
|
||||||
|
.ySuperscriptYSize = 1200,
|
||||||
|
.ySuperscriptXOffset = 0,
|
||||||
|
.ySuperscriptYOffset = 700,
|
||||||
|
.yStrikeoutSize = 100,
|
||||||
|
.yStrikeoutPosition = 550,
|
||||||
|
.sFamilyClass = 0,
|
||||||
|
.panose = .{ 2, 11, 6, 9, 6, 3, 0, 2, 0, 4 },
|
||||||
|
.ulUnicodeRange1 = 3843162111,
|
||||||
|
.ulUnicodeRange2 = 3603300351,
|
||||||
|
.ulUnicodeRange3 = 117760229,
|
||||||
|
.ulUnicodeRange4 = 96510060,
|
||||||
|
.achVendID = "corm".*,
|
||||||
|
.fsSelection = .{
|
||||||
|
.regular = true,
|
||||||
|
.use_typo_metrics = true,
|
||||||
|
},
|
||||||
|
.usFirstCharIndex = 13,
|
||||||
|
.usLastCharIndex = 65535,
|
||||||
|
.sTypoAscender = 1900,
|
||||||
|
.sTypoDescender = -450,
|
||||||
|
.sTypoLineGap = 0,
|
||||||
|
.usWinAscent = 2400,
|
||||||
|
.usWinDescent = 450,
|
||||||
|
.ulCodePageRange1 = 1613234687,
|
||||||
|
.ulCodePageRange2 = 0,
|
||||||
|
.sxHeight = 1100,
|
||||||
|
.sCapHeight = 1450,
|
||||||
|
.usDefaultChar = 0,
|
||||||
|
.usBreakChar = 32,
|
||||||
|
.usMaxContext = 126,
|
||||||
|
.usLowerOpticalPointSize = null,
|
||||||
|
.usUpperOpticalPointSize = null,
|
||||||
|
}, os2);
|
||||||
|
}
|
83
src/font/opentype/post.zig
Normal file
83
src/font/opentype/post.zig
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const sfnt = @import("sfnt.zig");
|
||||||
|
|
||||||
|
/// PostScript Table
|
||||||
|
///
|
||||||
|
/// This implementation doesn't parse the
|
||||||
|
/// extra fields in versions 2.0 and 2.5.
|
||||||
|
///
|
||||||
|
/// References:
|
||||||
|
/// - https://learn.microsoft.com/en-us/typography/opentype/spec/post
|
||||||
|
///
|
||||||
|
/// Field names are in camelCase to match names in spec.
|
||||||
|
pub const Post = extern struct {
|
||||||
|
version: sfnt.Version16Dot16 align(1),
|
||||||
|
|
||||||
|
/// Italic angle in counter-clockwise degrees from the vertical.
|
||||||
|
/// Zero for upright text, negative for text that leans to the
|
||||||
|
/// right (forward).
|
||||||
|
italicAngle: sfnt.Fixed align(1),
|
||||||
|
|
||||||
|
/// Suggested y-coordinate of the top of the underline.
|
||||||
|
underlinePosition: sfnt.FWORD align(1),
|
||||||
|
|
||||||
|
/// Suggested values for the underline thickness.
|
||||||
|
/// In general, the underline thickness should match the thickness of
|
||||||
|
/// the underscore character (U+005F LOW LINE), and should also match
|
||||||
|
/// the strikeout thickness, which is specified in the OS/2 table.
|
||||||
|
underlineThickness: sfnt.FWORD align(1),
|
||||||
|
|
||||||
|
/// Set to 0 if the font is proportionally spaced, non-zero if
|
||||||
|
/// the font is not proportionally spaced (i.e. monospaced).
|
||||||
|
isFixedPitch: sfnt.uint32 align(1),
|
||||||
|
|
||||||
|
/// Minimum memory usage when an OpenType font is downloaded.
|
||||||
|
minMemType42: sfnt.uint32 align(1),
|
||||||
|
|
||||||
|
/// Maximum memory usage when an OpenType font is downloaded.
|
||||||
|
maxMemType42: sfnt.uint32 align(1),
|
||||||
|
|
||||||
|
/// Minimum memory usage when an OpenType
|
||||||
|
/// font is downloaded as a Type 1 font.
|
||||||
|
minMemType1: sfnt.uint32 align(1),
|
||||||
|
|
||||||
|
/// Maximum memory usage when an OpenType
|
||||||
|
/// font is downloaded as a Type 1 font.
|
||||||
|
maxMemType1: sfnt.uint32 align(1),
|
||||||
|
|
||||||
|
/// Parse the table from raw data.
|
||||||
|
pub fn init(data: []const u8) error{EndOfStream}!Post {
|
||||||
|
var fbs = std.io.fixedBufferStream(data);
|
||||||
|
const reader = fbs.reader();
|
||||||
|
return try reader.readStructEndian(Post, .big);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test "post" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
const test_font = @import("../embedded.zig").julia_mono;
|
||||||
|
|
||||||
|
const font = try sfnt.SFNT.init(test_font, alloc);
|
||||||
|
defer font.deinit(alloc);
|
||||||
|
|
||||||
|
const table = font.getTable("post").?;
|
||||||
|
|
||||||
|
const post = try Post.init(table);
|
||||||
|
|
||||||
|
try testing.expectEqualDeep(
|
||||||
|
Post{
|
||||||
|
.version = sfnt.Version16Dot16{ .minor = 0, .major = 2 },
|
||||||
|
.italicAngle = sfnt.Fixed.from(0.0),
|
||||||
|
.underlinePosition = -200,
|
||||||
|
.underlineThickness = 100,
|
||||||
|
.isFixedPitch = 1,
|
||||||
|
.minMemType42 = 0,
|
||||||
|
.maxMemType42 = 0,
|
||||||
|
.minMemType1 = 0,
|
||||||
|
.maxMemType1 = 0,
|
||||||
|
},
|
||||||
|
post,
|
||||||
|
);
|
||||||
|
}
|
314
src/font/opentype/sfnt.zig
Normal file
314
src/font/opentype/sfnt.zig
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
/// 8-bit unsigned integer.
|
||||||
|
pub const uint8 = u8;
|
||||||
|
|
||||||
|
/// 8-bit signed integer.
|
||||||
|
pub const int8 = i8;
|
||||||
|
|
||||||
|
/// 16-bit unsigned integer.
|
||||||
|
pub const uint16 = u16;
|
||||||
|
|
||||||
|
/// 16-bit signed integer.
|
||||||
|
pub const int16 = i16;
|
||||||
|
|
||||||
|
/// 24-bit unsigned integer.
|
||||||
|
pub const uint24 = u24;
|
||||||
|
|
||||||
|
/// 32-bit unsigned integer.
|
||||||
|
pub const uint32 = u32;
|
||||||
|
|
||||||
|
/// 32-bit signed integer.
|
||||||
|
pub const int32 = i32;
|
||||||
|
|
||||||
|
/// 32-bit signed fixed-point number (16.16)
|
||||||
|
pub const Fixed = FixedPoint(i32, 16, 16);
|
||||||
|
|
||||||
|
/// int16 that describes a quantity in font design units.
|
||||||
|
pub const FWORD = i16;
|
||||||
|
|
||||||
|
/// uint16 that describes a quantity in font design units.
|
||||||
|
pub const UFWORD = u16;
|
||||||
|
|
||||||
|
/// 16-bit signed fixed number with the low 14 bits of fraction (2.14).
|
||||||
|
pub const F2DOT14 = FixedPoint(i16, 2, 14);
|
||||||
|
|
||||||
|
/// Date and time represented in number of seconds since 12:00 midnight, January 1, 1904, UTC. The value is represented as a signed 64-bit integer.
|
||||||
|
pub const LONGDATETIME = i64;
|
||||||
|
|
||||||
|
/// Array of four uint8s (length = 32 bits) used to identify a table,
|
||||||
|
/// design-variation axis, script, language system, feature, or baseline.
|
||||||
|
pub const Tag = [4]u8;
|
||||||
|
|
||||||
|
/// 8-bit offset to a table, same as uint8, NULL offset = 0x00
|
||||||
|
pub const Offset8 = u8;
|
||||||
|
|
||||||
|
/// Short offset to a table, same as uint16, NULL offset = 0x0000
|
||||||
|
pub const Offset16 = u16;
|
||||||
|
|
||||||
|
/// 24-bit offset to a table, same as uint24, NULL offset = 0x000000
|
||||||
|
pub const Offset24 = u24;
|
||||||
|
|
||||||
|
/// Long offset to a table, same as uint32, NULL offset = 0x00000000
|
||||||
|
pub const Offset32 = u32;
|
||||||
|
|
||||||
|
/// Packed 32-bit value with major and minor version numbers
|
||||||
|
pub const Version16Dot16 = packed struct(u32) {
|
||||||
|
minor: u16,
|
||||||
|
major: u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 32-bit signed 26.6 fixed point numbers.
|
||||||
|
pub const F26Dot6 = FixedPoint(i32, 26, 6);
|
||||||
|
|
||||||
|
fn FixedPoint(comptime T: type, int_bits: u64, frac_bits: u64) type {
|
||||||
|
const type_info: std.builtin.Type.Int = @typeInfo(T).Int;
|
||||||
|
comptime assert(int_bits + frac_bits == type_info.bits);
|
||||||
|
|
||||||
|
return packed struct(T) {
|
||||||
|
const Self = FixedPoint(T, int_bits, frac_bits);
|
||||||
|
const frac_factor: comptime_float = @floatFromInt(std.math.pow(
|
||||||
|
u64,
|
||||||
|
2,
|
||||||
|
frac_bits,
|
||||||
|
));
|
||||||
|
const half = @as(T, 1) << @intCast(frac_bits - 1);
|
||||||
|
|
||||||
|
frac: std.meta.Int(.unsigned, frac_bits),
|
||||||
|
int: std.meta.Int(type_info.signedness, int_bits),
|
||||||
|
|
||||||
|
pub fn to(self: Self, comptime FloatType: type) FloatType {
|
||||||
|
const i: FloatType = @floatFromInt(self.int);
|
||||||
|
const f: FloatType = @floatFromInt(self.frac);
|
||||||
|
|
||||||
|
return i + f / frac_factor;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from(float: anytype) Self {
|
||||||
|
const int = @floor(float);
|
||||||
|
const frac = @abs(float - int);
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.int = @intFromFloat(int),
|
||||||
|
.frac = @intFromFloat(@round(frac * frac_factor)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Round to the nearest integer, .5 rounds away from 0.
|
||||||
|
pub fn round(self: Self) T {
|
||||||
|
if (self.frac & half != 0)
|
||||||
|
return self.int + 1
|
||||||
|
else
|
||||||
|
return self.int;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format(
|
||||||
|
self: Self,
|
||||||
|
comptime fmt: []const u8,
|
||||||
|
options: std.fmt.FormatOptions,
|
||||||
|
writer: anytype,
|
||||||
|
) !void {
|
||||||
|
_ = fmt;
|
||||||
|
_ = options;
|
||||||
|
|
||||||
|
try writer.print("{d}", .{self.to(f64)});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test FixedPoint {
|
||||||
|
const testing = std.testing;
|
||||||
|
|
||||||
|
const p26d6 = F26Dot6.from(26.6);
|
||||||
|
try testing.expectEqual(F26Dot6{
|
||||||
|
.int = 26,
|
||||||
|
.frac = 38,
|
||||||
|
}, p26d6);
|
||||||
|
try testing.expectEqual(26.59375, p26d6.to(f64));
|
||||||
|
try testing.expectEqual(27, p26d6.round());
|
||||||
|
|
||||||
|
const n26d6 = F26Dot6.from(-26.6);
|
||||||
|
try testing.expectEqual(F26Dot6{
|
||||||
|
.int = -27,
|
||||||
|
.frac = 26,
|
||||||
|
}, n26d6);
|
||||||
|
try testing.expectEqual(-26.59375, n26d6.to(f64));
|
||||||
|
try testing.expectEqual(-27, n26d6.round());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper for parsing a SFNT font and accessing its tables.
|
||||||
|
///
|
||||||
|
/// References:
|
||||||
|
/// - https://learn.microsoft.com/en-us/typography/opentype/spec/otff
|
||||||
|
/// - https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6.html
|
||||||
|
pub const SFNT = struct {
|
||||||
|
const Directory = struct {
|
||||||
|
offset: OffsetSubtable,
|
||||||
|
records: []TableRecord,
|
||||||
|
|
||||||
|
/// The static (fixed-sized) portion of the table directory
|
||||||
|
///
|
||||||
|
/// This struct matches the memory layout of the TrueType/OpenType
|
||||||
|
/// TableDirectory, but does not include the TableRecord array, since
|
||||||
|
/// that is dynamically sized, so we parse it separately.
|
||||||
|
///
|
||||||
|
/// In the TrueType reference manual this
|
||||||
|
/// is referred to as the "offset subtable".
|
||||||
|
///
|
||||||
|
/// https://learn.microsoft.com/en-us/typography/opentype/spec/otff#table-directory
|
||||||
|
const OffsetSubtable = extern struct {
|
||||||
|
/// Indicates the type of font file we're reading.
|
||||||
|
/// - 0x00_01_00_00 ---- TrueType
|
||||||
|
/// - 0x74_72_75_65 'true' TrueType
|
||||||
|
/// - 0x4F_54_54_4F 'OTTO' OpenType
|
||||||
|
/// - 0x74_79_70_31 'typ1' PostScript
|
||||||
|
sfnt_version: uint32 align(1),
|
||||||
|
/// Number of tables.
|
||||||
|
num_tables: uint16 align(1),
|
||||||
|
/// Maximum power of 2 less than or equal to numTables, times 16 ((2**floor(log2(numTables))) * 16, where “**” is an exponentiation operator).
|
||||||
|
search_range: uint16 align(1),
|
||||||
|
/// Log2 of the maximum power of 2 less than or equal to numTables (log2(searchRange/16), which is equal to floor(log2(numTables))).
|
||||||
|
entry_selector: uint16 align(1),
|
||||||
|
/// numTables times 16, minus searchRange ((numTables * 16) - searchRange).
|
||||||
|
range_shift: uint16 align(1),
|
||||||
|
|
||||||
|
pub fn format(
|
||||||
|
self: OffsetSubtable,
|
||||||
|
comptime fmt: []const u8,
|
||||||
|
options: std.fmt.FormatOptions,
|
||||||
|
writer: anytype,
|
||||||
|
) !void {
|
||||||
|
_ = fmt;
|
||||||
|
_ = options;
|
||||||
|
|
||||||
|
try writer.print(
|
||||||
|
"OffsetSubtable('{s}'){{ .num_tables = {} }}",
|
||||||
|
.{
|
||||||
|
if (self.sfnt_version == 0x00_01_00_00)
|
||||||
|
&@as([10]u8, "0x00010000".*)
|
||||||
|
else
|
||||||
|
&@as([4]u8, @bitCast(
|
||||||
|
std.mem.nativeToBig(u32, self.sfnt_version),
|
||||||
|
)),
|
||||||
|
self.num_tables,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const TableRecord = extern struct {
|
||||||
|
/// Table identifier.
|
||||||
|
tag: Tag align(1),
|
||||||
|
/// Checksum for this table.
|
||||||
|
checksum: uint32 align(1),
|
||||||
|
/// Offset from beginning of font file.
|
||||||
|
offset: Offset32 align(1),
|
||||||
|
/// Length of this table.
|
||||||
|
length: uint32 align(1),
|
||||||
|
|
||||||
|
pub fn format(
|
||||||
|
self: TableRecord,
|
||||||
|
comptime fmt: []const u8,
|
||||||
|
options: std.fmt.FormatOptions,
|
||||||
|
writer: anytype,
|
||||||
|
) !void {
|
||||||
|
_ = fmt;
|
||||||
|
_ = options;
|
||||||
|
|
||||||
|
try writer.print(
|
||||||
|
"TableRecord(\"{s}\"){{ .checksum = {}, .offset = {}, .length = {} }}",
|
||||||
|
.{
|
||||||
|
self.tag,
|
||||||
|
self.checksum,
|
||||||
|
self.offset,
|
||||||
|
self.length,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
directory: Directory,
|
||||||
|
|
||||||
|
data: []const u8,
|
||||||
|
|
||||||
|
/// Parse a font from raw data. The struct will keep a
|
||||||
|
/// reference to `data` and use it for future operations.
|
||||||
|
pub fn init(data: []const u8, alloc: Allocator) !SFNT {
|
||||||
|
var fbs = std.io.fixedBufferStream(data);
|
||||||
|
const reader = fbs.reader();
|
||||||
|
|
||||||
|
// SFNT files use big endian, if our native endian is
|
||||||
|
// not big we'll need to byte swap the values we read.
|
||||||
|
const byte_swap = native_endian != .big;
|
||||||
|
|
||||||
|
var directory: Directory = undefined;
|
||||||
|
|
||||||
|
try reader.readNoEof(std.mem.asBytes(&directory.offset));
|
||||||
|
if (byte_swap) std.mem.byteSwapAllFields(
|
||||||
|
Directory.OffsetSubtable,
|
||||||
|
&directory.offset,
|
||||||
|
);
|
||||||
|
|
||||||
|
directory.records = try alloc.alloc(Directory.TableRecord, directory.offset.num_tables);
|
||||||
|
|
||||||
|
try reader.readNoEof(std.mem.sliceAsBytes(directory.records));
|
||||||
|
if (byte_swap) for (directory.records) |*record| {
|
||||||
|
std.mem.byteSwapAllFields(
|
||||||
|
Directory.TableRecord,
|
||||||
|
record,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.directory = directory,
|
||||||
|
.data = data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: SFNT, alloc: Allocator) void {
|
||||||
|
alloc.free(self.directory.records);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the bytes of the table with the provided tag if present.
|
||||||
|
pub fn getTable(self: SFNT, tag: *const [4]u8) ?[]const u8 {
|
||||||
|
for (self.directory.records) |record| {
|
||||||
|
if (std.mem.eql(u8, tag, &record.tag)) {
|
||||||
|
return self.data[record.offset..][0..record.length];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const native_endian = @import("builtin").target.cpu.arch.endian();
|
||||||
|
|
||||||
|
test "parse font" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
const test_font = @import("../embedded.zig").julia_mono;
|
||||||
|
|
||||||
|
const sfnt = try SFNT.init(&test_font.*, alloc);
|
||||||
|
defer sfnt.deinit(alloc);
|
||||||
|
|
||||||
|
try testing.expectEqual(19, sfnt.directory.offset.num_tables);
|
||||||
|
try testing.expectEqualStrings("prep", &sfnt.directory.records[18].tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "get table" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
const test_font = @import("../embedded.zig").julia_mono;
|
||||||
|
|
||||||
|
const sfnt = try SFNT.init(&test_font.*, alloc);
|
||||||
|
defer sfnt.deinit(alloc);
|
||||||
|
|
||||||
|
const svg = sfnt.getTable("SVG ").?;
|
||||||
|
|
||||||
|
try testing.expectEqual(430, svg.len);
|
||||||
|
}
|
@ -22,7 +22,10 @@ pub const SVG = struct {
|
|||||||
/// All records in the table.
|
/// All records in the table.
|
||||||
records: []const [12]u8,
|
records: []const [12]u8,
|
||||||
|
|
||||||
pub fn init(data: []const u8) !SVG {
|
pub fn init(data: []const u8) error{
|
||||||
|
EndOfStream,
|
||||||
|
SVGVersionNotSupported,
|
||||||
|
}!SVG {
|
||||||
var fbs = std.io.fixedBufferStream(data);
|
var fbs = std.io.fixedBufferStream(data);
|
||||||
const reader = fbs.reader();
|
const reader = fbs.reader();
|
||||||
|
|
||||||
|
@ -27,14 +27,8 @@ const Sprite = @import("../sprite.zig").Sprite;
|
|||||||
|
|
||||||
const log = std.log.scoped(.box_font);
|
const log = std.log.scoped(.box_font);
|
||||||
|
|
||||||
/// The cell width and height because the boxes are fit perfectly
|
/// Grid metrics for the rendering.
|
||||||
/// into a cell so that they all properly connect with zero spacing.
|
metrics: font.Metrics,
|
||||||
width: u32,
|
|
||||||
height: u32,
|
|
||||||
|
|
||||||
/// Base thickness value for lines of the box. This is in pixels. If you
|
|
||||||
/// want to do any DPI scaling, it is expected to be done earlier.
|
|
||||||
thickness: u32,
|
|
||||||
|
|
||||||
/// The thickness of a line.
|
/// The thickness of a line.
|
||||||
const Thickness = enum {
|
const Thickness = enum {
|
||||||
@ -218,8 +212,29 @@ pub fn renderGlyph(
|
|||||||
atlas: *font.Atlas,
|
atlas: *font.Atlas,
|
||||||
cp: u32,
|
cp: u32,
|
||||||
) !font.Glyph {
|
) !font.Glyph {
|
||||||
|
const metrics = self.metrics;
|
||||||
|
|
||||||
|
// Some codepoints (such as a few cursors) should not
|
||||||
|
// grow when the cell height is adjusted to be larger.
|
||||||
|
// And we also will need to adjust the vertical position.
|
||||||
|
const height, const dy = adjust: {
|
||||||
|
const h = metrics.cell_height;
|
||||||
|
if (unadjustedCodepoint(cp)) {
|
||||||
|
if (metrics.original_cell_height) |original| {
|
||||||
|
if (h > original) {
|
||||||
|
break :adjust .{ original, (h - original) / 2 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break :adjust .{ h, 0 };
|
||||||
|
};
|
||||||
|
|
||||||
// Create the canvas we'll use to draw
|
// Create the canvas we'll use to draw
|
||||||
var canvas = try font.sprite.Canvas.init(alloc, self.width, self.height);
|
var canvas = try font.sprite.Canvas.init(
|
||||||
|
alloc,
|
||||||
|
metrics.cell_width,
|
||||||
|
height,
|
||||||
|
);
|
||||||
defer canvas.deinit(alloc);
|
defer canvas.deinit(alloc);
|
||||||
|
|
||||||
// Perform the actual drawing
|
// Perform the actual drawing
|
||||||
@ -231,16 +246,20 @@ pub fn renderGlyph(
|
|||||||
// Our coordinates start at the BOTTOM for our renderers so we have to
|
// Our coordinates start at the BOTTOM for our renderers so we have to
|
||||||
// specify an offset of the full height because we rendered a full size
|
// specify an offset of the full height because we rendered a full size
|
||||||
// cell.
|
// cell.
|
||||||
const offset_y = @as(i32, @intCast(self.height));
|
//
|
||||||
|
// If we have an adjustment (see above) to the cell height that we need
|
||||||
|
// to account for, we subtract half the difference (dy) to keep the glyph
|
||||||
|
// centered.
|
||||||
|
const offset_y = @as(i32, @intCast(metrics.cell_height - dy));
|
||||||
|
|
||||||
return font.Glyph{
|
return font.Glyph{
|
||||||
.width = self.width,
|
.width = metrics.cell_width,
|
||||||
.height = self.height,
|
.height = height,
|
||||||
.offset_x = 0,
|
.offset_x = 0,
|
||||||
.offset_y = offset_y,
|
.offset_y = offset_y,
|
||||||
.atlas_x = region.x,
|
.atlas_x = region.x,
|
||||||
.atlas_y = region.y,
|
.atlas_y = region.y,
|
||||||
.advance_x = @floatFromInt(self.width),
|
.advance_x = @floatFromInt(metrics.cell_width),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1652,16 +1671,16 @@ fn draw_lines(
|
|||||||
canvas: *font.sprite.Canvas,
|
canvas: *font.sprite.Canvas,
|
||||||
lines: Lines,
|
lines: Lines,
|
||||||
) void {
|
) void {
|
||||||
const light_px = Thickness.light.height(self.thickness);
|
const light_px = Thickness.light.height(self.metrics.box_thickness);
|
||||||
const heavy_px = Thickness.heavy.height(self.thickness);
|
const heavy_px = Thickness.heavy.height(self.metrics.box_thickness);
|
||||||
|
|
||||||
// Top of light horizontal strokes
|
// Top of light horizontal strokes
|
||||||
const h_light_top = (self.height -| light_px) / 2;
|
const h_light_top = (self.metrics.cell_height -| light_px) / 2;
|
||||||
// Bottom of light horizontal strokes
|
// Bottom of light horizontal strokes
|
||||||
const h_light_bottom = h_light_top +| light_px;
|
const h_light_bottom = h_light_top +| light_px;
|
||||||
|
|
||||||
// Top of heavy horizontal strokes
|
// Top of heavy horizontal strokes
|
||||||
const h_heavy_top = (self.height -| heavy_px) / 2;
|
const h_heavy_top = (self.metrics.cell_height -| heavy_px) / 2;
|
||||||
// Bottom of heavy horizontal strokes
|
// Bottom of heavy horizontal strokes
|
||||||
const h_heavy_bottom = h_heavy_top +| heavy_px;
|
const h_heavy_bottom = h_heavy_top +| heavy_px;
|
||||||
|
|
||||||
@ -1671,12 +1690,12 @@ fn draw_lines(
|
|||||||
const h_double_bottom = h_light_bottom +| light_px;
|
const h_double_bottom = h_light_bottom +| light_px;
|
||||||
|
|
||||||
// Left of light vertical strokes
|
// Left of light vertical strokes
|
||||||
const v_light_left = (self.width -| light_px) / 2;
|
const v_light_left = (self.metrics.cell_width -| light_px) / 2;
|
||||||
// Right of light vertical strokes
|
// Right of light vertical strokes
|
||||||
const v_light_right = v_light_left +| light_px;
|
const v_light_right = v_light_left +| light_px;
|
||||||
|
|
||||||
// Left of heavy vertical strokes
|
// Left of heavy vertical strokes
|
||||||
const v_heavy_left = (self.width -| heavy_px) / 2;
|
const v_heavy_left = (self.metrics.cell_width -| heavy_px) / 2;
|
||||||
// Right of heavy vertical strokes
|
// Right of heavy vertical strokes
|
||||||
const v_heavy_right = v_heavy_left +| heavy_px;
|
const v_heavy_right = v_heavy_left +| heavy_px;
|
||||||
|
|
||||||
@ -1752,27 +1771,27 @@ fn draw_lines(
|
|||||||
|
|
||||||
switch (lines.right) {
|
switch (lines.right) {
|
||||||
.none => {},
|
.none => {},
|
||||||
.light => self.rect(canvas, right_left, h_light_top, self.width, h_light_bottom),
|
.light => self.rect(canvas, right_left, h_light_top, self.metrics.cell_width, h_light_bottom),
|
||||||
.heavy => self.rect(canvas, right_left, h_heavy_top, self.width, h_heavy_bottom),
|
.heavy => self.rect(canvas, right_left, h_heavy_top, self.metrics.cell_width, h_heavy_bottom),
|
||||||
.double => {
|
.double => {
|
||||||
const top_left = if (lines.up == .double) v_light_right else right_left;
|
const top_left = if (lines.up == .double) v_light_right else right_left;
|
||||||
const bottom_left = if (lines.down == .double) v_light_right else right_left;
|
const bottom_left = if (lines.down == .double) v_light_right else right_left;
|
||||||
|
|
||||||
self.rect(canvas, top_left, h_double_top, self.width, h_light_top);
|
self.rect(canvas, top_left, h_double_top, self.metrics.cell_width, h_light_top);
|
||||||
self.rect(canvas, bottom_left, h_light_bottom, self.width, h_double_bottom);
|
self.rect(canvas, bottom_left, h_light_bottom, self.metrics.cell_width, h_double_bottom);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (lines.down) {
|
switch (lines.down) {
|
||||||
.none => {},
|
.none => {},
|
||||||
.light => self.rect(canvas, v_light_left, down_top, v_light_right, self.height),
|
.light => self.rect(canvas, v_light_left, down_top, v_light_right, self.metrics.cell_height),
|
||||||
.heavy => self.rect(canvas, v_heavy_left, down_top, v_heavy_right, self.height),
|
.heavy => self.rect(canvas, v_heavy_left, down_top, v_heavy_right, self.metrics.cell_height),
|
||||||
.double => {
|
.double => {
|
||||||
const left_top = if (lines.left == .double) h_light_bottom else down_top;
|
const left_top = if (lines.left == .double) h_light_bottom else down_top;
|
||||||
const right_top = if (lines.right == .double) h_light_bottom else down_top;
|
const right_top = if (lines.right == .double) h_light_bottom else down_top;
|
||||||
|
|
||||||
self.rect(canvas, v_double_left, left_top, v_light_left, self.height);
|
self.rect(canvas, v_double_left, left_top, v_light_left, self.metrics.cell_height);
|
||||||
self.rect(canvas, v_light_right, right_top, v_double_right, self.height);
|
self.rect(canvas, v_light_right, right_top, v_double_right, self.metrics.cell_height);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1794,8 +1813,8 @@ fn draw_light_triple_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) voi
|
|||||||
self.draw_dash_horizontal(
|
self.draw_dash_horizontal(
|
||||||
canvas,
|
canvas,
|
||||||
3,
|
3,
|
||||||
Thickness.light.height(self.thickness),
|
Thickness.light.height(self.metrics.box_thickness),
|
||||||
@max(4, Thickness.light.height(self.thickness)),
|
@max(4, Thickness.light.height(self.metrics.box_thickness)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1803,8 +1822,8 @@ fn draw_heavy_triple_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) voi
|
|||||||
self.draw_dash_horizontal(
|
self.draw_dash_horizontal(
|
||||||
canvas,
|
canvas,
|
||||||
3,
|
3,
|
||||||
Thickness.heavy.height(self.thickness),
|
Thickness.heavy.height(self.metrics.box_thickness),
|
||||||
@max(4, Thickness.light.height(self.thickness)),
|
@max(4, Thickness.light.height(self.metrics.box_thickness)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1812,8 +1831,8 @@ fn draw_light_triple_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void
|
|||||||
self.draw_dash_vertical(
|
self.draw_dash_vertical(
|
||||||
canvas,
|
canvas,
|
||||||
3,
|
3,
|
||||||
Thickness.light.height(self.thickness),
|
Thickness.light.height(self.metrics.box_thickness),
|
||||||
@max(4, Thickness.light.height(self.thickness)),
|
@max(4, Thickness.light.height(self.metrics.box_thickness)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1821,8 +1840,8 @@ fn draw_heavy_triple_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void
|
|||||||
self.draw_dash_vertical(
|
self.draw_dash_vertical(
|
||||||
canvas,
|
canvas,
|
||||||
3,
|
3,
|
||||||
Thickness.heavy.height(self.thickness),
|
Thickness.heavy.height(self.metrics.box_thickness),
|
||||||
@max(4, Thickness.light.height(self.thickness)),
|
@max(4, Thickness.light.height(self.metrics.box_thickness)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1830,8 +1849,8 @@ fn draw_light_quadruple_dash_horizontal(self: Box, canvas: *font.sprite.Canvas)
|
|||||||
self.draw_dash_horizontal(
|
self.draw_dash_horizontal(
|
||||||
canvas,
|
canvas,
|
||||||
4,
|
4,
|
||||||
Thickness.light.height(self.thickness),
|
Thickness.light.height(self.metrics.box_thickness),
|
||||||
@max(4, Thickness.light.height(self.thickness)),
|
@max(4, Thickness.light.height(self.metrics.box_thickness)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1839,8 +1858,8 @@ fn draw_heavy_quadruple_dash_horizontal(self: Box, canvas: *font.sprite.Canvas)
|
|||||||
self.draw_dash_horizontal(
|
self.draw_dash_horizontal(
|
||||||
canvas,
|
canvas,
|
||||||
4,
|
4,
|
||||||
Thickness.heavy.height(self.thickness),
|
Thickness.heavy.height(self.metrics.box_thickness),
|
||||||
@max(4, Thickness.light.height(self.thickness)),
|
@max(4, Thickness.light.height(self.metrics.box_thickness)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1848,8 +1867,8 @@ fn draw_light_quadruple_dash_vertical(self: Box, canvas: *font.sprite.Canvas) vo
|
|||||||
self.draw_dash_vertical(
|
self.draw_dash_vertical(
|
||||||
canvas,
|
canvas,
|
||||||
4,
|
4,
|
||||||
Thickness.light.height(self.thickness),
|
Thickness.light.height(self.metrics.box_thickness),
|
||||||
@max(4, Thickness.light.height(self.thickness)),
|
@max(4, Thickness.light.height(self.metrics.box_thickness)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1857,8 +1876,8 @@ fn draw_heavy_quadruple_dash_vertical(self: Box, canvas: *font.sprite.Canvas) vo
|
|||||||
self.draw_dash_vertical(
|
self.draw_dash_vertical(
|
||||||
canvas,
|
canvas,
|
||||||
4,
|
4,
|
||||||
Thickness.heavy.height(self.thickness),
|
Thickness.heavy.height(self.metrics.box_thickness),
|
||||||
@max(4, Thickness.light.height(self.thickness)),
|
@max(4, Thickness.light.height(self.metrics.box_thickness)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1866,8 +1885,8 @@ fn draw_light_double_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) voi
|
|||||||
self.draw_dash_horizontal(
|
self.draw_dash_horizontal(
|
||||||
canvas,
|
canvas,
|
||||||
2,
|
2,
|
||||||
Thickness.light.height(self.thickness),
|
Thickness.light.height(self.metrics.box_thickness),
|
||||||
Thickness.light.height(self.thickness),
|
Thickness.light.height(self.metrics.box_thickness),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1875,8 +1894,8 @@ fn draw_heavy_double_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) voi
|
|||||||
self.draw_dash_horizontal(
|
self.draw_dash_horizontal(
|
||||||
canvas,
|
canvas,
|
||||||
2,
|
2,
|
||||||
Thickness.heavy.height(self.thickness),
|
Thickness.heavy.height(self.metrics.box_thickness),
|
||||||
Thickness.heavy.height(self.thickness),
|
Thickness.heavy.height(self.metrics.box_thickness),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1884,8 +1903,8 @@ fn draw_light_double_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void
|
|||||||
self.draw_dash_vertical(
|
self.draw_dash_vertical(
|
||||||
canvas,
|
canvas,
|
||||||
2,
|
2,
|
||||||
Thickness.light.height(self.thickness),
|
Thickness.light.height(self.metrics.box_thickness),
|
||||||
Thickness.heavy.height(self.thickness),
|
Thickness.heavy.height(self.metrics.box_thickness),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1893,26 +1912,26 @@ fn draw_heavy_double_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void
|
|||||||
self.draw_dash_vertical(
|
self.draw_dash_vertical(
|
||||||
canvas,
|
canvas,
|
||||||
2,
|
2,
|
||||||
Thickness.heavy.height(self.thickness),
|
Thickness.heavy.height(self.metrics.box_thickness),
|
||||||
Thickness.heavy.height(self.thickness),
|
Thickness.heavy.height(self.metrics.box_thickness),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_light_diagonal_upper_right_to_lower_left(self: Box, canvas: *font.sprite.Canvas) void {
|
fn draw_light_diagonal_upper_right_to_lower_left(self: Box, canvas: *font.sprite.Canvas) void {
|
||||||
canvas.line(.{
|
canvas.line(.{
|
||||||
.p0 = .{ .x = @floatFromInt(self.width), .y = 0 },
|
.p0 = .{ .x = @floatFromInt(self.metrics.cell_width), .y = 0 },
|
||||||
.p1 = .{ .x = 0, .y = @floatFromInt(self.height) },
|
.p1 = .{ .x = 0, .y = @floatFromInt(self.metrics.cell_height) },
|
||||||
}, @floatFromInt(Thickness.light.height(self.thickness)), .on) catch {};
|
}, @floatFromInt(Thickness.light.height(self.metrics.box_thickness)), .on) catch {};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_light_diagonal_upper_left_to_lower_right(self: Box, canvas: *font.sprite.Canvas) void {
|
fn draw_light_diagonal_upper_left_to_lower_right(self: Box, canvas: *font.sprite.Canvas) void {
|
||||||
canvas.line(.{
|
canvas.line(.{
|
||||||
.p0 = .{ .x = 0, .y = 0 },
|
.p0 = .{ .x = 0, .y = 0 },
|
||||||
.p1 = .{
|
.p1 = .{
|
||||||
.x = @floatFromInt(self.width),
|
.x = @floatFromInt(self.metrics.cell_width),
|
||||||
.y = @floatFromInt(self.height),
|
.y = @floatFromInt(self.metrics.cell_height),
|
||||||
},
|
},
|
||||||
}, @floatFromInt(Thickness.light.height(self.thickness)), .on) catch {};
|
}, @floatFromInt(Thickness.light.height(self.metrics.box_thickness)), .on) catch {};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_light_diagonal_cross(self: Box, canvas: *font.sprite.Canvas) void {
|
fn draw_light_diagonal_cross(self: Box, canvas: *font.sprite.Canvas) void {
|
||||||
@ -1938,21 +1957,21 @@ fn draw_block_shade(
|
|||||||
comptime height: f64,
|
comptime height: f64,
|
||||||
comptime shade: Shade,
|
comptime shade: Shade,
|
||||||
) void {
|
) void {
|
||||||
const float_width: f64 = @floatFromInt(self.width);
|
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
|
||||||
const float_height: f64 = @floatFromInt(self.height);
|
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
|
||||||
|
|
||||||
const w: u32 = @intFromFloat(@round(float_width * width));
|
const w: u32 = @intFromFloat(@round(float_width * width));
|
||||||
const h: u32 = @intFromFloat(@round(float_height * height));
|
const h: u32 = @intFromFloat(@round(float_height * height));
|
||||||
|
|
||||||
const x = switch (alignment.horizontal) {
|
const x = switch (alignment.horizontal) {
|
||||||
.left => 0,
|
.left => 0,
|
||||||
.right => self.width - w,
|
.right => self.metrics.cell_width - w,
|
||||||
.center => (self.width - w) / 2,
|
.center => (self.metrics.cell_width - w) / 2,
|
||||||
};
|
};
|
||||||
const y = switch (alignment.vertical) {
|
const y = switch (alignment.vertical) {
|
||||||
.top => 0,
|
.top => 0,
|
||||||
.bottom => self.height - h,
|
.bottom => self.metrics.cell_height - h,
|
||||||
.middle => (self.height - h) / 2,
|
.middle => (self.metrics.cell_height - h) / 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
canvas.rect(.{
|
canvas.rect(.{
|
||||||
@ -1970,10 +1989,10 @@ fn draw_corner_triangle_shade(
|
|||||||
comptime shade: Shade,
|
comptime shade: Shade,
|
||||||
) void {
|
) void {
|
||||||
const x0, const y0, const x1, const y1, const x2, const y2 = switch (corner) {
|
const x0, const y0, const x1, const y1, const x2, const y2 = switch (corner) {
|
||||||
.tl => .{ 0, 0, 0, self.height, self.width, 0 },
|
.tl => .{ 0, 0, 0, self.metrics.cell_height, self.metrics.cell_width, 0 },
|
||||||
.tr => .{ 0, 0, self.width, self.height, self.width, 0 },
|
.tr => .{ 0, 0, self.metrics.cell_width, self.metrics.cell_height, self.metrics.cell_width, 0 },
|
||||||
.bl => .{ 0, 0, 0, self.height, self.width, self.height },
|
.bl => .{ 0, 0, 0, self.metrics.cell_height, self.metrics.cell_width, self.metrics.cell_height },
|
||||||
.br => .{ 0, self.height, self.width, self.height, self.width, 0 },
|
.br => .{ 0, self.metrics.cell_height, self.metrics.cell_width, self.metrics.cell_height, self.metrics.cell_width, 0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
canvas.triangle(.{
|
canvas.triangle(.{
|
||||||
@ -1984,26 +2003,26 @@ fn draw_corner_triangle_shade(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_full_block(self: Box, canvas: *font.sprite.Canvas) void {
|
fn draw_full_block(self: Box, canvas: *font.sprite.Canvas) void {
|
||||||
self.rect(canvas, 0, 0, self.width, self.height);
|
self.rect(canvas, 0, 0, self.metrics.cell_width, self.metrics.cell_height);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_vertical_one_eighth_block_n(self: Box, canvas: *font.sprite.Canvas, n: u32) void {
|
fn draw_vertical_one_eighth_block_n(self: Box, canvas: *font.sprite.Canvas, n: u32) void {
|
||||||
const x = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(self.width)) / 8)));
|
const x = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(self.metrics.cell_width)) / 8)));
|
||||||
const w = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.width)) / 8)));
|
const w = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 8)));
|
||||||
self.rect(canvas, x, 0, x + w, self.height);
|
self.rect(canvas, x, 0, x + w, self.metrics.cell_height);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_checkerboard_fill(self: Box, canvas: *font.sprite.Canvas, parity: u1) void {
|
fn draw_checkerboard_fill(self: Box, canvas: *font.sprite.Canvas, parity: u1) void {
|
||||||
const float_width: f64 = @floatFromInt(self.width);
|
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
|
||||||
const float_height: f64 = @floatFromInt(self.height);
|
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
|
||||||
const x_size: usize = 4;
|
const x_size: usize = 4;
|
||||||
const y_size: usize = @intFromFloat(@round(4 * (float_height / float_width)));
|
const y_size: usize = @intFromFloat(@round(4 * (float_height / float_width)));
|
||||||
for (0..x_size) |x| {
|
for (0..x_size) |x| {
|
||||||
const x0 = (self.width * x) / x_size;
|
const x0 = (self.metrics.cell_width * x) / x_size;
|
||||||
const x1 = (self.width * (x + 1)) / x_size;
|
const x1 = (self.metrics.cell_width * (x + 1)) / x_size;
|
||||||
for (0..y_size) |y| {
|
for (0..y_size) |y| {
|
||||||
const y0 = (self.height * y) / y_size;
|
const y0 = (self.metrics.cell_height * y) / y_size;
|
||||||
const y1 = (self.height * (y + 1)) / y_size;
|
const y1 = (self.metrics.cell_height * (y + 1)) / y_size;
|
||||||
if ((x + y) % 2 == parity) {
|
if ((x + y) % 2 == parity) {
|
||||||
canvas.rect(.{
|
canvas.rect(.{
|
||||||
.x = @intCast(x0),
|
.x = @intCast(x0),
|
||||||
@ -2017,11 +2036,11 @@ fn draw_checkerboard_fill(self: Box, canvas: *font.sprite.Canvas, parity: u1) vo
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_upper_left_to_lower_right_fill(self: Box, canvas: *font.sprite.Canvas) void {
|
fn draw_upper_left_to_lower_right_fill(self: Box, canvas: *font.sprite.Canvas) void {
|
||||||
const thick_px = Thickness.light.height(self.thickness);
|
const thick_px = Thickness.light.height(self.metrics.box_thickness);
|
||||||
const line_count = self.width / (2 * thick_px);
|
const line_count = self.metrics.cell_width / (2 * thick_px);
|
||||||
|
|
||||||
const float_width: f64 = @floatFromInt(self.width);
|
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
|
||||||
const float_height: f64 = @floatFromInt(self.height);
|
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
|
||||||
const float_thick: f64 = @floatFromInt(thick_px);
|
const float_thick: f64 = @floatFromInt(thick_px);
|
||||||
const stride = @round(float_width / @as(f64, @floatFromInt(line_count)));
|
const stride = @round(float_width / @as(f64, @floatFromInt(line_count)));
|
||||||
|
|
||||||
@ -2037,11 +2056,11 @@ fn draw_upper_left_to_lower_right_fill(self: Box, canvas: *font.sprite.Canvas) v
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_upper_right_to_lower_left_fill(self: Box, canvas: *font.sprite.Canvas) void {
|
fn draw_upper_right_to_lower_left_fill(self: Box, canvas: *font.sprite.Canvas) void {
|
||||||
const thick_px = Thickness.light.height(self.thickness);
|
const thick_px = Thickness.light.height(self.metrics.box_thickness);
|
||||||
const line_count = self.width / (2 * thick_px);
|
const line_count = self.metrics.cell_width / (2 * thick_px);
|
||||||
|
|
||||||
const float_width: f64 = @floatFromInt(self.width);
|
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
|
||||||
const float_height: f64 = @floatFromInt(self.height);
|
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
|
||||||
const float_thick: f64 = @floatFromInt(thick_px);
|
const float_thick: f64 = @floatFromInt(thick_px);
|
||||||
const stride = @round(float_width / @as(f64, @floatFromInt(line_count)));
|
const stride = @round(float_width / @as(f64, @floatFromInt(line_count)));
|
||||||
|
|
||||||
@ -2061,13 +2080,13 @@ fn draw_corner_diagonal_lines(
|
|||||||
canvas: *font.sprite.Canvas,
|
canvas: *font.sprite.Canvas,
|
||||||
comptime corners: Quads,
|
comptime corners: Quads,
|
||||||
) void {
|
) void {
|
||||||
const thick_px = Thickness.light.height(self.thickness);
|
const thick_px = Thickness.light.height(self.metrics.box_thickness);
|
||||||
|
|
||||||
const float_width: f64 = @floatFromInt(self.width);
|
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
|
||||||
const float_height: f64 = @floatFromInt(self.height);
|
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
|
||||||
const float_thick: f64 = @floatFromInt(thick_px);
|
const float_thick: f64 = @floatFromInt(thick_px);
|
||||||
const center_x: f64 = @floatFromInt(self.width / 2 + self.width % 2);
|
const center_x: f64 = @floatFromInt(self.metrics.cell_width / 2 + self.metrics.cell_width % 2);
|
||||||
const center_y: f64 = @floatFromInt(self.height / 2 + self.height % 2);
|
const center_y: f64 = @floatFromInt(self.metrics.cell_height / 2 + self.metrics.cell_height % 2);
|
||||||
|
|
||||||
if (corners.tl) canvas.line(.{
|
if (corners.tl) canvas.line(.{
|
||||||
.p0 = .{ .x = center_x, .y = 0 },
|
.p0 = .{ .x = center_x, .y = 0 },
|
||||||
@ -2096,8 +2115,8 @@ fn draw_cell_diagonal(
|
|||||||
comptime from: Alignment,
|
comptime from: Alignment,
|
||||||
comptime to: Alignment,
|
comptime to: Alignment,
|
||||||
) void {
|
) void {
|
||||||
const float_width: f64 = @floatFromInt(self.width);
|
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
|
||||||
const float_height: f64 = @floatFromInt(self.height);
|
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
|
||||||
|
|
||||||
const x0: f64 = switch (from.horizontal) {
|
const x0: f64 = switch (from.horizontal) {
|
||||||
.left => 0,
|
.left => 0,
|
||||||
@ -2134,16 +2153,16 @@ fn draw_fading_line(
|
|||||||
comptime to: Edge,
|
comptime to: Edge,
|
||||||
comptime thickness: Thickness,
|
comptime thickness: Thickness,
|
||||||
) void {
|
) void {
|
||||||
const thick_px = thickness.height(self.thickness);
|
const thick_px = thickness.height(self.metrics.box_thickness);
|
||||||
const float_width: f64 = @floatFromInt(self.width);
|
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
|
||||||
const float_height: f64 = @floatFromInt(self.height);
|
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
|
||||||
|
|
||||||
// Top of horizontal strokes
|
// Top of horizontal strokes
|
||||||
const h_top = (self.height -| thick_px) / 2;
|
const h_top = (self.metrics.cell_height -| thick_px) / 2;
|
||||||
// Bottom of horizontal strokes
|
// Bottom of horizontal strokes
|
||||||
const h_bottom = h_top +| thick_px;
|
const h_bottom = h_top +| thick_px;
|
||||||
// Left of vertical strokes
|
// Left of vertical strokes
|
||||||
const v_left = (self.width -| thick_px) / 2;
|
const v_left = (self.metrics.cell_width -| thick_px) / 2;
|
||||||
// Right of vertical strokes
|
// Right of vertical strokes
|
||||||
const v_right = v_left +| thick_px;
|
const v_right = v_left +| thick_px;
|
||||||
|
|
||||||
@ -2163,7 +2182,7 @@ fn draw_fading_line(
|
|||||||
|
|
||||||
switch (to) {
|
switch (to) {
|
||||||
.top, .bottom => {
|
.top, .bottom => {
|
||||||
for (0..self.height) |y| {
|
for (0..self.metrics.cell_height) |y| {
|
||||||
for (v_left..v_right) |x| {
|
for (v_left..v_right) |x| {
|
||||||
canvas.pixel(
|
canvas.pixel(
|
||||||
@intCast(x),
|
@intCast(x),
|
||||||
@ -2175,7 +2194,7 @@ fn draw_fading_line(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
.left, .right => {
|
.left, .right => {
|
||||||
for (0..self.width) |x| {
|
for (0..self.metrics.cell_width) |x| {
|
||||||
for (h_top..h_bottom) |y| {
|
for (h_top..h_bottom) |y| {
|
||||||
canvas.pixel(
|
canvas.pixel(
|
||||||
@intCast(x),
|
@intCast(x),
|
||||||
@ -2195,17 +2214,17 @@ fn draw_branch_node(
|
|||||||
node: BranchNode,
|
node: BranchNode,
|
||||||
comptime thickness: Thickness,
|
comptime thickness: Thickness,
|
||||||
) void {
|
) void {
|
||||||
const thick_px = thickness.height(self.thickness);
|
const thick_px = thickness.height(self.metrics.box_thickness);
|
||||||
const float_width: f64 = @floatFromInt(self.width);
|
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
|
||||||
const float_height: f64 = @floatFromInt(self.height);
|
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
|
||||||
const float_thick: f64 = @floatFromInt(thick_px);
|
const float_thick: f64 = @floatFromInt(thick_px);
|
||||||
|
|
||||||
// Top of horizontal strokes
|
// Top of horizontal strokes
|
||||||
const h_top = (self.height -| thick_px) / 2;
|
const h_top = (self.metrics.cell_height -| thick_px) / 2;
|
||||||
// Bottom of horizontal strokes
|
// Bottom of horizontal strokes
|
||||||
const h_bottom = h_top +| thick_px;
|
const h_bottom = h_top +| thick_px;
|
||||||
// Left of vertical strokes
|
// Left of vertical strokes
|
||||||
const v_left = (self.width -| thick_px) / 2;
|
const v_left = (self.metrics.cell_width -| thick_px) / 2;
|
||||||
// Right of vertical strokes
|
// Right of vertical strokes
|
||||||
const v_right = v_left +| thick_px;
|
const v_right = v_left +| thick_px;
|
||||||
|
|
||||||
@ -2240,9 +2259,9 @@ fn draw_branch_node(
|
|||||||
if (node.up)
|
if (node.up)
|
||||||
self.rect(canvas, v_left, 0, v_right, @intFromFloat(@ceil(cy - r)));
|
self.rect(canvas, v_left, 0, v_right, @intFromFloat(@ceil(cy - r)));
|
||||||
if (node.right)
|
if (node.right)
|
||||||
self.rect(canvas, @intFromFloat(@floor(cx + r)), h_top, self.width, h_bottom);
|
self.rect(canvas, @intFromFloat(@floor(cx + r)), h_top, self.metrics.cell_width, h_bottom);
|
||||||
if (node.down)
|
if (node.down)
|
||||||
self.rect(canvas, v_left, @intFromFloat(@floor(cy + r)), v_right, self.height);
|
self.rect(canvas, v_left, @intFromFloat(@floor(cy + r)), v_right, self.metrics.cell_height);
|
||||||
if (node.left)
|
if (node.left)
|
||||||
self.rect(canvas, 0, h_top, @intFromFloat(@ceil(cx - r)), h_bottom);
|
self.rect(canvas, 0, h_top, @intFromFloat(@ceil(cx - r)), h_bottom);
|
||||||
|
|
||||||
@ -2263,8 +2282,8 @@ fn draw_circle(
|
|||||||
comptime position: Alignment,
|
comptime position: Alignment,
|
||||||
comptime filled: bool,
|
comptime filled: bool,
|
||||||
) void {
|
) void {
|
||||||
const float_width: f64 = @floatFromInt(self.width);
|
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
|
||||||
const float_height: f64 = @floatFromInt(self.height);
|
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
|
||||||
|
|
||||||
const x: f64 = switch (position.horizontal) {
|
const x: f64 = switch (position.horizontal) {
|
||||||
.left => 0,
|
.left => 0,
|
||||||
@ -2285,7 +2304,7 @@ fn draw_circle(
|
|||||||
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } },
|
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
.line_width = @floatFromInt(Thickness.light.height(self.thickness)),
|
.line_width = @floatFromInt(Thickness.light.height(self.metrics.box_thickness)),
|
||||||
};
|
};
|
||||||
|
|
||||||
var path = z2d.Path.init(canvas.alloc);
|
var path = z2d.Path.init(canvas.alloc);
|
||||||
@ -2311,7 +2330,7 @@ fn draw_line(
|
|||||||
) !void {
|
) !void {
|
||||||
canvas.line(
|
canvas.line(
|
||||||
.{ .p0 = p0, .p1 = p1 },
|
.{ .p0 = p0, .p1 = p1 },
|
||||||
@floatFromInt(thickness.height(self.thickness)),
|
@floatFromInt(thickness.height(self.metrics.box_thickness)),
|
||||||
.on,
|
.on,
|
||||||
) catch {};
|
) catch {};
|
||||||
}
|
}
|
||||||
@ -2320,8 +2339,8 @@ fn draw_shade(self: Box, canvas: *font.sprite.Canvas, v: u16) void {
|
|||||||
canvas.rect((font.sprite.Box(u32){
|
canvas.rect((font.sprite.Box(u32){
|
||||||
.p0 = .{ .x = 0, .y = 0 },
|
.p0 = .{ .x = 0, .y = 0 },
|
||||||
.p1 = .{
|
.p1 = .{
|
||||||
.x = self.width,
|
.x = self.metrics.cell_width,
|
||||||
.y = self.height,
|
.y = self.metrics.cell_height,
|
||||||
},
|
},
|
||||||
}).rect(), @as(font.sprite.Color, @enumFromInt(v)));
|
}).rect(), @as(font.sprite.Color, @enumFromInt(v)));
|
||||||
}
|
}
|
||||||
@ -2339,12 +2358,12 @@ fn draw_dark_shade(self: Box, canvas: *font.sprite.Canvas) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_horizontal_one_eighth_block_n(self: Box, canvas: *font.sprite.Canvas, n: u32) void {
|
fn draw_horizontal_one_eighth_block_n(self: Box, canvas: *font.sprite.Canvas, n: u32) void {
|
||||||
const h = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.height)) / 8)));
|
const h = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.metrics.cell_height)) / 8)));
|
||||||
const y = @min(
|
const y = @min(
|
||||||
self.height -| h,
|
self.metrics.cell_height -| h,
|
||||||
@as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(self.height)) / 8))),
|
@as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(self.metrics.cell_height)) / 8))),
|
||||||
);
|
);
|
||||||
self.rect(canvas, 0, y, self.width, y + h);
|
self.rect(canvas, 0, y, self.metrics.cell_width, y + h);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_horizontal_one_eighth_1358_block(self: Box, canvas: *font.sprite.Canvas) void {
|
fn draw_horizontal_one_eighth_1358_block(self: Box, canvas: *font.sprite.Canvas) void {
|
||||||
@ -2355,24 +2374,24 @@ fn draw_horizontal_one_eighth_1358_block(self: Box, canvas: *font.sprite.Canvas)
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_quadrant(self: Box, canvas: *font.sprite.Canvas, comptime quads: Quads) void {
|
fn draw_quadrant(self: Box, canvas: *font.sprite.Canvas, comptime quads: Quads) void {
|
||||||
const center_x = self.width / 2 + self.width % 2;
|
const center_x = self.metrics.cell_width / 2 + self.metrics.cell_width % 2;
|
||||||
const center_y = self.height / 2 + self.height % 2;
|
const center_y = self.metrics.cell_height / 2 + self.metrics.cell_height % 2;
|
||||||
|
|
||||||
if (quads.tl) self.rect(canvas, 0, 0, center_x, center_y);
|
if (quads.tl) self.rect(canvas, 0, 0, center_x, center_y);
|
||||||
if (quads.tr) self.rect(canvas, center_x, 0, self.width, center_y);
|
if (quads.tr) self.rect(canvas, center_x, 0, self.metrics.cell_width, center_y);
|
||||||
if (quads.bl) self.rect(canvas, 0, center_y, center_x, self.height);
|
if (quads.bl) self.rect(canvas, 0, center_y, center_x, self.metrics.cell_height);
|
||||||
if (quads.br) self.rect(canvas, center_x, center_y, self.width, self.height);
|
if (quads.br) self.rect(canvas, center_x, center_y, self.metrics.cell_width, self.metrics.cell_height);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_braille(self: Box, canvas: *font.sprite.Canvas, cp: u32) void {
|
fn draw_braille(self: Box, canvas: *font.sprite.Canvas, cp: u32) void {
|
||||||
var w: u32 = @min(self.width / 4, self.height / 8);
|
var w: u32 = @min(self.metrics.cell_width / 4, self.metrics.cell_height / 8);
|
||||||
var x_spacing: u32 = self.width / 4;
|
var x_spacing: u32 = self.metrics.cell_width / 4;
|
||||||
var y_spacing: u32 = self.height / 8;
|
var y_spacing: u32 = self.metrics.cell_height / 8;
|
||||||
var x_margin: u32 = x_spacing / 2;
|
var x_margin: u32 = x_spacing / 2;
|
||||||
var y_margin: u32 = y_spacing / 2;
|
var y_margin: u32 = y_spacing / 2;
|
||||||
|
|
||||||
var x_px_left: u32 = self.width - 2 * x_margin - x_spacing - 2 * w;
|
var x_px_left: u32 = self.metrics.cell_width - 2 * x_margin - x_spacing - 2 * w;
|
||||||
var y_px_left: u32 = self.height - 2 * y_margin - 3 * y_spacing - 4 * w;
|
var y_px_left: u32 = self.metrics.cell_height - 2 * y_margin - 3 * y_spacing - 4 * w;
|
||||||
|
|
||||||
// First, try hard to ensure the DOT width is non-zero
|
// First, try hard to ensure the DOT width is non-zero
|
||||||
if (x_px_left >= 2 and y_px_left >= 4 and w == 0) {
|
if (x_px_left >= 2 and y_px_left >= 4 and w == 0) {
|
||||||
@ -2419,8 +2438,8 @@ fn draw_braille(self: Box, canvas: *font.sprite.Canvas, cp: u32) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert(x_px_left <= 1 or y_px_left <= 1);
|
assert(x_px_left <= 1 or y_px_left <= 1);
|
||||||
assert(2 * x_margin + 2 * w + x_spacing <= self.width);
|
assert(2 * x_margin + 2 * w + x_spacing <= self.metrics.cell_width);
|
||||||
assert(2 * y_margin + 4 * w + 3 * y_spacing <= self.height);
|
assert(2 * y_margin + 4 * w + 3 * y_spacing <= self.metrics.cell_height);
|
||||||
|
|
||||||
const x = [2]u32{ x_margin, x_margin + w + x_spacing };
|
const x = [2]u32{ x_margin, x_margin + w + x_spacing };
|
||||||
const y = y: {
|
const y = y: {
|
||||||
@ -2479,25 +2498,25 @@ fn draw_sextant(self: Box, canvas: *font.sprite.Canvas, cp: u32) void {
|
|||||||
const y_thirds = self.yThirds();
|
const y_thirds = self.yThirds();
|
||||||
|
|
||||||
if (sex.tl) self.rect(canvas, 0, 0, x_halfs[0], y_thirds[0]);
|
if (sex.tl) self.rect(canvas, 0, 0, x_halfs[0], y_thirds[0]);
|
||||||
if (sex.tr) self.rect(canvas, x_halfs[1], 0, self.width, y_thirds[0]);
|
if (sex.tr) self.rect(canvas, x_halfs[1], 0, self.metrics.cell_width, y_thirds[0]);
|
||||||
if (sex.ml) self.rect(canvas, 0, y_thirds[0], x_halfs[0], y_thirds[1]);
|
if (sex.ml) self.rect(canvas, 0, y_thirds[0], x_halfs[0], y_thirds[1]);
|
||||||
if (sex.mr) self.rect(canvas, x_halfs[1], y_thirds[0], self.width, y_thirds[1]);
|
if (sex.mr) self.rect(canvas, x_halfs[1], y_thirds[0], self.metrics.cell_width, y_thirds[1]);
|
||||||
if (sex.bl) self.rect(canvas, 0, y_thirds[1], x_halfs[0], self.height);
|
if (sex.bl) self.rect(canvas, 0, y_thirds[1], x_halfs[0], self.metrics.cell_height);
|
||||||
if (sex.br) self.rect(canvas, x_halfs[1], y_thirds[1], self.width, self.height);
|
if (sex.br) self.rect(canvas, x_halfs[1], y_thirds[1], self.metrics.cell_width, self.metrics.cell_height);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn xHalfs(self: Box) [2]u32 {
|
fn xHalfs(self: Box) [2]u32 {
|
||||||
return .{
|
return .{
|
||||||
@as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.width)) / 2))),
|
@as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2))),
|
||||||
@as(u32, @intFromFloat(@as(f64, @floatFromInt(self.width)) / 2)),
|
@as(u32, @intFromFloat(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn yThirds(self: Box) [2]u32 {
|
fn yThirds(self: Box) [2]u32 {
|
||||||
return switch (@mod(self.height, 3)) {
|
return switch (@mod(self.metrics.cell_height, 3)) {
|
||||||
0 => .{ self.height / 3, 2 * self.height / 3 },
|
0 => .{ self.metrics.cell_height / 3, 2 * self.metrics.cell_height / 3 },
|
||||||
1 => .{ self.height / 3, 2 * self.height / 3 + 1 },
|
1 => .{ self.metrics.cell_height / 3, 2 * self.metrics.cell_height / 3 + 1 },
|
||||||
2 => .{ self.height / 3 + 1, 2 * self.height / 3 },
|
2 => .{ self.metrics.cell_height / 3 + 1, 2 * self.metrics.cell_height / 3 },
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -2511,10 +2530,10 @@ fn draw_smooth_mosaic(
|
|||||||
const top: f64 = 0.0;
|
const top: f64 = 0.0;
|
||||||
const upper: f64 = @floatFromInt(y_thirds[0]);
|
const upper: f64 = @floatFromInt(y_thirds[0]);
|
||||||
const lower: f64 = @floatFromInt(y_thirds[1]);
|
const lower: f64 = @floatFromInt(y_thirds[1]);
|
||||||
const bottom: f64 = @floatFromInt(self.height);
|
const bottom: f64 = @floatFromInt(self.metrics.cell_height);
|
||||||
const left: f64 = 0.0;
|
const left: f64 = 0.0;
|
||||||
const center: f64 = @round(@as(f64, @floatFromInt(self.width)) / 2);
|
const center: f64 = @round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2);
|
||||||
const right: f64 = @floatFromInt(self.width);
|
const right: f64 = @floatFromInt(self.metrics.cell_width);
|
||||||
|
|
||||||
var path = z2d.Path.init(canvas.alloc);
|
var path = z2d.Path.init(canvas.alloc);
|
||||||
defer path.deinit();
|
defer path.deinit();
|
||||||
@ -2549,11 +2568,11 @@ fn draw_edge_triangle(
|
|||||||
comptime edge: Edge,
|
comptime edge: Edge,
|
||||||
) !void {
|
) !void {
|
||||||
const upper: f64 = 0.0;
|
const upper: f64 = 0.0;
|
||||||
const middle: f64 = @round(@as(f64, @floatFromInt(self.height)) / 2);
|
const middle: f64 = @round(@as(f64, @floatFromInt(self.metrics.cell_height)) / 2);
|
||||||
const lower: f64 = @floatFromInt(self.height);
|
const lower: f64 = @floatFromInt(self.metrics.cell_height);
|
||||||
const left: f64 = 0.0;
|
const left: f64 = 0.0;
|
||||||
const center: f64 = @round(@as(f64, @floatFromInt(self.width)) / 2);
|
const center: f64 = @round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2);
|
||||||
const right: f64 = @floatFromInt(self.width);
|
const right: f64 = @floatFromInt(self.metrics.cell_width);
|
||||||
|
|
||||||
var path = z2d.Path.init(canvas.alloc);
|
var path = z2d.Path.init(canvas.alloc);
|
||||||
defer path.deinit();
|
defer path.deinit();
|
||||||
@ -2588,12 +2607,12 @@ fn draw_arc(
|
|||||||
comptime corner: Corner,
|
comptime corner: Corner,
|
||||||
comptime thickness: Thickness,
|
comptime thickness: Thickness,
|
||||||
) !void {
|
) !void {
|
||||||
const thick_px = thickness.height(self.thickness);
|
const thick_px = thickness.height(self.metrics.box_thickness);
|
||||||
const float_width: f64 = @floatFromInt(self.width);
|
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
|
||||||
const float_height: f64 = @floatFromInt(self.height);
|
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
|
||||||
const float_thick: f64 = @floatFromInt(thick_px);
|
const float_thick: f64 = @floatFromInt(thick_px);
|
||||||
const center_x: f64 = @as(f64, @floatFromInt((self.width -| thick_px) / 2)) + float_thick / 2;
|
const center_x: f64 = @as(f64, @floatFromInt((self.metrics.cell_width -| thick_px) / 2)) + float_thick / 2;
|
||||||
const center_y: f64 = @as(f64, @floatFromInt((self.height -| thick_px) / 2)) + float_thick / 2;
|
const center_y: f64 = @as(f64, @floatFromInt((self.metrics.cell_height -| thick_px) / 2)) + float_thick / 2;
|
||||||
|
|
||||||
const r = @min(float_width, float_height) / 2;
|
const r = @min(float_width, float_height) / 2;
|
||||||
|
|
||||||
@ -2703,23 +2722,23 @@ fn draw_dash_horizontal(
|
|||||||
// We need at least 1 pixel for each gap and each dash, if we don't
|
// We need at least 1 pixel for each gap and each dash, if we don't
|
||||||
// have that then we can't draw our dashed line correctly so we just
|
// have that then we can't draw our dashed line correctly so we just
|
||||||
// draw a solid line and return.
|
// draw a solid line and return.
|
||||||
if (self.width < count + gap_count) {
|
if (self.metrics.cell_width < count + gap_count) {
|
||||||
self.hline_middle(canvas, .light);
|
self.hline_middle(canvas, .light);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We never want the gaps to take up more than 50% of the space,
|
// We never want the gaps to take up more than 50% of the space,
|
||||||
// because if they do the dashes are too small and look wrong.
|
// because if they do the dashes are too small and look wrong.
|
||||||
const gap_width = @min(desired_gap, self.width / (2 * count));
|
const gap_width = @min(desired_gap, self.metrics.cell_width / (2 * count));
|
||||||
const total_gap_width = gap_count * gap_width;
|
const total_gap_width = gap_count * gap_width;
|
||||||
const total_dash_width = self.width - total_gap_width;
|
const total_dash_width = self.metrics.cell_width - total_gap_width;
|
||||||
const dash_width = total_dash_width / count;
|
const dash_width = total_dash_width / count;
|
||||||
const remaining = total_dash_width % count;
|
const remaining = total_dash_width % count;
|
||||||
|
|
||||||
assert(dash_width * count + gap_width * gap_count + remaining == self.width);
|
assert(dash_width * count + gap_width * gap_count + remaining == self.metrics.cell_width);
|
||||||
|
|
||||||
// Our dashes should be centered vertically.
|
// Our dashes should be centered vertically.
|
||||||
const y: u32 = (self.height -| thick_px) / 2;
|
const y: u32 = (self.metrics.cell_height -| thick_px) / 2;
|
||||||
|
|
||||||
// We start at half a gap from the left edge, in order to center
|
// We start at half a gap from the left edge, in order to center
|
||||||
// our dashes properly.
|
// our dashes properly.
|
||||||
@ -2782,23 +2801,23 @@ fn draw_dash_vertical(
|
|||||||
// We need at least 1 pixel for each gap and each dash, if we don't
|
// We need at least 1 pixel for each gap and each dash, if we don't
|
||||||
// have that then we can't draw our dashed line correctly so we just
|
// have that then we can't draw our dashed line correctly so we just
|
||||||
// draw a solid line and return.
|
// draw a solid line and return.
|
||||||
if (self.height < count + gap_count) {
|
if (self.metrics.cell_height < count + gap_count) {
|
||||||
self.vline_middle(canvas, .light);
|
self.vline_middle(canvas, .light);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We never want the gaps to take up more than 50% of the space,
|
// We never want the gaps to take up more than 50% of the space,
|
||||||
// because if they do the dashes are too small and look wrong.
|
// because if they do the dashes are too small and look wrong.
|
||||||
const gap_height = @min(desired_gap, self.height / (2 * count));
|
const gap_height = @min(desired_gap, self.metrics.cell_height / (2 * count));
|
||||||
const total_gap_height = gap_count * gap_height;
|
const total_gap_height = gap_count * gap_height;
|
||||||
const total_dash_height = self.height - total_gap_height;
|
const total_dash_height = self.metrics.cell_height - total_gap_height;
|
||||||
const dash_height = total_dash_height / count;
|
const dash_height = total_dash_height / count;
|
||||||
const remaining = total_dash_height % count;
|
const remaining = total_dash_height % count;
|
||||||
|
|
||||||
assert(dash_height * count + gap_height * gap_count + remaining == self.height);
|
assert(dash_height * count + gap_height * gap_count + remaining == self.metrics.cell_height);
|
||||||
|
|
||||||
// Our dashes should be centered horizontally.
|
// Our dashes should be centered horizontally.
|
||||||
const x: u32 = (self.width -| thick_px) / 2;
|
const x: u32 = (self.metrics.cell_width -| thick_px) / 2;
|
||||||
|
|
||||||
// We start at the top of the cell.
|
// We start at the top of the cell.
|
||||||
var y: u32 = 0;
|
var y: u32 = 0;
|
||||||
@ -2824,32 +2843,49 @@ fn draw_dash_vertical(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_cursor_rect(self: Box, canvas: *font.sprite.Canvas) void {
|
fn draw_cursor_rect(self: Box, canvas: *font.sprite.Canvas) void {
|
||||||
self.rect(canvas, 0, 0, self.width, self.height);
|
// The cursor should fit itself to the canvas it's given, since if
|
||||||
|
// the cell height is adjusted upwards it will be given a canvas
|
||||||
|
// with the original un-adjusted height, so we can't use the height
|
||||||
|
// from the metrics.
|
||||||
|
const height: u32 = @intCast(canvas.sfc.getHeight());
|
||||||
|
self.rect(canvas, 0, 0, self.metrics.cell_width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_cursor_hollow_rect(self: Box, canvas: *font.sprite.Canvas) void {
|
fn draw_cursor_hollow_rect(self: Box, canvas: *font.sprite.Canvas) void {
|
||||||
const thick_px = Thickness.super_light.height(self.thickness);
|
// The cursor should fit itself to the canvas it's given, since if
|
||||||
|
// the cell height is adjusted upwards it will be given a canvas
|
||||||
|
// with the original un-adjusted height, so we can't use the height
|
||||||
|
// from the metrics.
|
||||||
|
const height: u32 = @intCast(canvas.sfc.getHeight());
|
||||||
|
|
||||||
self.vline(canvas, 0, self.height, 0, thick_px);
|
const thick_px = Thickness.super_light.height(self.metrics.cursor_thickness);
|
||||||
self.vline(canvas, 0, self.height, self.width -| thick_px, thick_px);
|
|
||||||
self.hline(canvas, 0, self.width, 0, thick_px);
|
self.rect(canvas, 0, 0, self.metrics.cell_width, thick_px);
|
||||||
self.hline(canvas, 0, self.width, self.height -| thick_px, thick_px);
|
self.rect(canvas, 0, 0, thick_px, height);
|
||||||
|
self.rect(canvas, self.metrics.cell_width -| thick_px, 0, self.metrics.cell_width, height);
|
||||||
|
self.rect(canvas, 0, height -| thick_px, self.metrics.cell_width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_cursor_bar(self: Box, canvas: *font.sprite.Canvas) void {
|
fn draw_cursor_bar(self: Box, canvas: *font.sprite.Canvas) void {
|
||||||
const thick_px = Thickness.light.height(self.thickness);
|
// The cursor should fit itself to the canvas it's given, since if
|
||||||
|
// the cell height is adjusted upwards it will be given a canvas
|
||||||
|
// with the original un-adjusted height, so we can't use the height
|
||||||
|
// from the metrics.
|
||||||
|
const height: u32 = @intCast(canvas.sfc.getHeight());
|
||||||
|
|
||||||
self.vline(canvas, 0, self.height, 0, thick_px);
|
const thick_px = Thickness.light.height(self.metrics.cursor_thickness);
|
||||||
|
|
||||||
|
self.rect(canvas, 0, 0, thick_px, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn vline_middle(self: Box, canvas: *font.sprite.Canvas, thickness: Thickness) void {
|
fn vline_middle(self: Box, canvas: *font.sprite.Canvas, thickness: Thickness) void {
|
||||||
const thick_px = thickness.height(self.thickness);
|
const thick_px = thickness.height(self.metrics.box_thickness);
|
||||||
self.vline(canvas, 0, self.height, (self.width -| thick_px) / 2, thick_px);
|
self.vline(canvas, 0, self.metrics.cell_height, (self.metrics.cell_width -| thick_px) / 2, thick_px);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hline_middle(self: Box, canvas: *font.sprite.Canvas, thickness: Thickness) void {
|
fn hline_middle(self: Box, canvas: *font.sprite.Canvas, thickness: Thickness) void {
|
||||||
const thick_px = thickness.height(self.thickness);
|
const thick_px = thickness.height(self.metrics.box_thickness);
|
||||||
self.hline(canvas, 0, self.width, (self.height -| thick_px) / 2, thick_px);
|
self.hline(canvas, 0, self.metrics.cell_width, (self.metrics.cell_height -| thick_px) / 2, thick_px);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn vline(
|
fn vline(
|
||||||
@ -2861,11 +2897,11 @@ fn vline(
|
|||||||
thickness_px: u32,
|
thickness_px: u32,
|
||||||
) void {
|
) void {
|
||||||
canvas.rect((font.sprite.Box(u32){ .p0 = .{
|
canvas.rect((font.sprite.Box(u32){ .p0 = .{
|
||||||
.x = @min(@max(x, 0), self.width),
|
.x = @min(@max(x, 0), self.metrics.cell_width),
|
||||||
.y = @min(@max(y1, 0), self.height),
|
.y = @min(@max(y1, 0), self.metrics.cell_height),
|
||||||
}, .p1 = .{
|
}, .p1 = .{
|
||||||
.x = @min(@max(x + thickness_px, 0), self.width),
|
.x = @min(@max(x + thickness_px, 0), self.metrics.cell_width),
|
||||||
.y = @min(@max(y2, 0), self.height),
|
.y = @min(@max(y2, 0), self.metrics.cell_height),
|
||||||
} }).rect(), .on);
|
} }).rect(), .on);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2878,11 +2914,11 @@ fn hline(
|
|||||||
thickness_px: u32,
|
thickness_px: u32,
|
||||||
) void {
|
) void {
|
||||||
canvas.rect((font.sprite.Box(u32){ .p0 = .{
|
canvas.rect((font.sprite.Box(u32){ .p0 = .{
|
||||||
.x = @min(@max(x1, 0), self.width),
|
.x = @min(@max(x1, 0), self.metrics.cell_width),
|
||||||
.y = @min(@max(y, 0), self.height),
|
.y = @min(@max(y, 0), self.metrics.cell_height),
|
||||||
}, .p1 = .{
|
}, .p1 = .{
|
||||||
.x = @min(@max(x2, 0), self.width),
|
.x = @min(@max(x2, 0), self.metrics.cell_width),
|
||||||
.y = @min(@max(y + thickness_px, 0), self.height),
|
.y = @min(@max(y + thickness_px, 0), self.metrics.cell_height),
|
||||||
} }).rect(), .on);
|
} }).rect(), .on);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2895,11 +2931,11 @@ fn rect(
|
|||||||
y2: u32,
|
y2: u32,
|
||||||
) void {
|
) void {
|
||||||
canvas.rect((font.sprite.Box(u32){ .p0 = .{
|
canvas.rect((font.sprite.Box(u32){ .p0 = .{
|
||||||
.x = @min(@max(x1, 0), self.width),
|
.x = @min(@max(x1, 0), self.metrics.cell_width),
|
||||||
.y = @min(@max(y1, 0), self.height),
|
.y = @min(@max(y1, 0), self.metrics.cell_height),
|
||||||
}, .p1 = .{
|
}, .p1 = .{
|
||||||
.x = @min(@max(x2, 0), self.width),
|
.x = @min(@max(x2, 0), self.metrics.cell_width),
|
||||||
.y = @min(@max(y2, 0), self.height),
|
.y = @min(@max(y2, 0), self.metrics.cell_height),
|
||||||
} }).rect(), .on);
|
} }).rect(), .on);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2913,14 +2949,21 @@ test "all" {
|
|||||||
var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale);
|
var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale);
|
||||||
defer atlas_grayscale.deinit(alloc);
|
defer atlas_grayscale.deinit(alloc);
|
||||||
|
|
||||||
const face: Box = .{ .width = 18, .height = 36, .thickness = 2 };
|
const face: Box = .{
|
||||||
|
.metrics = font.Metrics.calc(.{
|
||||||
|
.cell_width = 18.0,
|
||||||
|
.ascent = 30.0,
|
||||||
|
.descent = -6.0,
|
||||||
|
.line_gap = 0.0,
|
||||||
|
}),
|
||||||
|
};
|
||||||
const glyph = try face.renderGlyph(
|
const glyph = try face.renderGlyph(
|
||||||
alloc,
|
alloc,
|
||||||
&atlas_grayscale,
|
&atlas_grayscale,
|
||||||
cp,
|
cp,
|
||||||
);
|
);
|
||||||
try testing.expectEqual(@as(u32, face.width), glyph.width);
|
try testing.expectEqual(@as(u32, face.metrics.cell_width), glyph.width);
|
||||||
try testing.expectEqual(@as(u32, face.height), glyph.height);
|
try testing.expectEqual(@as(u32, face.metrics.cell_height), glyph.height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3037,18 +3080,28 @@ test "render all sprites" {
|
|||||||
var atlas_grayscale = try font.Atlas.init(alloc, 1024, .grayscale);
|
var atlas_grayscale = try font.Atlas.init(alloc, 1024, .grayscale);
|
||||||
defer atlas_grayscale.deinit(alloc);
|
defer atlas_grayscale.deinit(alloc);
|
||||||
|
|
||||||
// Even cell size and thickness
|
// Even cell size and thickness (18 x 36)
|
||||||
try (Box{
|
try (Box{
|
||||||
.width = 18,
|
.metrics = font.Metrics.calc(.{
|
||||||
.height = 36,
|
.cell_width = 18.0,
|
||||||
.thickness = 2,
|
.ascent = 30.0,
|
||||||
|
.descent = -6.0,
|
||||||
|
.line_gap = 0.0,
|
||||||
|
.underline_thickness = 2.0,
|
||||||
|
.strikethrough_thickness = 2.0,
|
||||||
|
}),
|
||||||
}).testRenderAll(alloc, &atlas_grayscale);
|
}).testRenderAll(alloc, &atlas_grayscale);
|
||||||
|
|
||||||
// Odd cell size and thickness
|
// Odd cell size and thickness (9 x 15)
|
||||||
try (Box{
|
try (Box{
|
||||||
.width = 9,
|
.metrics = font.Metrics.calc(.{
|
||||||
.height = 15,
|
.cell_width = 9.0,
|
||||||
.thickness = 1,
|
.ascent = 12.0,
|
||||||
|
.descent = -3.0,
|
||||||
|
.line_gap = 0.0,
|
||||||
|
.underline_thickness = 1.0,
|
||||||
|
.strikethrough_thickness = 1.0,
|
||||||
|
}),
|
||||||
}).testRenderAll(alloc, &atlas_grayscale);
|
}).testRenderAll(alloc, &atlas_grayscale);
|
||||||
|
|
||||||
const ground_truth = @embedFile("./testdata/Box.ppm");
|
const ground_truth = @embedFile("./testdata/Box.ppm");
|
||||||
|
@ -24,22 +24,8 @@ const underline = @import("underline.zig");
|
|||||||
|
|
||||||
const log = std.log.scoped(.font_sprite);
|
const log = std.log.scoped(.font_sprite);
|
||||||
|
|
||||||
/// The cell width and height.
|
/// Grid metrics for rendering sprites.
|
||||||
width: u32,
|
metrics: font.Metrics,
|
||||||
height: u32,
|
|
||||||
|
|
||||||
/// Base thickness value for lines of sprites. This is in pixels. If you
|
|
||||||
/// want to do any DPI scaling, it is expected to be done earlier.
|
|
||||||
thickness: u32 = 1,
|
|
||||||
|
|
||||||
/// The position of the underline.
|
|
||||||
underline_position: u32 = 0,
|
|
||||||
|
|
||||||
/// The position of the strikethrough.
|
|
||||||
// NOTE(mitchellh): We don't use a dedicated strikethrough thickness
|
|
||||||
// setting yet but fonts can in theory set this. If this becomes an
|
|
||||||
// issue in practice we can add it here.
|
|
||||||
strikethrough_position: u32 = 0,
|
|
||||||
|
|
||||||
/// Returns true if the codepoint exists in our sprite font.
|
/// Returns true if the codepoint exists in our sprite font.
|
||||||
pub fn hasCodepoint(self: Face, cp: u32, p: ?font.Presentation) bool {
|
pub fn hasCodepoint(self: Face, cp: u32, p: ?font.Presentation) bool {
|
||||||
@ -65,10 +51,12 @@ pub fn renderGlyph(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const metrics = opts.grid_metrics orelse self.metrics;
|
||||||
|
|
||||||
// We adjust our sprite width based on the cell width.
|
// We adjust our sprite width based on the cell width.
|
||||||
const width = switch (opts.cell_width orelse 1) {
|
const width = switch (opts.cell_width orelse 1) {
|
||||||
0, 1 => self.width,
|
0, 1 => metrics.cell_width,
|
||||||
else => |width| self.width * width,
|
else => |width| metrics.cell_width * width,
|
||||||
};
|
};
|
||||||
|
|
||||||
// It should be impossible for this to be null and we assert that
|
// It should be impossible for this to be null and we assert that
|
||||||
@ -86,58 +74,16 @@ pub fn renderGlyph(
|
|||||||
|
|
||||||
// Safe to ".?" because of the above assertion.
|
// Safe to ".?" because of the above assertion.
|
||||||
return switch (kind) {
|
return switch (kind) {
|
||||||
.box => box: {
|
.box => (Box{ .metrics = metrics }).renderGlyph(alloc, atlas, cp),
|
||||||
const thickness = switch (cp) {
|
|
||||||
@intFromEnum(Sprite.cursor_rect),
|
|
||||||
@intFromEnum(Sprite.cursor_hollow_rect),
|
|
||||||
@intFromEnum(Sprite.cursor_bar),
|
|
||||||
=> if (opts.grid_metrics) |m| m.cursor_thickness else self.thickness,
|
|
||||||
else => self.thickness,
|
|
||||||
};
|
|
||||||
|
|
||||||
const f: Box, const y_offset: u32 = face: {
|
|
||||||
// Expected, usual values.
|
|
||||||
var f: Box = .{
|
|
||||||
.width = width,
|
|
||||||
.height = self.height,
|
|
||||||
.thickness = thickness,
|
|
||||||
};
|
|
||||||
|
|
||||||
// If the codepoint is unadjusted then we want to adjust
|
|
||||||
// (heh) the width/height to the proper size and also record
|
|
||||||
// an offset to apply to our final glyph so it renders in the
|
|
||||||
// correct place because renderGlyph assumes full size.
|
|
||||||
var y_offset: u32 = 0;
|
|
||||||
if (Box.unadjustedCodepoint(cp)) unadjust: {
|
|
||||||
const metrics = opts.grid_metrics orelse break :unadjust;
|
|
||||||
const height = metrics.original_cell_height orelse break :unadjust;
|
|
||||||
|
|
||||||
// If our height shrunk, then we use the original adjusted
|
|
||||||
// height because we don't want to overflow the cell.
|
|
||||||
if (height >= self.height) break :unadjust;
|
|
||||||
|
|
||||||
// The offset is divided by two because it is vertically
|
|
||||||
// centered.
|
|
||||||
y_offset = (self.height - height) / 2;
|
|
||||||
f.height = height;
|
|
||||||
}
|
|
||||||
|
|
||||||
break :face .{ f, y_offset };
|
|
||||||
};
|
|
||||||
|
|
||||||
var g = try f.renderGlyph(alloc, atlas, cp);
|
|
||||||
g.offset_y += @intCast(y_offset);
|
|
||||||
break :box g;
|
|
||||||
},
|
|
||||||
|
|
||||||
.underline => try underline.renderGlyph(
|
.underline => try underline.renderGlyph(
|
||||||
alloc,
|
alloc,
|
||||||
atlas,
|
atlas,
|
||||||
@enumFromInt(cp),
|
@enumFromInt(cp),
|
||||||
width,
|
width,
|
||||||
self.height,
|
metrics.cell_height,
|
||||||
self.underline_position,
|
metrics.underline_position,
|
||||||
self.thickness,
|
metrics.underline_thickness,
|
||||||
),
|
),
|
||||||
|
|
||||||
.strikethrough => try underline.renderGlyph(
|
.strikethrough => try underline.renderGlyph(
|
||||||
@ -145,26 +91,34 @@ pub fn renderGlyph(
|
|||||||
atlas,
|
atlas,
|
||||||
@enumFromInt(cp),
|
@enumFromInt(cp),
|
||||||
width,
|
width,
|
||||||
self.height,
|
metrics.cell_height,
|
||||||
self.strikethrough_position,
|
metrics.strikethrough_position,
|
||||||
self.thickness,
|
metrics.strikethrough_thickness,
|
||||||
),
|
),
|
||||||
|
|
||||||
.overline => try underline.renderGlyph(
|
.overline => overline: {
|
||||||
alloc,
|
var g = try underline.renderGlyph(
|
||||||
atlas,
|
alloc,
|
||||||
@enumFromInt(cp),
|
atlas,
|
||||||
width,
|
@enumFromInt(cp),
|
||||||
self.height,
|
width,
|
||||||
0,
|
metrics.cell_height,
|
||||||
self.thickness,
|
0,
|
||||||
),
|
metrics.overline_thickness,
|
||||||
|
);
|
||||||
|
|
||||||
|
// We have to manually subtract the overline position
|
||||||
|
// on the rendered glyph since it can be negative.
|
||||||
|
g.offset_y -= metrics.overline_position;
|
||||||
|
|
||||||
|
break :overline g;
|
||||||
|
},
|
||||||
|
|
||||||
.powerline => powerline: {
|
.powerline => powerline: {
|
||||||
const f: Powerline = .{
|
const f: Powerline = .{
|
||||||
.width = width,
|
.width = metrics.cell_width,
|
||||||
.height = self.height,
|
.height = metrics.cell_height,
|
||||||
.thickness = self.thickness,
|
.thickness = metrics.box_thickness,
|
||||||
};
|
};
|
||||||
|
|
||||||
break :powerline try f.renderGlyph(alloc, atlas, cp);
|
break :powerline try f.renderGlyph(alloc, atlas, cp);
|
||||||
|
Reference in New Issue
Block a user