mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-06-15 08:03:56 +00:00
terminal/apc: glyph glossary registration business logic
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
const Glossary = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const CircBuf = @import("../../../datastruct/circ_buf.zig").CircBuf;
|
||||
const face = @import("../../../font/face.zig");
|
||||
@@ -13,6 +14,13 @@ const glyf_rasterize = @import("../../../font/glyf_rasterize.zig");
|
||||
const request = @import("request.zig");
|
||||
const RegisterReq = request.Request.Register;
|
||||
|
||||
/// Maximum entries allowed in the glossary before eviction.
|
||||
/// Defined by the specification.
|
||||
pub const max_entries = 1024;
|
||||
|
||||
/// An empty glossary with no registered glyphs.
|
||||
pub const empty: Glossary = .{ .entries = .empty };
|
||||
|
||||
/// The set of entries in the glossary keyed by the codepoint.
|
||||
///
|
||||
/// The array hash map preserves insertion order and has O(N)
|
||||
@@ -24,7 +32,51 @@ const RegisterReq = request.Request.Register;
|
||||
/// I'm also operating under the assumption that full glossaries
|
||||
/// for a session will be rare, so the eviction cost shouldn't
|
||||
/// happen regularly.
|
||||
entries: std.AutoArrayHashMap(u21, Entry),
|
||||
entries: std.AutoArrayHashMapUnmanaged(u21, Entry),
|
||||
|
||||
/// Release all glyph entries and hash map storage owned by the glossary.
|
||||
pub fn deinit(self: *Glossary, alloc: Allocator) void {
|
||||
for (self.entries.values()) |*entry| entry.deinit(alloc);
|
||||
self.entries.deinit(alloc);
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
/// Register the given glyph entry.
|
||||
///
|
||||
/// This will act according to the glyph specification
|
||||
pub fn register(
|
||||
self: *Glossary,
|
||||
alloc: Allocator,
|
||||
cp: u21,
|
||||
entry: Entry,
|
||||
) (Allocator.Error || error{OutOfNamespace})!void {
|
||||
// Validate codepoint according to spec.
|
||||
if (!isPrivateUse(cp)) return error.OutOfNamespace;
|
||||
|
||||
const gop = try self.entries.getOrPut(alloc, cp);
|
||||
if (gop.found_existing) {
|
||||
// Found an existing entry, we need to shift the FIFO so
|
||||
// that this is now the most recent (at the end). This is
|
||||
// O(N) but N is usually small and max N is bounded by the spec.
|
||||
gop.value_ptr.*.deinit(alloc);
|
||||
assert(self.entries.orderedRemove(cp));
|
||||
|
||||
// We already had enough capacity for this key before removing it, so
|
||||
// reinserting the replacement cannot require another allocation.
|
||||
self.entries.putAssumeCapacity(cp, entry);
|
||||
return;
|
||||
}
|
||||
|
||||
// Array hash maps preserve insertion order so always immediately insert.
|
||||
gop.value_ptr.* = entry;
|
||||
|
||||
// Fast, typical path: we fit within the glossary, just return.
|
||||
if (self.entries.count() <= max_entries) return;
|
||||
|
||||
// Slow path: we need to evict.
|
||||
self.entries.values()[0].deinit(alloc);
|
||||
self.entries.orderedRemoveAt(0);
|
||||
}
|
||||
|
||||
/// A single glyph registration entry.
|
||||
pub const Entry = struct {
|
||||
@@ -55,9 +107,6 @@ pub const Entry = struct {
|
||||
/// explicitly-provided option value.
|
||||
InvalidOptions,
|
||||
|
||||
/// `cp` is not in any PUA range.
|
||||
OutOfNamespace,
|
||||
|
||||
/// The requested payload format is not supported by this glossary.
|
||||
UnsupportedFormat,
|
||||
};
|
||||
@@ -68,30 +117,26 @@ pub const Entry = struct {
|
||||
/// decodes the base64 glyph payload, and stores the decoded outline. The
|
||||
/// returned entry owns decoded glyph memory and must be released with
|
||||
/// `deinit`.
|
||||
pub fn init(alloc: Allocator, register: RegisterReq) InitError!Entry {
|
||||
// Validate codepoint
|
||||
const cp = register.get(.cp) orelse return error.InvalidOptions;
|
||||
if (!isPrivateUse(cp)) return error.OutOfNamespace;
|
||||
|
||||
pub fn init(alloc: Allocator, req: RegisterReq) InitError!Entry {
|
||||
// Validate format
|
||||
const fmt = register.get(.fmt) orelse return error.InvalidOptions;
|
||||
const fmt = req.get(.fmt) orelse return error.InvalidOptions;
|
||||
const design: glyf_rasterize.DesignMetrics = .{
|
||||
.units_per_em = register.get(.upm) orelse return error.InvalidOptions,
|
||||
.advance_width = register.get(.aw) orelse return error.InvalidOptions,
|
||||
.line_height = register.get(.lh) orelse return error.InvalidOptions,
|
||||
.units_per_em = req.get(.upm) orelse return error.InvalidOptions,
|
||||
.advance_width = req.get(.aw) orelse return error.InvalidOptions,
|
||||
.line_height = req.get(.lh) orelse return error.InvalidOptions,
|
||||
};
|
||||
if (design.units_per_em == 0 or
|
||||
design.advance_width == 0 or
|
||||
design.line_height == 0) return error.InvalidOptions;
|
||||
const width = register.get(.width) orelse return error.InvalidOptions;
|
||||
const width = req.get(.width) orelse return error.InvalidOptions;
|
||||
|
||||
// Get our constraints
|
||||
const constraint = try constraintFromRegister(register);
|
||||
const constraint = try constraintFromRegister(req);
|
||||
|
||||
// Decode the payload into some usable glyph format for
|
||||
// future rasterization.
|
||||
const glyph: Glyph = switch (fmt) {
|
||||
.glyf => .{ .glyf = try register.decodeGlyfPayload(alloc) },
|
||||
.glyf => .{ .glyf = try req.decodeGlyfPayload(alloc) },
|
||||
.colrv0, .colrv1 => return error.UnsupportedFormat,
|
||||
};
|
||||
|
||||
@@ -122,13 +167,13 @@ pub const Entry = struct {
|
||||
/// it does not have exact equivalents for every protocol size mode, so this
|
||||
/// function is the single normalization point for those policy choices.
|
||||
fn constraintFromRegister(
|
||||
register: RegisterReq,
|
||||
req: RegisterReq,
|
||||
) error{InvalidOptions}!face.RenderOptions.Constraint {
|
||||
// Register.get applies the Glyph Protocol §6.1 defaults when options
|
||||
// are omitted: size=height, align=center,center, and pad=0,0,0,0.
|
||||
const size = register.get(.size) orelse return error.InvalidOptions;
|
||||
const alignment = register.get(.@"align") orelse return error.InvalidOptions;
|
||||
const pad = register.get(.pad) orelse return error.InvalidOptions;
|
||||
const size = req.get(.size) orelse return error.InvalidOptions;
|
||||
const alignment = req.get(.@"align") orelse return error.InvalidOptions;
|
||||
const pad = req.get(.pad) orelse return error.InvalidOptions;
|
||||
|
||||
return .{
|
||||
.size = switch (size) {
|
||||
@@ -170,37 +215,60 @@ pub const Entry = struct {
|
||||
.pad_left = pad.left,
|
||||
};
|
||||
}
|
||||
|
||||
/// Return true if `cp` is in one of the Unicode Private Use Areas.
|
||||
fn isPrivateUse(cp: u21) bool {
|
||||
return (cp >= 0xE000 and cp <= 0xF8FF) or
|
||||
(cp >= 0xF0000 and cp <= 0xFFFFD) or
|
||||
(cp >= 0x100000 and cp <= 0x10FFFD);
|
||||
}
|
||||
};
|
||||
|
||||
/// Return true if `cp` is in one of the Unicode Private Use Areas.
|
||||
fn isPrivateUse(cp: u21) bool {
|
||||
return (cp >= 0xE000 and cp <= 0xF8FF) or
|
||||
(cp >= 0xF0000 and cp <= 0xFFFFD) or
|
||||
(cp >= 0x100000 and cp <= 0x10FFFD);
|
||||
}
|
||||
|
||||
fn testParseRegister(alloc: Allocator, data: []const u8) !RegisterReq {
|
||||
const raw = try alloc.dupe(u8, data);
|
||||
errdefer alloc.free(raw);
|
||||
|
||||
const req = try request.Request.parse(alloc, raw);
|
||||
switch (req) {
|
||||
.register => |register| return register,
|
||||
.register => |reg| return reg,
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
// Base64-encoded glyf payload from the "glyf: decode triangle" test in
|
||||
// font/opentype/glyf.zig. This is a real simple-glyph record with one contour
|
||||
// and three on-curve points.
|
||||
const test_triangle_glyf_payload = "AAEAZABkA4QDhAACAAABAQEB9P5wAyADhPzgAAA=";
|
||||
|
||||
fn testRegisterReq(alloc: Allocator, cp: u21) !RegisterReq {
|
||||
const data = try std.fmt.allocPrint(
|
||||
alloc,
|
||||
"r;cp={x};upm=2048;aw=1024;lh=1536;width=2;size=stretch;align=end,start;pad=0.1,0.2,0.3,0.4;{s}",
|
||||
.{ cp, test_triangle_glyf_payload },
|
||||
);
|
||||
errdefer alloc.free(data);
|
||||
|
||||
const req = try request.Request.parse(alloc, data);
|
||||
switch (req) {
|
||||
.register => |reg| return reg,
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
fn testRegisterEntry(alloc: Allocator, cp: u21) !Entry {
|
||||
const req = try testRegisterReq(alloc, cp);
|
||||
defer alloc.free(req.raw);
|
||||
return try Entry.init(alloc, req);
|
||||
}
|
||||
|
||||
test "Entry init decodes glyf payload and applies register fields" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const register = try testParseRegister(
|
||||
alloc,
|
||||
"r;cp=e000;upm=2048;aw=1024;lh=1536;width=2;size=stretch;align=end,start;pad=0.1,0.2,0.3,0.4;AAAAAAAAAAAAAA==",
|
||||
);
|
||||
defer alloc.free(register.raw);
|
||||
const req = try testRegisterReq(alloc, 0xE000);
|
||||
defer alloc.free(req.raw);
|
||||
|
||||
var entry = try Entry.init(alloc, register);
|
||||
var entry = try Entry.init(alloc, req);
|
||||
defer entry.deinit(alloc);
|
||||
|
||||
try testing.expectEqual(@as(u32, 2048), entry.design.units_per_em);
|
||||
@@ -215,16 +283,63 @@ test "Entry init decodes glyf payload and applies register fields" {
|
||||
try testing.expectEqual(@as(f64, 0.3), entry.constraint.pad_bottom);
|
||||
try testing.expectEqual(@as(f64, 0.4), entry.constraint.pad_left);
|
||||
|
||||
try testing.expectEqual(@as(usize, 0), entry.glyph.glyf.points.len);
|
||||
try testing.expectEqual(@as(usize, 0), entry.glyph.glyf.contours.len);
|
||||
try testing.expectEqual(@as(usize, 3), entry.glyph.glyf.points.len);
|
||||
try testing.expectEqual(@as(usize, 1), entry.glyph.glyf.contours.len);
|
||||
}
|
||||
|
||||
test "Entry init rejects invalid register payload" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const register = try testParseRegister(alloc, "r;cp=e000;%%%not-base64%%%");
|
||||
defer alloc.free(register.raw);
|
||||
const req = try testParseRegister(alloc, "r;cp=e000;%%%not-base64%%%");
|
||||
defer alloc.free(req.raw);
|
||||
|
||||
try testing.expectError(error.MalformedPayload, Entry.init(alloc, register));
|
||||
try testing.expectError(error.MalformedPayload, Entry.init(alloc, req));
|
||||
}
|
||||
|
||||
test "Glossary register overwrites and moves entry to newest position" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var glossary: Glossary = .empty;
|
||||
defer glossary.deinit(alloc);
|
||||
|
||||
try glossary.register(alloc, 0xE000, try testRegisterEntry(alloc, 0xE000));
|
||||
try glossary.register(alloc, 0xE001, try testRegisterEntry(alloc, 0xE001));
|
||||
try glossary.register(alloc, 0xE000, try testRegisterEntry(alloc, 0xE000));
|
||||
|
||||
try testing.expectEqual(@as(usize, 2), glossary.entries.count());
|
||||
try testing.expectEqual(@as(u21, 0xE001), glossary.entries.keys()[0]);
|
||||
try testing.expectEqual(@as(u21, 0xE000), glossary.entries.keys()[1]);
|
||||
}
|
||||
|
||||
test "Glossary register evicts oldest entry" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var glossary: Glossary = .empty;
|
||||
defer glossary.deinit(alloc);
|
||||
|
||||
for (0..max_entries + 1) |i| {
|
||||
const cp: u21 = @intCast(0xE000 + i);
|
||||
try glossary.register(alloc, cp, try testRegisterEntry(alloc, cp));
|
||||
}
|
||||
|
||||
try testing.expectEqual(@as(usize, max_entries), glossary.entries.count());
|
||||
try testing.expect(!glossary.entries.contains(0xE000));
|
||||
try testing.expect(glossary.entries.contains(0xE001));
|
||||
try testing.expect(glossary.entries.contains(0xE000 + max_entries));
|
||||
}
|
||||
|
||||
test "Glossary register rejects non-PUA codepoint" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var glossary: Glossary = .empty;
|
||||
defer glossary.deinit(alloc);
|
||||
|
||||
var entry = try testRegisterEntry(alloc, 0xE000);
|
||||
errdefer entry.deinit(alloc);
|
||||
|
||||
try testing.expectError(error.OutOfNamespace, glossary.register(alloc, 'A', entry));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user