diff --git a/pkg/freetype/face.zig b/pkg/freetype/face.zig index 8bbc75616..e3bcd5292 100644 --- a/pkg/freetype/face.zig +++ b/pkg/freetype/face.zig @@ -215,6 +215,8 @@ pub const SfntTag = enum(c_int) { pub fn DataType(comptime self: SfntTag) type { return switch (self) { .os2 => c.TT_OS2, + .head => c.TT_Header, + .post => c.TT_Postscript, else => unreachable, // As-needed... }; } diff --git a/pkg/macos/text.zig b/pkg/macos/text.zig index 149cef66b..0589f8692 100644 --- a/pkg/macos/text.zig +++ b/pkg/macos/text.zig @@ -20,6 +20,7 @@ pub const FontVariationAxisKey = font_descriptor.FontVariationAxisKey; pub const FontSymbolicTraits = font_descriptor.FontSymbolicTraits; pub const createFontDescriptorsFromURL = font_manager.createFontDescriptorsFromURL; pub const createFontDescriptorsFromData = font_manager.createFontDescriptorsFromData; +pub const createFontDescriptorFromData = font_manager.createFontDescriptorFromData; pub const Frame = frame.Frame; pub const Framesetter = framesetter.Framesetter; pub const Line = line.Line; diff --git a/pkg/macos/text/font_manager.zig b/pkg/macos/text/font_manager.zig index f918167a0..988da1220 100644 --- a/pkg/macos/text/font_manager.zig +++ b/pkg/macos/text/font_manager.zig @@ -1,6 +1,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const foundation = @import("../foundation.zig"); +const FontDescriptor = @import("./font_descriptor.zig").FontDescriptor; const c = @import("c.zig").c; pub fn createFontDescriptorsFromURL(url: *foundation.URL) ?*foundation.Array { @@ -14,3 +15,9 @@ pub fn createFontDescriptorsFromData(data: *foundation.Data) ?*foundation.Array @ptrCast(data), ))); } + +pub fn createFontDescriptorFromData(data: *foundation.Data) ?*FontDescriptor { + return @ptrFromInt(@intFromPtr(c.CTFontManagerCreateFontDescriptorFromData( + @ptrCast(data), + ))); +} diff --git a/src/font/CodepointResolver.zig b/src/font/CodepointResolver.zig index f3be843c5..326ca0186 100644 --- a/src/font/CodepointResolver.zig +++ b/src/font/CodepointResolver.zig @@ -520,7 +520,14 @@ test "getIndex box glyph" { var r: CodepointResolver = .{ .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); diff --git a/src/font/SharedGrid.zig b/src/font/SharedGrid.zig index 8af385b84..f907b59ad 100644 --- a/src/font/SharedGrid.zig +++ b/src/font/SharedGrid.zig @@ -122,13 +122,7 @@ fn reloadMetrics(self: *SharedGrid) !void { self.metrics = face.metrics; // Setup our sprite font. - self.resolver.sprite = .{ - .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, - }; + self.resolver.sprite = .{ .metrics = self.metrics }; } /// Returns the grid cell size. diff --git a/src/font/face/Metrics.zig b/src/font/face/Metrics.zig index a1eb50bdd..d6b1bdd0c 100644 --- a/src/font/face/Metrics.zig +++ b/src/font/face/Metrics.zig @@ -6,21 +6,28 @@ const std = @import("std"); cell_width: u32, cell_height: u32, -/// For monospace grids, the recommended y-value from the bottom to set -/// 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. +/// Distance in pixels from the bottom of the cell to the text baseline. cell_baseline: u32, -/// The position of the underline from the top of the cell and the -/// thickness in pixels. +/// Distance in pixels from the top of the cell to the top of the underline. underline_position: u32, +/// Thickness in pixels of the underline. underline_thickness: u32, -/// The position and thickness of a strikethrough. Same units/style -/// as the underline fields. +/// Distance in pixels from the top of the cell to the top of the strikethrough. strikethrough_position: u32, +/// Thickness in pixels of the strikethrough. 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 /// because it is not determined by fonts but rather by user configuration. cursor_thickness: u32 = 1, @@ -30,6 +37,146 @@ cursor_thickness: u32 = 1, original_cell_width: ?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. pub fn apply(self: *Metrics, mods: ModifierSet) void { 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 @@ -152,23 +314,26 @@ pub const Modifier = union(enum) { } /// 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) { .percent => |p| percent: { const p_clamped: f64 = @max(0, p); const v_f64: f64 = @floatFromInt(v); const applied_f64: f64 = @round(v_f64 * p_clamped); - const applied_u32: u32 = @intFromFloat(applied_f64); - break :percent applied_u32; + const applied_T: T = @intFromFloat(applied_f64); + break :percent applied_T; }, .absolute => |abs| absolute: { const v_i64: i64 = @intCast(v); const abs_i64: i64 = @intCast(abs); - const applied_i64: i64 = @max(0, v_i64 +| abs_i64); - const applied_u32: u32 = std.math.cast(u32, applied_i64) orelse - std.math.maxInt(u32); - break :absolute applied_u32; + const applied_i64: i64 = v_i64 +| abs_i64; + const clamped_i64: i64 = if (signed) applied_i64 else @max(0, applied_i64); + const applied_T: T = std.math.cast(T, clamped_i64) orelse + 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 count: usize = 0; 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 }; count += 1; } @@ -242,6 +407,9 @@ fn init() Metrics { .underline_thickness = 0, .strikethrough_position = 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 v: u32 = m.apply(100); + const v: u32 = m.apply(@as(u32, 100)); try testing.expectEqual(@as(u32, 80), v); } { 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); } } @@ -352,17 +520,17 @@ test "Modifier: absolute" { { 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); } { 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); } { 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); } } diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 363dbacd8..8749f9092 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -55,12 +55,10 @@ pub const Face = struct { const data = try macos.foundation.Data.createWithBytesNoCopy(source); defer data.release(); - const arr = macos.text.createFontDescriptorsFromData(data) orelse + const desc = macos.text.createFontDescriptorFromData(data) orelse return error.FontInitFailure; - defer arr.release(); - if (arr.getCount() == 0) return error.FontInitFailure; + defer desc.release(); - const desc = arr.getValueAtIndex(macos.text.FontDescriptor, 0); const ct_font = try macos.text.Font.createWithFontDescriptor(desc, 12); 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 // visible ASCII characters. Usually 'M' is widest but we just take // whatever is widest. - const cell_width: f32 = cell_width: { + const cell_width: f64 = cell_width: { // Build a comptime array of all the ASCII chars const unichars = comptime unichars: { const len = 127 - 32; @@ -564,93 +665,29 @@ pub const Face = struct { 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 - // the font. I also tried Kitty's approach at one point which is to - // use the CoreText layout engine but this led to some glyphs being - // set incorrectly. - const layout_metrics: struct { - height: f32, - ascent: f32, - leading: f32, - } = metrics: { - const ascent = ct_font.getAscent(); - const descent = ct_font.getDescent(); - - // 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; + return font.face.Metrics.calc(.{ + .cell_width = cell_width, + .ascent = ascent, + .descent = descent, + .line_gap = line_gap, + .underline_position = underline_position, + .underline_thickness = underline_thickness, + .strikethrough_position = strikethrough_position, + .strikethrough_thickness = strikethrough_thickness, + .cap_height = cap_height, + .ex_height = ex_height, + }); } /// 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 return null; defer data.release(); @@ -678,7 +715,9 @@ const ColorState = struct { svg: ?opentype.SVG, 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. // In the future we probably want to actually parse it and // check for glyphs. @@ -699,8 +738,16 @@ const ColorState = struct { errdefer data.release(); const ptr = data.getPointer(); 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 .{ - .svg = try opentype.SVG.init(ptr[0..len]), + .svg = svg, .data = data, }; }; @@ -907,3 +954,58 @@ test "glyphIndex colored vs text" { 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); +} diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 683f80cc8..c3d4a449b 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -16,6 +16,7 @@ const font = @import("../main.zig"); const Glyph = font.Glyph; const Library = font.Library; const convert = @import("freetype_convert.zig"); +const opentype = @import("../opentype.zig"); const fastmem = @import("../../fastmem.zig"); const quirks = @import("../../quirks.zig"); const config = @import("../../config.zig"); @@ -85,7 +86,7 @@ pub const Face = struct { .lib = lib.lib, .face = face, .hb_font = hb_font, - .metrics = calcMetrics(face, opts.metric_modifiers), + .metrics = try calcMetrics(face, opts.metric_modifiers), .load_flags = opts.freetype_load_flags, }; 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. pub fn setSize(self: *Face, opts: font.face.Options) !void { 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 { @@ -258,7 +259,7 @@ pub const Face = struct { try self.face.setVarDesignCoordinates(coords); // 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 @@ -593,6 +594,15 @@ pub const Face = struct { 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 /// the metrics are calculated for every face and cached since they're /// frequently required for renderers and take up next to little memory space @@ -605,138 +615,143 @@ pub const Face = struct { fn calcMetrics( face: freetype.Face, modifiers: ?*const font.face.Metrics.ModifierSet, - ) font.face.Metrics { + ) CalcMetricsError!font.face.Metrics { const size_metrics = face.handle.*.size.*.metrics; - // Cell width is calculated by preferring to use 'M' as the width of a - // cell since 'M' is generally the widest ASCII character. If loading 'M' - // fails then we use the max advance of the font face size metrics. - const cell_width: f32 = cell_width: { - if (face.getCharIndex('M')) |glyph_index| { - if (face.loadGlyph(glyph_index, .{ .render = true })) { - break :cell_width f26dot6ToFloat(face.handle.*.glyph.*.advance.x); - } else |_| { - // Ignore the error since we just fall back to max_advance below + // This code relies on this assumption, and it should always be + // true since we don't do any non-uniform scaling on the font ever. + assert(size_metrics.x_ppem == size_metrics.y_ppem); + + // Read the 'head' table out of the font data. + const head = face.getSfntTable(.head) orelse return error.CopyTableError; + + // Read the 'post' table out of the font data. + 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. - // If that fails then we just pretend it's 65% of the ascent height. - const ex_height: f32 = ex_height: { + // The OS/2 table does not include sCapHeight or sxHeight in version 1. + const has_os2_height_metrics = os2.version >= 2; + + // 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.loadGlyph(glyph_index, .{ .render = true })) { - break :ex_height f26dot6ToFloat(face.handle.*.glyph.*.metrics.height); - } else |_| { - // Ignore the error since we just fall back to 65% of the ascent below - } + break :ex_height f26dot6ToF64(face.handle.*.glyph.*.metrics.height); + } else |_| {} } - 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 - // to handle edge cases in fonts: (1) the height as reported in metadata - // 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); + var result = font.face.Metrics.calc(.{ + .cell_width = cell_width, - // The maximum height a glyph can take in the font - const max_glyph_height = f26dot6ToFloat(size_metrics.ascender) - - f26dot6ToFloat(size_metrics.descender); + .ascent = ascent, + .descent = descent, + .line_gap = line_gap, - // The height of the underscore character - const underscore_height = underscore: { - 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 - } - } + .underline_position = underline_position, + .underline_thickness = underline_thickness, - break :underscore 0; - }; + .strikethrough_position = strikethrough_position, + .strikethrough_thickness = strikethrough_thickness, - break :cell_height @max( - face_height, - @max(max_glyph_height, underscore_height), - ); - }; + .cap_height = cap_height, + .ex_height = ex_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.*); // std.log.warn("font metrics={}", .{result}); @@ -744,13 +759,6 @@ pub const Face = struct { 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. pub fn copyTable(self: Face, alloc: Allocator, tag: *const [4]u8) !?[]u8 { return try self.face.loadSfntTable(alloc, freetype.Tag.init(tag)); @@ -828,6 +836,9 @@ test "color emoji" { .underline_thickness = 0, .strikethrough_position = 0, .strikethrough_thickness = 0, + .overline_position = 0, + .overline_thickness = 0, + .box_thickness = 0, }, }); try testing.expectEqual(@as(u32, 24), glyph.height); @@ -853,24 +864,42 @@ test "metrics" { try testing.expectEqual(font.face.Metrics{ .cell_width = 8, - .cell_height = 1.8e1, - .cell_baseline = 4, - .underline_position = 18, + // 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, }, ft_font.metrics); // Resize should change metrics try ft_font.setSize(.{ .size = .{ .points = 24, .xdpi = 96, .ydpi = 96 } }); try testing.expectEqual(font.face.Metrics{ .cell_width = 16, - .cell_height = 35, - .cell_baseline = 7, - .underline_position = 35, + .cell_height = 34, + .cell_baseline = 6, + .underline_position = 34, .underline_thickness = 2, - .strikethrough_position = 20, + .strikethrough_position = 19, .strikethrough_thickness = 2, + .overline_position = 0, + .overline_thickness = 2, + .box_thickness = 2, }, ft_font.metrics); } diff --git a/src/font/opentype.zig b/src/font/opentype.zig index 798df5b2c..dd02efeb3 100644 --- a/src/font/opentype.zig +++ b/src/font/opentype.zig @@ -1,6 +1,16 @@ +pub const sfnt = @import("opentype/sfnt.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 OS2 = os2.OS2; +pub const Post = post.Post; +pub const Hhea = hhea.Hhea; +pub const Head = head.Head; test { @import("std").testing.refAllDecls(@This()); diff --git a/src/font/opentype/head.zig b/src/font/opentype/head.zig new file mode 100644 index 000000000..b4ee3ffd4 --- /dev/null +++ b/src/font/opentype/head.zig @@ -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, + ); +} diff --git a/src/font/opentype/hhea.zig b/src/font/opentype/hhea.zig new file mode 100644 index 000000000..300f29c7a --- /dev/null +++ b/src/font/opentype/hhea.zig @@ -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, + ); +} diff --git a/src/font/opentype/os2.zig b/src/font/opentype/os2.zig new file mode 100644 index 000000000..a18538d5f --- /dev/null +++ b/src/font/opentype/os2.zig @@ -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); +} diff --git a/src/font/opentype/post.zig b/src/font/opentype/post.zig new file mode 100644 index 000000000..ff56a5013 --- /dev/null +++ b/src/font/opentype/post.zig @@ -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, + ); +} diff --git a/src/font/opentype/sfnt.zig b/src/font/opentype/sfnt.zig new file mode 100644 index 000000000..cbce50455 --- /dev/null +++ b/src/font/opentype/sfnt.zig @@ -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); +} diff --git a/src/font/opentype/svg.zig b/src/font/opentype/svg.zig index ff431dee2..15edff5aa 100644 --- a/src/font/opentype/svg.zig +++ b/src/font/opentype/svg.zig @@ -22,7 +22,10 @@ pub const SVG = struct { /// All records in the table. 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); const reader = fbs.reader(); diff --git a/src/font/sprite/Box.zig b/src/font/sprite/Box.zig index 382aa4206..a6317196f 100644 --- a/src/font/sprite/Box.zig +++ b/src/font/sprite/Box.zig @@ -27,14 +27,8 @@ const Sprite = @import("../sprite.zig").Sprite; const log = std.log.scoped(.box_font); -/// The cell width and height because the boxes are fit perfectly -/// into a cell so that they all properly connect with zero spacing. -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, +/// Grid metrics for the rendering. +metrics: font.Metrics, /// The thickness of a line. const Thickness = enum { @@ -218,8 +212,29 @@ pub fn renderGlyph( atlas: *font.Atlas, cp: u32, ) !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 - 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); // Perform the actual drawing @@ -231,16 +246,20 @@ pub fn renderGlyph( // 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 // 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{ - .width = self.width, - .height = self.height, + .width = metrics.cell_width, + .height = height, .offset_x = 0, .offset_y = offset_y, .atlas_x = region.x, .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, lines: Lines, ) void { - const light_px = Thickness.light.height(self.thickness); - const heavy_px = Thickness.heavy.height(self.thickness); + const light_px = Thickness.light.height(self.metrics.box_thickness); + const heavy_px = Thickness.heavy.height(self.metrics.box_thickness); // 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 const h_light_bottom = h_light_top +| light_px; // 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 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; // 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 const v_light_right = v_light_left +| light_px; // 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 const v_heavy_right = v_heavy_left +| heavy_px; @@ -1752,27 +1771,27 @@ fn draw_lines( switch (lines.right) { .none => {}, - .light => self.rect(canvas, right_left, h_light_top, self.width, h_light_bottom), - .heavy => self.rect(canvas, right_left, h_heavy_top, self.width, h_heavy_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.metrics.cell_width, h_heavy_bottom), .double => { 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; - self.rect(canvas, top_left, h_double_top, self.width, h_light_top); - self.rect(canvas, bottom_left, h_light_bottom, self.width, h_double_bottom); + self.rect(canvas, top_left, h_double_top, self.metrics.cell_width, h_light_top); + self.rect(canvas, bottom_left, h_light_bottom, self.metrics.cell_width, h_double_bottom); }, } switch (lines.down) { .none => {}, - .light => self.rect(canvas, v_light_left, down_top, v_light_right, self.height), - .heavy => self.rect(canvas, v_heavy_left, down_top, v_heavy_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.metrics.cell_height), .double => { 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; - self.rect(canvas, v_double_left, left_top, v_light_left, self.height); - self.rect(canvas, v_light_right, right_top, v_double_right, 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.metrics.cell_height); }, } @@ -1794,8 +1813,8 @@ fn draw_light_triple_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) voi self.draw_dash_horizontal( canvas, 3, - Thickness.light.height(self.thickness), - @max(4, Thickness.light.height(self.thickness)), + Thickness.light.height(self.metrics.box_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( canvas, 3, - Thickness.heavy.height(self.thickness), - @max(4, Thickness.light.height(self.thickness)), + Thickness.heavy.height(self.metrics.box_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( canvas, 3, - Thickness.light.height(self.thickness), - @max(4, Thickness.light.height(self.thickness)), + Thickness.light.height(self.metrics.box_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( canvas, 3, - Thickness.heavy.height(self.thickness), - @max(4, Thickness.light.height(self.thickness)), + Thickness.heavy.height(self.metrics.box_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( canvas, 4, - Thickness.light.height(self.thickness), - @max(4, Thickness.light.height(self.thickness)), + Thickness.light.height(self.metrics.box_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( canvas, 4, - Thickness.heavy.height(self.thickness), - @max(4, Thickness.light.height(self.thickness)), + Thickness.heavy.height(self.metrics.box_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( canvas, 4, - Thickness.light.height(self.thickness), - @max(4, Thickness.light.height(self.thickness)), + Thickness.light.height(self.metrics.box_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( canvas, 4, - Thickness.heavy.height(self.thickness), - @max(4, Thickness.light.height(self.thickness)), + Thickness.heavy.height(self.metrics.box_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( canvas, 2, - Thickness.light.height(self.thickness), - Thickness.light.height(self.thickness), + Thickness.light.height(self.metrics.box_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( canvas, 2, - Thickness.heavy.height(self.thickness), - Thickness.heavy.height(self.thickness), + Thickness.heavy.height(self.metrics.box_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( canvas, 2, - Thickness.light.height(self.thickness), - Thickness.heavy.height(self.thickness), + Thickness.light.height(self.metrics.box_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( canvas, 2, - Thickness.heavy.height(self.thickness), - Thickness.heavy.height(self.thickness), + Thickness.heavy.height(self.metrics.box_thickness), + Thickness.heavy.height(self.metrics.box_thickness), ); } fn draw_light_diagonal_upper_right_to_lower_left(self: Box, canvas: *font.sprite.Canvas) void { canvas.line(.{ - .p0 = .{ .x = @floatFromInt(self.width), .y = 0 }, - .p1 = .{ .x = 0, .y = @floatFromInt(self.height) }, - }, @floatFromInt(Thickness.light.height(self.thickness)), .on) catch {}; + .p0 = .{ .x = @floatFromInt(self.metrics.cell_width), .y = 0 }, + .p1 = .{ .x = 0, .y = @floatFromInt(self.metrics.cell_height) }, + }, @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 { canvas.line(.{ .p0 = .{ .x = 0, .y = 0 }, .p1 = .{ - .x = @floatFromInt(self.width), - .y = @floatFromInt(self.height), + .x = @floatFromInt(self.metrics.cell_width), + .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 { @@ -1938,21 +1957,21 @@ fn draw_block_shade( comptime height: f64, comptime shade: Shade, ) void { - const float_width: f64 = @floatFromInt(self.width); - const float_height: f64 = @floatFromInt(self.height); + const float_width: f64 = @floatFromInt(self.metrics.cell_width); + const float_height: f64 = @floatFromInt(self.metrics.cell_height); const w: u32 = @intFromFloat(@round(float_width * width)); const h: u32 = @intFromFloat(@round(float_height * height)); const x = switch (alignment.horizontal) { .left => 0, - .right => self.width - w, - .center => (self.width - w) / 2, + .right => self.metrics.cell_width - w, + .center => (self.metrics.cell_width - w) / 2, }; const y = switch (alignment.vertical) { .top => 0, - .bottom => self.height - h, - .middle => (self.height - h) / 2, + .bottom => self.metrics.cell_height - h, + .middle => (self.metrics.cell_height - h) / 2, }; canvas.rect(.{ @@ -1970,10 +1989,10 @@ fn draw_corner_triangle_shade( comptime shade: Shade, ) void { const x0, const y0, const x1, const y1, const x2, const y2 = switch (corner) { - .tl => .{ 0, 0, 0, self.height, self.width, 0 }, - .tr => .{ 0, 0, self.width, self.height, self.width, 0 }, - .bl => .{ 0, 0, 0, self.height, self.width, self.height }, - .br => .{ 0, self.height, self.width, self.height, self.width, 0 }, + .tl => .{ 0, 0, 0, self.metrics.cell_height, self.metrics.cell_width, 0 }, + .tr => .{ 0, 0, self.metrics.cell_width, self.metrics.cell_height, self.metrics.cell_width, 0 }, + .bl => .{ 0, 0, 0, self.metrics.cell_height, self.metrics.cell_width, self.metrics.cell_height }, + .br => .{ 0, self.metrics.cell_height, self.metrics.cell_width, self.metrics.cell_height, self.metrics.cell_width, 0 }, }; canvas.triangle(.{ @@ -1984,26 +2003,26 @@ fn draw_corner_triangle_shade( } 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 { - const x = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(self.width)) / 8))); - const w = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.width)) / 8))); - self.rect(canvas, x, 0, x + w, self.height); + 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.metrics.cell_width)) / 8))); + self.rect(canvas, x, 0, x + w, self.metrics.cell_height); } fn draw_checkerboard_fill(self: Box, canvas: *font.sprite.Canvas, parity: u1) void { - const float_width: f64 = @floatFromInt(self.width); - const float_height: f64 = @floatFromInt(self.height); + const float_width: f64 = @floatFromInt(self.metrics.cell_width); + const float_height: f64 = @floatFromInt(self.metrics.cell_height); const x_size: usize = 4; const y_size: usize = @intFromFloat(@round(4 * (float_height / float_width))); for (0..x_size) |x| { - const x0 = (self.width * x) / x_size; - const x1 = (self.width * (x + 1)) / x_size; + const x0 = (self.metrics.cell_width * x) / x_size; + const x1 = (self.metrics.cell_width * (x + 1)) / x_size; for (0..y_size) |y| { - const y0 = (self.height * y) / y_size; - const y1 = (self.height * (y + 1)) / y_size; + const y0 = (self.metrics.cell_height * y) / y_size; + const y1 = (self.metrics.cell_height * (y + 1)) / y_size; if ((x + y) % 2 == parity) { canvas.rect(.{ .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 { - const thick_px = Thickness.light.height(self.thickness); - const line_count = self.width / (2 * thick_px); + const thick_px = Thickness.light.height(self.metrics.box_thickness); + const line_count = self.metrics.cell_width / (2 * thick_px); - const float_width: f64 = @floatFromInt(self.width); - const float_height: f64 = @floatFromInt(self.height); + const float_width: f64 = @floatFromInt(self.metrics.cell_width); + const float_height: f64 = @floatFromInt(self.metrics.cell_height); const float_thick: f64 = @floatFromInt(thick_px); 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 { - const thick_px = Thickness.light.height(self.thickness); - const line_count = self.width / (2 * thick_px); + const thick_px = Thickness.light.height(self.metrics.box_thickness); + const line_count = self.metrics.cell_width / (2 * thick_px); - const float_width: f64 = @floatFromInt(self.width); - const float_height: f64 = @floatFromInt(self.height); + const float_width: f64 = @floatFromInt(self.metrics.cell_width); + const float_height: f64 = @floatFromInt(self.metrics.cell_height); const float_thick: f64 = @floatFromInt(thick_px); const stride = @round(float_width / @as(f64, @floatFromInt(line_count))); @@ -2061,13 +2080,13 @@ fn draw_corner_diagonal_lines( canvas: *font.sprite.Canvas, comptime corners: Quads, ) 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_height: f64 = @floatFromInt(self.height); + const float_width: f64 = @floatFromInt(self.metrics.cell_width); + const float_height: f64 = @floatFromInt(self.metrics.cell_height); const float_thick: f64 = @floatFromInt(thick_px); - const center_x: f64 = @floatFromInt(self.width / 2 + self.width % 2); - const center_y: f64 = @floatFromInt(self.height / 2 + self.height % 2); + const center_x: f64 = @floatFromInt(self.metrics.cell_width / 2 + self.metrics.cell_width % 2); + const center_y: f64 = @floatFromInt(self.metrics.cell_height / 2 + self.metrics.cell_height % 2); if (corners.tl) canvas.line(.{ .p0 = .{ .x = center_x, .y = 0 }, @@ -2096,8 +2115,8 @@ fn draw_cell_diagonal( comptime from: Alignment, comptime to: Alignment, ) void { - const float_width: f64 = @floatFromInt(self.width); - const float_height: f64 = @floatFromInt(self.height); + const float_width: f64 = @floatFromInt(self.metrics.cell_width); + const float_height: f64 = @floatFromInt(self.metrics.cell_height); const x0: f64 = switch (from.horizontal) { .left => 0, @@ -2134,16 +2153,16 @@ fn draw_fading_line( comptime to: Edge, comptime thickness: Thickness, ) void { - const thick_px = thickness.height(self.thickness); - const float_width: f64 = @floatFromInt(self.width); - const float_height: f64 = @floatFromInt(self.height); + const thick_px = thickness.height(self.metrics.box_thickness); + const float_width: f64 = @floatFromInt(self.metrics.cell_width); + const float_height: f64 = @floatFromInt(self.metrics.cell_height); // 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 const h_bottom = h_top +| thick_px; // 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 const v_right = v_left +| thick_px; @@ -2163,7 +2182,7 @@ fn draw_fading_line( switch (to) { .top, .bottom => { - for (0..self.height) |y| { + for (0..self.metrics.cell_height) |y| { for (v_left..v_right) |x| { canvas.pixel( @intCast(x), @@ -2175,7 +2194,7 @@ fn draw_fading_line( } }, .left, .right => { - for (0..self.width) |x| { + for (0..self.metrics.cell_width) |x| { for (h_top..h_bottom) |y| { canvas.pixel( @intCast(x), @@ -2195,17 +2214,17 @@ fn draw_branch_node( node: BranchNode, comptime thickness: Thickness, ) void { - const thick_px = thickness.height(self.thickness); - const float_width: f64 = @floatFromInt(self.width); - const float_height: f64 = @floatFromInt(self.height); + const thick_px = thickness.height(self.metrics.box_thickness); + const float_width: f64 = @floatFromInt(self.metrics.cell_width); + const float_height: f64 = @floatFromInt(self.metrics.cell_height); const float_thick: f64 = @floatFromInt(thick_px); // 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 const h_bottom = h_top +| thick_px; // 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 const v_right = v_left +| thick_px; @@ -2240,9 +2259,9 @@ fn draw_branch_node( if (node.up) self.rect(canvas, v_left, 0, v_right, @intFromFloat(@ceil(cy - r))); 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) - 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) self.rect(canvas, 0, h_top, @intFromFloat(@ceil(cx - r)), h_bottom); @@ -2263,8 +2282,8 @@ fn draw_circle( comptime position: Alignment, comptime filled: bool, ) void { - const float_width: f64 = @floatFromInt(self.width); - const float_height: f64 = @floatFromInt(self.height); + const float_width: f64 = @floatFromInt(self.metrics.cell_width); + const float_height: f64 = @floatFromInt(self.metrics.cell_height); const x: f64 = switch (position.horizontal) { .left => 0, @@ -2285,7 +2304,7 @@ fn draw_circle( .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); @@ -2311,7 +2330,7 @@ fn draw_line( ) !void { canvas.line( .{ .p0 = p0, .p1 = p1 }, - @floatFromInt(thickness.height(self.thickness)), + @floatFromInt(thickness.height(self.metrics.box_thickness)), .on, ) catch {}; } @@ -2320,8 +2339,8 @@ fn draw_shade(self: Box, canvas: *font.sprite.Canvas, v: u16) void { canvas.rect((font.sprite.Box(u32){ .p0 = .{ .x = 0, .y = 0 }, .p1 = .{ - .x = self.width, - .y = self.height, + .x = self.metrics.cell_width, + .y = self.metrics.cell_height, }, }).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 { - 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( - self.height -| h, - @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(self.height)) / 8))), + self.metrics.cell_height -| h, + @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 { @@ -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 { - const center_x = self.width / 2 + self.width % 2; - const center_y = self.height / 2 + self.height % 2; + const center_x = self.metrics.cell_width / 2 + self.metrics.cell_width % 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.tr) self.rect(canvas, center_x, 0, self.width, center_y); - if (quads.bl) self.rect(canvas, 0, center_y, center_x, self.height); - if (quads.br) self.rect(canvas, center_x, center_y, self.width, self.height); + 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.metrics.cell_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 { - var w: u32 = @min(self.width / 4, self.height / 8); - var x_spacing: u32 = self.width / 4; - var y_spacing: u32 = self.height / 8; + var w: u32 = @min(self.metrics.cell_width / 4, self.metrics.cell_height / 8); + var x_spacing: u32 = self.metrics.cell_width / 4; + var y_spacing: u32 = self.metrics.cell_height / 8; var x_margin: u32 = x_spacing / 2; var y_margin: u32 = y_spacing / 2; - var x_px_left: u32 = self.width - 2 * x_margin - x_spacing - 2 * w; - var y_px_left: u32 = self.height - 2 * y_margin - 3 * y_spacing - 4 * w; + var x_px_left: u32 = self.metrics.cell_width - 2 * x_margin - x_spacing - 2 * 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 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(2 * x_margin + 2 * w + x_spacing <= self.width); - assert(2 * y_margin + 4 * w + 3 * y_spacing <= self.height); + assert(2 * x_margin + 2 * w + x_spacing <= self.metrics.cell_width); + 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 y = y: { @@ -2479,25 +2498,25 @@ fn draw_sextant(self: Box, canvas: *font.sprite.Canvas, cp: u32) void { const y_thirds = self.yThirds(); 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.mr) self.rect(canvas, x_halfs[1], y_thirds[0], self.width, y_thirds[1]); - if (sex.bl) self.rect(canvas, 0, y_thirds[1], x_halfs[0], self.height); - if (sex.br) self.rect(canvas, x_halfs[1], y_thirds[1], self.width, self.height); + 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.metrics.cell_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 { return .{ - @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.width)) / 2))), - @as(u32, @intFromFloat(@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.metrics.cell_width)) / 2)), }; } fn yThirds(self: Box) [2]u32 { - return switch (@mod(self.height, 3)) { - 0 => .{ self.height / 3, 2 * self.height / 3 }, - 1 => .{ self.height / 3, 2 * self.height / 3 + 1 }, - 2 => .{ self.height / 3 + 1, 2 * self.height / 3 }, + return switch (@mod(self.metrics.cell_height, 3)) { + 0 => .{ self.metrics.cell_height / 3, 2 * self.metrics.cell_height / 3 }, + 1 => .{ self.metrics.cell_height / 3, 2 * self.metrics.cell_height / 3 + 1 }, + 2 => .{ self.metrics.cell_height / 3 + 1, 2 * self.metrics.cell_height / 3 }, else => unreachable, }; } @@ -2511,10 +2530,10 @@ fn draw_smooth_mosaic( const top: f64 = 0.0; const upper: f64 = @floatFromInt(y_thirds[0]); 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 center: f64 = @round(@as(f64, @floatFromInt(self.width)) / 2); - const right: f64 = @floatFromInt(self.width); + const center: f64 = @round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2); + const right: f64 = @floatFromInt(self.metrics.cell_width); var path = z2d.Path.init(canvas.alloc); defer path.deinit(); @@ -2549,11 +2568,11 @@ fn draw_edge_triangle( comptime edge: Edge, ) !void { const upper: f64 = 0.0; - const middle: f64 = @round(@as(f64, @floatFromInt(self.height)) / 2); - const lower: f64 = @floatFromInt(self.height); + const middle: f64 = @round(@as(f64, @floatFromInt(self.metrics.cell_height)) / 2); + const lower: f64 = @floatFromInt(self.metrics.cell_height); const left: f64 = 0.0; - const center: f64 = @round(@as(f64, @floatFromInt(self.width)) / 2); - const right: f64 = @floatFromInt(self.width); + const center: f64 = @round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2); + const right: f64 = @floatFromInt(self.metrics.cell_width); var path = z2d.Path.init(canvas.alloc); defer path.deinit(); @@ -2588,12 +2607,12 @@ fn draw_arc( comptime corner: Corner, comptime thickness: Thickness, ) !void { - const thick_px = thickness.height(self.thickness); - const float_width: f64 = @floatFromInt(self.width); - const float_height: f64 = @floatFromInt(self.height); + const thick_px = thickness.height(self.metrics.box_thickness); + const float_width: f64 = @floatFromInt(self.metrics.cell_width); + const float_height: f64 = @floatFromInt(self.metrics.cell_height); const float_thick: f64 = @floatFromInt(thick_px); - const center_x: f64 = @as(f64, @floatFromInt((self.width -| thick_px) / 2)) + float_thick / 2; - const center_y: f64 = @as(f64, @floatFromInt((self.height -| 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.metrics.cell_height -| thick_px) / 2)) + float_thick / 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 // have that then we can't draw our dashed line correctly so we just // 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); return; } // 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. - 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_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 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. - 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 // 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 // have that then we can't draw our dashed line correctly so we just // 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); return; } // 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. - 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_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 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. - 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. var y: u32 = 0; @@ -2824,32 +2843,49 @@ fn draw_dash_vertical( } 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 { - 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); - self.vline(canvas, 0, self.height, self.width -| thick_px, thick_px); - self.hline(canvas, 0, self.width, 0, thick_px); - self.hline(canvas, 0, self.width, self.height -| thick_px, thick_px); + const thick_px = Thickness.super_light.height(self.metrics.cursor_thickness); + + self.rect(canvas, 0, 0, self.metrics.cell_width, 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 { - 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 { - const thick_px = thickness.height(self.thickness); - self.vline(canvas, 0, self.height, (self.width -| thick_px) / 2, thick_px); + const thick_px = thickness.height(self.metrics.box_thickness); + 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 { - const thick_px = thickness.height(self.thickness); - self.hline(canvas, 0, self.width, (self.height -| thick_px) / 2, thick_px); + const thick_px = thickness.height(self.metrics.box_thickness); + self.hline(canvas, 0, self.metrics.cell_width, (self.metrics.cell_height -| thick_px) / 2, thick_px); } fn vline( @@ -2861,11 +2897,11 @@ fn vline( thickness_px: u32, ) void { canvas.rect((font.sprite.Box(u32){ .p0 = .{ - .x = @min(@max(x, 0), self.width), - .y = @min(@max(y1, 0), self.height), + .x = @min(@max(x, 0), self.metrics.cell_width), + .y = @min(@max(y1, 0), self.metrics.cell_height), }, .p1 = .{ - .x = @min(@max(x + thickness_px, 0), self.width), - .y = @min(@max(y2, 0), self.height), + .x = @min(@max(x + thickness_px, 0), self.metrics.cell_width), + .y = @min(@max(y2, 0), self.metrics.cell_height), } }).rect(), .on); } @@ -2878,11 +2914,11 @@ fn hline( thickness_px: u32, ) void { canvas.rect((font.sprite.Box(u32){ .p0 = .{ - .x = @min(@max(x1, 0), self.width), - .y = @min(@max(y, 0), self.height), + .x = @min(@max(x1, 0), self.metrics.cell_width), + .y = @min(@max(y, 0), self.metrics.cell_height), }, .p1 = .{ - .x = @min(@max(x2, 0), self.width), - .y = @min(@max(y + thickness_px, 0), self.height), + .x = @min(@max(x2, 0), self.metrics.cell_width), + .y = @min(@max(y + thickness_px, 0), self.metrics.cell_height), } }).rect(), .on); } @@ -2895,11 +2931,11 @@ fn rect( y2: u32, ) void { canvas.rect((font.sprite.Box(u32){ .p0 = .{ - .x = @min(@max(x1, 0), self.width), - .y = @min(@max(y1, 0), self.height), + .x = @min(@max(x1, 0), self.metrics.cell_width), + .y = @min(@max(y1, 0), self.metrics.cell_height), }, .p1 = .{ - .x = @min(@max(x2, 0), self.width), - .y = @min(@max(y2, 0), self.height), + .x = @min(@max(x2, 0), self.metrics.cell_width), + .y = @min(@max(y2, 0), self.metrics.cell_height), } }).rect(), .on); } @@ -2913,14 +2949,21 @@ test "all" { var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale); 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( alloc, &atlas_grayscale, cp, ); - try testing.expectEqual(@as(u32, face.width), glyph.width); - try testing.expectEqual(@as(u32, face.height), glyph.height); + try testing.expectEqual(@as(u32, face.metrics.cell_width), glyph.width); + 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); defer atlas_grayscale.deinit(alloc); - // Even cell size and thickness + // Even cell size and thickness (18 x 36) try (Box{ - .width = 18, - .height = 36, - .thickness = 2, + .metrics = font.Metrics.calc(.{ + .cell_width = 18.0, + .ascent = 30.0, + .descent = -6.0, + .line_gap = 0.0, + .underline_thickness = 2.0, + .strikethrough_thickness = 2.0, + }), }).testRenderAll(alloc, &atlas_grayscale); - // Odd cell size and thickness + // Odd cell size and thickness (9 x 15) try (Box{ - .width = 9, - .height = 15, - .thickness = 1, + .metrics = font.Metrics.calc(.{ + .cell_width = 9.0, + .ascent = 12.0, + .descent = -3.0, + .line_gap = 0.0, + .underline_thickness = 1.0, + .strikethrough_thickness = 1.0, + }), }).testRenderAll(alloc, &atlas_grayscale); const ground_truth = @embedFile("./testdata/Box.ppm"); diff --git a/src/font/sprite/Face.zig b/src/font/sprite/Face.zig index e1cd12f00..b8c89c74e 100644 --- a/src/font/sprite/Face.zig +++ b/src/font/sprite/Face.zig @@ -24,22 +24,8 @@ const underline = @import("underline.zig"); const log = std.log.scoped(.font_sprite); -/// The cell width and height. -width: u32, -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, +/// Grid metrics for rendering sprites. +metrics: font.Metrics, /// Returns true if the codepoint exists in our sprite font. 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. const width = switch (opts.cell_width orelse 1) { - 0, 1 => self.width, - else => |width| self.width * width, + 0, 1 => metrics.cell_width, + else => |width| metrics.cell_width * width, }; // 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. return switch (kind) { - .box => box: { - 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; - }, + .box => (Box{ .metrics = metrics }).renderGlyph(alloc, atlas, cp), .underline => try underline.renderGlyph( alloc, atlas, @enumFromInt(cp), width, - self.height, - self.underline_position, - self.thickness, + metrics.cell_height, + metrics.underline_position, + metrics.underline_thickness, ), .strikethrough => try underline.renderGlyph( @@ -145,26 +91,34 @@ pub fn renderGlyph( atlas, @enumFromInt(cp), width, - self.height, - self.strikethrough_position, - self.thickness, + metrics.cell_height, + metrics.strikethrough_position, + metrics.strikethrough_thickness, ), - .overline => try underline.renderGlyph( - alloc, - atlas, - @enumFromInt(cp), - width, - self.height, - 0, - self.thickness, - ), + .overline => overline: { + var g = try underline.renderGlyph( + alloc, + atlas, + @enumFromInt(cp), + width, + metrics.cell_height, + 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: { const f: Powerline = .{ - .width = width, - .height = self.height, - .thickness = self.thickness, + .width = metrics.cell_width, + .height = metrics.cell_height, + .thickness = metrics.box_thickness, }; break :powerline try f.renderGlyph(alloc, atlas, cp);