diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-07 18:29:21 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-07 18:29:21 +0300 |
| commit | dcfe11a7588fcd42471159453810abc9b7720a57 (patch) | |
| tree | c32a77266028e73ca2382b96c96f8438a5fcd621 /vendor/libghostty-vt/src/font | |
| parent | 2dfed860103c4e4505ac7f25def9fe37da1e1e6a (diff) | |
build: update libghostty-vt vendor
vendor ghostty 063ac3e and remove the musl libc++ link workaround.
expose the new selection formatter and hyperlink uri apis for follow-up rendering work.
add manual artifact validation for static linux builds.
Diffstat (limited to 'vendor/libghostty-vt/src/font')
| -rw-r--r-- | vendor/libghostty-vt/src/font/DeferredFace.zig | 71 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/font/SharedGridSet.zig | 2 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/font/backend.zig | 18 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/font/discovery.zig | 314 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/font/face.zig | 1 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/font/library.zig | 1 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/font/opentype.zig | 2 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/font/opentype/glyf.zig | 585 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/font/shape.zig | 1 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/font/shaper/coretext.zig | 4 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/font/shaper/harfbuzz.zig | 4 |
11 files changed, 985 insertions, 18 deletions
diff --git a/vendor/libghostty-vt/src/font/DeferredFace.zig b/vendor/libghostty-vt/src/font/DeferredFace.zig index e818cca3..425f3c28 100644 --- a/vendor/libghostty-vt/src/font/DeferredFace.zig +++ b/vendor/libghostty-vt/src/font/DeferredFace.zig @@ -26,6 +26,10 @@ fc: if (options.backend == .fontconfig_freetype) ?Fontconfig else void = ct: if (font.Discover == font.discovery.CoreText) ?CoreText else void = if (font.Discover == font.discovery.CoreText) null else {}, +/// Windows (FreeType directory scan) +win: if (options.backend == .freetype_windows) ?Windows else void = + if (options.backend == .freetype_windows) null else {}, + /// Canvas wc: if (options.backend == .web_canvas) ?WebCanvas else void = if (options.backend == .web_canvas) null else {}, @@ -51,6 +55,42 @@ pub const Fontconfig = struct { } }; +/// Windows specific data. Only present with the freetype_windows backend. +/// +/// Unlike Fontconfig/CoreText which carry lightweight descriptor handles, +/// the Windows backend has no external descriptor service — the "deferred" +/// metadata is the FreeType face itself. We keep a pre-loaded face (loaded +/// at discovery time) to answer `hasCodepoint` cheaply without re-opening +/// the file on every query, and remember the path so `load()` can open a +/// fresh face at the caller's requested size/options. +pub const Windows = struct { + /// Path to the font file. Owned here. + path: [:0]const u8, + + /// Face index within the file (for .ttc collections). + face_index: i32, + + /// Variations to apply on load. + variations: []const font.face.Variation, + + /// Pre-loaded face used for cheap metadata queries (glyphIndex, + /// hasColor). The size it was opened at is irrelevant for these + /// queries since the CMap is size-independent. Deinit'd with us. + peek: Face, + + /// Whether the face presents as emoji (has color glyphs) or text. + presentation: Presentation, + + /// Allocator that owns `path`. + alloc: Allocator, + + pub fn deinit(self: *Windows) void { + self.peek.deinit(); + self.alloc.free(self.path); + self.* = undefined; + } +}; + /// CoreText specific data. This is only present when building with CoreText. pub const CoreText = struct { /// The initialized font @@ -88,6 +128,7 @@ pub fn deinit(self: *DeferredFace) void { switch (options.backend) { .fontconfig_freetype => if (self.fc) |*fc| fc.deinit(), .freetype => {}, + .freetype_windows => if (self.win) |*w| w.deinit(), .web_canvas => if (self.wc) |*wc| wc.deinit(), .coretext, .coretext_freetype, @@ -103,6 +144,8 @@ pub fn familyName(self: DeferredFace, buf: []u8) ![]const u8 { switch (options.backend) { .freetype => {}, + .freetype_windows => if (self.win) |w| return try w.peek.name(buf), + .fontconfig_freetype => if (self.fc) |fc| return (try fc.pattern.get(.family, 0)).string, @@ -131,6 +174,8 @@ pub fn name(self: DeferredFace, buf: []u8) ![]const u8 { switch (options.backend) { .freetype => {}, + .freetype_windows => if (self.win) |w| return try w.peek.name(buf), + .fontconfig_freetype => if (self.fc) |fc| return (try fc.pattern.get(.fullname, 0)).string, @@ -164,6 +209,7 @@ pub fn load( ) !Face { return switch (options.backend) { .fontconfig_freetype => try self.loadFontconfig(lib, opts), + .freetype_windows => try self.loadWindows(lib, opts), .coretext, .coretext_harfbuzz, .coretext_noshape => try self.loadCoreText(lib, opts), .coretext_freetype => try self.loadCoreTextFreetype(lib, opts), .web_canvas => try self.loadWebCanvas(opts), @@ -191,6 +237,19 @@ fn loadFontconfig( return face; } +fn loadWindows( + self: *DeferredFace, + lib: Library, + opts: font.face.Options, +) !Face { + const w = self.win.?; + + var face = try Face.initFile(lib, w.path, w.face_index, opts); + errdefer face.deinit(); + try face.setVariations(w.variations, opts); + return face; +} + fn loadCoreText( self: *DeferredFace, lib: Library, @@ -287,6 +346,14 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool { } }, + .freetype_windows => { + // Use the pre-loaded peek face for a cheap CMap lookup. + if (self.win) |w| { + if (p) |desired| if (w.presentation != desired) return false; + return w.peek.glyphIndex(cp) != null; + } + }, + .coretext, .coretext_freetype, .coretext_harfbuzz, @@ -411,7 +478,7 @@ test "fontconfig" { // Get a deferred face from fontconfig var def = def: { - var fc = discovery.Fontconfig.init(); + var fc = discovery.Fontconfig.init(lib); defer fc.deinit(); var it = try fc.discover(alloc, .{ .family = "monospace", .size = 12 }); defer it.deinit(); @@ -443,7 +510,7 @@ test "coretext" { // Get a deferred face from fontconfig var def = def: { - var fc = discovery.CoreText.init(); + var fc = discovery.CoreText.init(lib); var it = try fc.discover(alloc, .{ .family = "Monaco", .size = 12 }); defer it.deinit(); break :def (try it.next()).?; diff --git a/vendor/libghostty-vt/src/font/SharedGridSet.zig b/vendor/libghostty-vt/src/font/SharedGridSet.zig index b832139b..9d8148bd 100644 --- a/vendor/libghostty-vt/src/font/SharedGridSet.zig +++ b/vendor/libghostty-vt/src/font/SharedGridSet.zig @@ -442,7 +442,7 @@ fn discover(self: *SharedGridSet) !?*Discover { // If we initialized, use it if (self.font_discover) |*v| return v; - self.font_discover = .init(); + self.font_discover = .init(self.font_lib); return &self.font_discover.?; } diff --git a/vendor/libghostty-vt/src/font/backend.zig b/vendor/libghostty-vt/src/font/backend.zig index d24edec7..867421de 100644 --- a/vendor/libghostty-vt/src/font/backend.zig +++ b/vendor/libghostty-vt/src/font/backend.zig @@ -6,6 +6,12 @@ pub const Backend = enum { /// FreeType for font rendering with no font discovery enabled. freetype, + /// FreeType for font rendering with a built-in Windows font directory + /// scanner (C:\Windows\Fonts + %LOCALAPPDATA%\Microsoft\Windows\Fonts). + /// Used when DirectWrite is not available; matches by family_name and + /// SFNT name table without any external index. + freetype_windows, + /// Fontconfig for font discovery and FreeType for font rendering. fontconfig_freetype, @@ -42,10 +48,10 @@ pub const Backend = enum { if (target.os.tag == .windows) { // Avoid fontconfig on Windows because its libxml2 dependency - // may not unpack due to symlinks. Use plain freetype for now - // which means no font discovery. Full solution would likely use - // DirectWrite which has its own discovery API. - return .freetype; + // may not unpack due to symlinks. Use the FreeType-based + // Windows font-directory scanner for discovery. A future + // DirectWrite backend can replace this if needed. + return .freetype_windows; } // macOS also supports "coretext_freetype" but there is no scenario @@ -60,6 +66,7 @@ pub const Backend = enum { pub fn hasFreetype(self: Backend) bool { return switch (self) { .freetype, + .freetype_windows, .fontconfig_freetype, .coretext_freetype, => true, @@ -81,6 +88,7 @@ pub const Backend = enum { => true, .freetype, + .freetype_windows, .fontconfig_freetype, .web_canvas, => false, @@ -92,6 +100,7 @@ pub const Backend = enum { .fontconfig_freetype => true, .freetype, + .freetype_windows, .coretext, .coretext_freetype, .coretext_harfbuzz, @@ -104,6 +113,7 @@ pub const Backend = enum { pub fn hasHarfbuzz(self: Backend) bool { return switch (self) { .freetype, + .freetype_windows, .fontconfig_freetype, .coretext_freetype, .coretext_harfbuzz, diff --git a/vendor/libghostty-vt/src/font/discovery.zig b/vendor/libghostty-vt/src/font/discovery.zig index c419d36a..b945aa01 100644 --- a/vendor/libghostty-vt/src/font/discovery.zig +++ b/vendor/libghostty-vt/src/font/discovery.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const assert = @import("../quirks.zig").inlineAssert; const fontconfig = @import("fontconfig"); @@ -7,6 +8,9 @@ const opentype = @import("opentype.zig"); const options = @import("main.zig").options; const Collection = @import("main.zig").Collection; const DeferredFace = @import("main.zig").DeferredFace; +const Face = @import("main.zig").Face; +const Library = @import("main.zig").Library; +const Presentation = @import("main.zig").Presentation; const Variation = @import("main.zig").face.Variation; const log = std.log.scoped(.discovery); @@ -14,6 +18,7 @@ const log = std.log.scoped(.discovery); /// Discover implementation for the compile options. pub const Discover = switch (options.backend) { .freetype => void, // no discovery + .freetype_windows => Windows, .fontconfig_freetype => Fontconfig, .web_canvas => void, // no discovery .coretext, @@ -242,7 +247,8 @@ pub const Descriptor = struct { pub const Fontconfig = struct { fc_config: *fontconfig.Config, - pub fn init() Fontconfig { + pub fn init(lib: Library) Fontconfig { + _ = lib; // safe to call multiple times and concurrently _ = fontconfig.init(); return .{ .fc_config = fontconfig.initLoadConfigAndFonts() }; @@ -333,7 +339,8 @@ pub const Fontconfig = struct { }; pub const CoreText = struct { - pub fn init() CoreText { + pub fn init(lib: Library) CoreText { + _ = lib; // Required for the "interface" but does nothing for CoreText. return .{}; } @@ -879,6 +886,263 @@ pub const CoreText = struct { }; }; +/// Windows font discovery. Enumerates font files in the system and +/// per-user font directories and matches them to a descriptor via +/// FreeType's family_name field (with a fallback to the SFNT name +/// table when family_name is missing). +/// +/// No external service is used; each discover() call walks the +/// directories, opening candidate files with FreeType only as needed. +/// For typical Windows installations (~300 fonts) a name query is in +/// the tens of milliseconds. A codepoint fallback query may be +/// noticeably slower because every candidate has to be opened to +/// probe its CMap. +pub const Windows = struct { + lib: Library, + + pub fn init(lib: Library) Windows { + return .{ .lib = lib }; + } + + pub fn deinit(self: *Windows) void { + _ = self; + } + + pub fn discover( + self: *const Windows, + alloc: Allocator, + desc: Descriptor, + ) !DiscoverIterator { + return .{ + .alloc = alloc, + .lib = self.lib, + .desc = desc, + .variations = desc.variations, + .state = .system, + .dir = null, + .iter = null, + .system_path = null, + .user_path = null, + }; + } + + pub fn discoverFallback( + self: *const Windows, + alloc: Allocator, + collection: *Collection, + desc: Descriptor, + ) !DiscoverIterator { + _ = collection; + return self.discover(alloc, desc); + } + + pub const DiscoverIterator = struct { + alloc: Allocator, + lib: Library, + desc: Descriptor, + variations: []const Variation, + state: State, + dir: ?std.fs.Dir, + iter: ?std.fs.Dir.Iterator, + system_path: ?[:0]const u8, + user_path: ?[:0]const u8, + + const State = enum { system, user, done }; + + pub fn deinit(self: *DiscoverIterator) void { + if (self.dir) |*d| d.close(); + if (self.system_path) |p| self.alloc.free(p); + if (self.user_path) |p| self.alloc.free(p); + self.* = undefined; + } + + pub fn next(self: *DiscoverIterator) !?DeferredFace { + while (true) { + // Ensure we have a directory iterator for the current state. + if (self.iter == null) { + switch (self.state) { + .system => { + const path = self.systemFontsPath() orelse { + self.state = .user; + continue; + }; + self.system_path = path; + self.dir = std.fs.openDirAbsoluteZ( + path, + .{ .iterate = true }, + ) catch { + self.state = .user; + continue; + }; + self.iter = self.dir.?.iterate(); + }, + .user => { + const path = self.userFontsPath() orelse { + self.state = .done; + continue; + }; + self.user_path = path; + self.dir = std.fs.openDirAbsoluteZ( + path, + .{ .iterate = true }, + ) catch { + self.state = .done; + continue; + }; + self.iter = self.dir.?.iterate(); + }, + .done => return null, + } + } + + const entry = (self.iter.?.next() catch null) orelse { + // Finished this directory; advance state. + if (self.dir) |*d| d.close(); + self.dir = null; + self.iter = null; + self.state = switch (self.state) { + .system => .user, + .user => .done, + .done => .done, + }; + continue; + }; + + if (entry.kind != .file) continue; + if (!isFontFile(entry.name)) continue; + + if (try self.tryMatch(entry.name)) |face| return face; + } + } + + /// Build the system fonts directory from %SYSTEMROOT%. Returns null + /// if SYSTEMROOT is unset, which shouldn't happen on a healthy + /// Windows install but we just skip the directory rather than + /// falling back to a hardcoded drive letter. + fn systemFontsPath(self: *DiscoverIterator) ?[:0]const u8 { + const systemroot = std.process.getEnvVarOwned( + self.alloc, + "SYSTEMROOT", + ) catch return null; + defer self.alloc.free(systemroot); + return std.fmt.allocPrintSentinel( + self.alloc, + "{s}\\Fonts", + .{systemroot}, + 0, + ) catch null; + } + + fn userFontsPath(self: *DiscoverIterator) ?[:0]const u8 { + const local_appdata = std.process.getEnvVarOwned( + self.alloc, + "LOCALAPPDATA", + ) catch return null; + defer self.alloc.free(local_appdata); + return std.fmt.allocPrintSentinel( + self.alloc, + "{s}\\Microsoft\\Windows\\Fonts", + .{local_appdata}, + 0, + ) catch null; + } + + fn tryMatch( + self: *DiscoverIterator, + name: []const u8, + ) !?DeferredFace { + const dir_path = switch (self.state) { + .system => self.system_path.?, + .user => self.user_path.?, + .done => return null, + }; + + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + const full_path = std.fmt.bufPrintZ( + &path_buf, + "{s}\\{s}", + .{ dir_path, name }, + ) catch return null; + + const is_ttc = std.ascii.endsWithIgnoreCase(name, ".ttc"); + const max_faces: i32 = if (is_ttc) 16 else 1; + + // Probe each face in the file. + var face_index: i32 = 0; + while (face_index < max_faces) : (face_index += 1) { + var face = Face.initFile( + self.lib, + full_path, + face_index, + .{ .size = .{ .points = 12 } }, + ) catch break; + + if (self.matches(&face)) { + return try self.makeDeferred(face, full_path, face_index); + } + + face.deinit(); + } + + return null; + } + + /// Check whether the given face matches the descriptor. + fn matches(self: *const DiscoverIterator, face: *Face) bool { + if (self.desc.family) |family| { + if (!familyMatches(face, family)) return false; + } + if (self.desc.codepoint != 0) { + if (face.glyphIndex(self.desc.codepoint) == null) return false; + } + return true; + } + + fn makeDeferred( + self: *DiscoverIterator, + face: Face, + full_path: []const u8, + face_index: i32, + ) !DeferredFace { + const path_owned = try self.alloc.dupeZ(u8, full_path); + errdefer self.alloc.free(path_owned); + + const presentation: Presentation = + if (face.hasColor()) .emoji else .text; + + return DeferredFace{ + .win = .{ + .path = path_owned, + .face_index = face_index, + .variations = self.variations, + .peek = face, + .presentation = presentation, + .alloc = self.alloc, + }, + }; + } + }; + + fn isFontFile(name: []const u8) bool { + return std.ascii.endsWithIgnoreCase(name, ".ttf") or + std.ascii.endsWithIgnoreCase(name, ".ttc") or + std.ascii.endsWithIgnoreCase(name, ".otf"); + } + + /// Compare a face's family against a requested family name. Checks + /// FreeType's family_name first, then falls back to the SFNT name + /// table entry. + fn familyMatches(face: *Face, family: [:0]const u8) bool { + const ft_family: ?[*:0]const u8 = face.face.handle.*.family_name; + if (ft_family) |f| { + if (std.ascii.eqlIgnoreCase(std.mem.span(f), family)) return true; + } + var buf: [256]u8 = undefined; + const sfnt = face.name(&buf) catch ""; + return sfnt.len > 0 and std.ascii.eqlIgnoreCase(sfnt, family); + } +}; + test "descriptor hash" { const testing = std.testing; @@ -900,7 +1164,10 @@ test "fontconfig" { const testing = std.testing; const alloc = testing.allocator; - var fc = Fontconfig.init(); + var lib = try Library.init(alloc); + defer lib.deinit(); + + var fc = Fontconfig.init(lib); defer fc.deinit(); var it = try fc.discover(alloc, .{ .family = "monospace", .size = 12 }); defer it.deinit(); @@ -912,7 +1179,10 @@ test "fontconfig codepoint" { const testing = std.testing; const alloc = testing.allocator; - var fc = Fontconfig.init(); + var lib = try Library.init(alloc); + defer lib.deinit(); + + var fc = Fontconfig.init(lib); defer fc.deinit(); var it = try fc.discover(alloc, .{ .codepoint = 'A', .size = 12 }); defer it.deinit(); @@ -934,7 +1204,10 @@ test "coretext" { const testing = std.testing; const alloc = testing.allocator; - var ct = CoreText.init(); + var lib = try Library.init(alloc); + defer lib.deinit(); + + var ct = CoreText.init(lib); defer ct.deinit(); var it = try ct.discover(alloc, .{ .family = "Monaco", .size = 12 }); defer it.deinit(); @@ -952,7 +1225,10 @@ test "coretext codepoint" { const testing = std.testing; const alloc = testing.allocator; - var ct = CoreText.init(); + var lib = try Library.init(alloc); + defer lib.deinit(); + + var ct = CoreText.init(lib); defer ct.deinit(); var it = try ct.discover(alloc, .{ .codepoint = 'A', .size = 12 }); defer it.deinit(); @@ -981,7 +1257,10 @@ test "coretext sorting" { const testing = std.testing; const alloc = testing.allocator; - var ct = CoreText.init(); + var lib = try Library.init(alloc); + defer lib.deinit(); + + var ct = CoreText.init(lib); defer ct.deinit(); // We try to get a Regular, Italic, Bold, & Bold Italic version of SF Pro, @@ -1047,3 +1326,24 @@ test "coretext sorting" { try testing.expectEqualStrings("SF Pro Bold Italic", name); } } + +test "windows" { + if (options.backend != .freetype_windows) return error.SkipZigTest; + + const testing = std.testing; + const alloc = testing.allocator; + + var lib = try Library.init(alloc); + defer lib.deinit(); + + var win = Windows.init(lib); + defer win.deinit(); + + // Arial ships on every stock Windows install. + var it = try win.discover(alloc, .{ .family = "Arial", .size = 12 }); + defer it.deinit(); + + var face = (try it.next()) orelse return error.TestFontNotFound; + defer face.deinit(); + try testing.expect(face.hasCodepoint('A', null)); +} diff --git a/vendor/libghostty-vt/src/font/face.zig b/vendor/libghostty-vt/src/font/face.zig index a1312c45..d77253ad 100644 --- a/vendor/libghostty-vt/src/font/face.zig +++ b/vendor/libghostty-vt/src/font/face.zig @@ -11,6 +11,7 @@ pub const web_canvas = @import("face/web_canvas.zig"); /// Face implementation for the compile options. pub const Face = switch (options.backend) { .freetype, + .freetype_windows, .fontconfig_freetype, .coretext_freetype, => freetype.Face, diff --git a/vendor/libghostty-vt/src/font/library.zig b/vendor/libghostty-vt/src/font/library.zig index dce6dbd5..56946343 100644 --- a/vendor/libghostty-vt/src/font/library.zig +++ b/vendor/libghostty-vt/src/font/library.zig @@ -10,6 +10,7 @@ const font = @import("main.zig"); pub const Library = switch (options.backend) { // Freetype requires a state library .freetype, + .freetype_windows, .fontconfig_freetype, .coretext_freetype, => FreetypeLibrary, diff --git a/vendor/libghostty-vt/src/font/opentype.zig b/vendor/libghostty-vt/src/font/opentype.zig index dd02efeb..0cba9225 100644 --- a/vendor/libghostty-vt/src/font/opentype.zig +++ b/vendor/libghostty-vt/src/font/opentype.zig @@ -5,12 +5,14 @@ const os2 = @import("opentype/os2.zig"); const post = @import("opentype/post.zig"); const hhea = @import("opentype/hhea.zig"); const head = @import("opentype/head.zig"); +const glyf = @import("opentype/glyf.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; +pub const Glyf = glyf.Glyf; test { @import("std").testing.refAllDecls(@This()); diff --git a/vendor/libghostty-vt/src/font/opentype/glyf.zig b/vendor/libghostty-vt/src/font/opentype/glyf.zig new file mode 100644 index 00000000..7392729a --- /dev/null +++ b/vendor/libghostty-vt/src/font/opentype/glyf.zig @@ -0,0 +1,585 @@ +const std = @import("std"); +const sfnt = @import("sfnt.zig"); + +/// Glyph Data Table +/// +/// This takes a little bit of a different form than other tables that we +/// have parsers for. Due to the fact that this table contains arrays of +/// arbitrary length, we store a pointer (slice) to the underlying data, +/// and then have functions for getting and interpreting specific parts. +/// +/// References: +/// - https://learn.microsoft.com/en-us/typography/opentype/spec/glyf +/// +/// Field names are in camelCase to match names in spec. +pub const Glyf = struct { + data: []const u8, + + /// https://learn.microsoft.com/en-us/typography/opentype/spec/glyf#table-organization + pub const Entry = struct { + header: Header, + + /// We store a reference to the original bytes so that we can + /// validate or iterate the contours or components of the glyph. + /// + /// This data starts immediately after the header. + data: []const u8, + + /// The header that's always present at + /// the start of any glyph in the table. + /// + /// Depending on the number of contours, the data that + /// comes afterwards must be interpreted differently. + /// + /// References: + /// - https://learn.microsoft.com/en-us/typography/opentype/spec/glyf#glyph-headers + pub const Header = extern struct { + /// If the number of contours is greater than + /// or equal to zero, this is a simple glyph. + /// + /// If negative, this is a composite glyph — the + /// value -1 should be used for composite glyphs. + numberOfContours: sfnt.int16 align(1), + + /// Minimum x for coordinate data. + xMin: sfnt.int16 align(1), + + /// Minimum y for coordinate data. + yMin: sfnt.int16 align(1), + + /// Maximum x for coordinate data. + xMax: sfnt.int16 align(1), + + /// Maximum y for coordinate data. + yMax: sfnt.int16 align(1), + }; + + /// The bit flags that describe the point data in simple glyph entries. + /// + /// Doc strings for each field are copied with minimal modification + /// from the opentype spec. Field names are altered to be clearer and + /// more succinct, and mentions of those field names in doc strings + /// have been similarly modified to match the ones in the struct. + /// + /// The relationship between the <x|y>_short and <x|y>_repeat_or_sign + /// fields is important, and poorly explained in prose, so instead of + /// that, here's a table that should make it easier to understand. + /// + /// x_short > | false | true | + /// x_repeat_or_sign V |------------------|------------------| + /// | The x-coordinate | The x-coordinate | + /// false | of this point is | of this point is | + /// | a signed 16-bit | an unsigned byte | + /// | value added to | value treated as | + /// | the *Coordinates | negative, added | + /// | array. | to the array of | + /// | | xCoordinates. | + /// -------------------|------------------|------------------| + /// | The x-coordinate | The x-coordinate | + /// true | of this point is | of this point is | + /// | the same as the | an unsigned byte | + /// | previous point; | value treated as | + /// | nothing added to | positive, added | + /// | the xCoordinates | to the array of | + /// | array. | xCoordinates. | + /// -------------------|------------------|------------------| + /// + /// References: + /// - https://learn.microsoft.com/en-us/typography/opentype/spec/glyf#simple-glyph-description + pub const SimpleFlags = packed struct(u8) { + /// If set, the point is on the curve; otherwise, it is off the curve. + on_curve: bool, + + /// If set, the corresponding x-coordinate is 1 byte long, + /// and the sign is determined by the x_repeat_or_sign flag. + /// + /// If not set, its interpretation depends on the x_repeat_or_sign + /// flag: If that other flag is set, the x-coordinate is the same + /// as the previous x-coordinate, and no element is added to the + /// xCoordinates array. If both flags are not set, the corresponding + /// element in the xCoordinates array is two bytes and interpreted + /// as a signed integer. + /// + /// See the description of the x_repeat_or_sign flag for additional + /// information. + x_short: bool, + + /// If set, the corresponding y-coordinate is 1 byte long, + /// and the sign is determined by the y_repeat_or_sign flag. + /// + /// If not set, its interpretation depends on the y_repeat_or_sign + /// flag: If that other flag is set, the y-coordinate is the same + /// as the previous y-coordinate, and no element is added to the + /// yCoordinates array. If both flags are not set, the corresponding + /// element in the yCoordinates array is two bytes and interpreted + /// as a signed integer. + /// + /// See the description of the y_repeat_or_sign flag for additional + /// information. + y_short: bool, + + /// If set, the next byte (read as unsigned) specifies the number + /// of additional times this flag byte is to be repeated in the + /// logical flags array — that is, the number of additional logical + /// flag entries inserted after this entry. (In the expanded logical + /// array, this bit is ignored.) In this way, the number of flags + /// listed can be smaller than the number of points in the glyph + /// description. + repeat: bool, + + /// This flag has two meanings, depending on how the x_short flag + /// is set. If x_short is set, this bit describes the sign of the + /// value, with 1 equaling positive and 0 negative. If x_short is + /// not set and this bit is set, then the current x-coordinate is + /// the same as the previous x-coordinate. If x_short is not set + /// and this bit is also not set, the current x-coordinate is a + /// signed 16-bit delta vector. + x_repeat_or_sign: bool, + + /// This flag has two meanings, depending on how the y_short flag + /// is set. If y_short is set, this bit describes the sign of the + /// value, with 1 equaling positive and 0 negative. If y_short is + /// not set and this bit is set, then the current y-coordinate is + /// the same as the previous y-coordinate. If y_short is not set + /// and this bit is also not set, the current y-coordinate is a + /// signed 16-bit delta vector. + y_repeat_or_sign: bool, + + /// If set, contours in the glyph description could overlap. + /// + /// Use of this flag is not required — that is, contours may + /// overlap without having this flag set. When used, it must + /// be set on the first flag byte for the glyph. + overlap: bool, + + /// Bit 7 is reserved: set to zero. + reserved: bool, + + /// Determine the size (in bytes) of the corresponding + /// value in the `xCoordinates` array for this flagset. + /// + /// See doc comments on the struct for an explanation. + pub inline fn xBytes(self: SimpleFlags) u2 { + return if (self.x_short) + // short, 1 byte + 1 + else if (self.x_repeat_or_sign) + // repeat, 0 bytes + 0 + else + // otherwise, 16-bit, 2 bytes. + 2; + } + + /// Determine the size (in bytes) of the corresponding + /// value in the `yCoordinates` array for this flagset. + /// + /// See doc comments on the struct for an explanation. + pub inline fn yBytes(self: SimpleFlags) u2 { + return if (self.y_short) + // short, 1 byte + 1 + else if (self.y_repeat_or_sign) + // repeat, 0 bytes + 0 + else + // otherwise, 16-bit, 2 bytes. + 2; + } + }; + + pub const Type = enum { + /// A glyph made of standard contours. + simple, + /// A glyph made of references to other glyphs. + composite, + }; + + /// Initialize an entry from the provided data. + /// + /// This DOES NOT COPY the data, it only stores a pointer to it. + /// + /// The lifetime of this struct, then, is the same as the + /// lifetime of the data that is used to initialize it. + pub fn init(data: []const u8) error{EndOfStream}!Entry { + var fbs = std.io.fixedBufferStream(data); + const reader = fbs.reader(); + const header = try reader.readStructEndian(Header, .big); + return .{ .header = header, .data = data[fbs.pos..] }; + } + + /// Identifies what type (simple or composite) of entry this is. + pub fn entryType(self: Entry) Type { + return if (self.header.numberOfContours >= 0) + .simple + else + .composite; + } + + /// Errors that can be returned from `Entry.size()`. + pub const SizeError = error{ + /// The entry's data wasn't large enough, ran + /// out of bytes before we were done reading. + EndOfStream, + + /// The entry contains hinting instructions, + /// which we don't currently support. + InstructionsNotSupported, + + /// The entry is a composite glyph, + /// which we don't currently support. + CompositeNotSupported, + + /// The elements of the end points array + /// must strictly monotonically increase. + /// + /// This error means the provided entry violated that. + EndPointsOutOfOrder, + + /// This entry defines points past the index determined + /// by the final element of the endPtsOfContours array. + TooManyPoints, + }; + + /// Determines the size (in bytes) of this entry. + /// + /// If the entry is valid, returns the number of bytes + /// taken up by this entry, including its header. + /// + /// NOTE: Currently produces errors when given composite glyphs + /// or any glyphs that have hinting instructions included. + pub fn size(self: Entry) SizeError!usize { + var fbs = std.io.fixedBufferStream(self.data); + const reader = fbs.reader(); + switch (self.entryType()) { + // https://learn.microsoft.com/en-us/typography/opentype/spec/glyf#simple-glyph-description + .simple => { + const num_contours: usize = @intCast(self.header.numberOfContours); + + // From the spec: + // + // > If a glyph has zero contours, no additional glyph + // > data beyond the header is required. A glyph with + // > zero contours may have additional data, however; + // > in particular, it may have instructions that + // > operate on phantom points. + // + // If our number of contours is 0, and there's less than + // two bytes in the remaining data, then we just return + // the size of the header as our size. The reason for + // two bytes is because that's the minimum size of the + // extra data, since `instructionLength` is 16 bits. + if (num_contours == 0 and self.data.len < 2) { + return @sizeOf(Header); + } + + // uint16 endPtsOfContours[numberOfContours] + // + // Array of point indices for the last point + // of each contour, in increasing numeric order. + var max_point_index: isize = -1; + for (0..num_contours) |_| { + const index = try reader.readInt(sfnt.uint16, .big); + // The endpoints are supposed to monotonically increase. + if (index <= max_point_index) return error.EndPointsOutOfOrder; + max_point_index = index; + } + + // uint16 instructionLength + // + // Total number of bytes for instructions. + // + // If instructionLength is zero, no instructions + // are present for this glyph, and this field is + // followed directly by the flags field. + const instructions_length = try reader.readInt(sfnt.uint16, .big); + + // Since we don't have code that validates instruction + // byte code, we just reject all glyphs that contain any. + // + // In the future we could change this to just ignore the + // instructions, or even validate them, but for now this + // is fine, since we only need this function at all to + // validate glyf entries from the glyph protocol, which + // explicitly forbids instructions anyway. + if (instructions_length > 0) return error.InstructionsNotSupported; + + // uint8 flags[variable] + // + // Array of flag elements. + // + // --- + // + // We do additional accounting here to figure out how many + // bytes the next two fields (the [x|y]Coordinates arrays) + // should take, so that we can just try to throw out that + // many bytes in order to validate them. This is because + // the length of each one depends on the flags. + // + // We're using `i` here to count the number of logical + // entries we have, which should reach the number of + // points defined by the final endpoint (from earlier). + var i: usize = 0; + var x_coords_len: usize = 0; + var y_coords_len: usize = 0; + while (i <= max_point_index) : (i += 1) { + const flag: SimpleFlags = @bitCast(try reader.readByte()); + + // Determine how many bytes the x and y coordinates will + // be represented with in the corresponding arrays, add + // them to our tallies. + x_coords_len += flag.xBytes(); + y_coords_len += flag.yBytes(); + + // 0x08 REPEAT_FLAG + // Bit 3: If set, the next byte (read as unsigned) + // specifies the number of additional times this flag + // byte is to be repeated in the logical flags array + // — that is, the number of additional logical flag + // entries inserted after this entry. + if (flag.repeat) { + // The flag is repeated a certain number of times, + // which means that the point count is increased by + // that count, and the x_coords_len and y_coords_len + // must be increased by the correct number of bytes + // as well. + const repeat_count: usize = try reader.readByte(); + i += repeat_count; + x_coords_len += repeat_count * flag.xBytes(); + y_coords_len += repeat_count * flag.yBytes(); + + // If the repeat count pushes our logical point + // number beyond the max point index which we + // figured out earlier from the end points, then + // there's an issue with this entry, error out. + if (i > max_point_index) return error.TooManyPoints; + } + } + + // uint8 or int16 xCoordinates[variable] + // + // Contour point x-coordinates. + // + // --- + // + // We determined the length of this section (in bytes) + // above while processing the flags, so that we can just + // skip that many bytes to validate this field. + try reader.skipBytes(x_coords_len, .{}); + + // uint8 or int16 yCoordinates[variable] + // + // Contour point y-coordinates. + // + // --- + // + // We determined the length of this section (in bytes) + // above while processing the flags, so that we can just + // skip that many bytes to validate this field. + try reader.skipBytes(y_coords_len, .{}); + }, + + .composite => { + // We don't have code for validating composite glyphs, + // mainly because we don't need it, since we only use + // this function for the glyph protocol which explicitly + // forbids composite glyphs anyway. + // + // So we return false for composite glyphs. + return error.CompositeNotSupported; + }, + } + + // No issues found, the glyf entry is valid, return its length. + return @sizeOf(Header) + fbs.pos; + } + }; + + /// Initialize the table from the provided data. + /// + /// This DOES NOT COPY the data, it only stores a pointer to it. + /// + /// The lifetime of this struct, then, is the same as the + /// lifetime of the data that is used to initialize it. + pub fn init(data: []const u8) Glyf { + return .{ .data = data }; + } + + /// Retrieve the entry at the provided offset. + pub fn entry(self: Glyf, index: usize) error{EndOfStream}!Entry { + return try Entry.init(self.data[index..]); + } +}; + +/// TESTING ONLY +/// +/// Retrieves the glyf at the provided index from the provided font. +/// +/// Returns it in a tuple with the expected length based on the loca table, and the entry. +pub fn getGlyph(font: sfnt.SFNT, index: usize) !struct { usize, Glyf.Entry } { + comptime if (!@import("builtin").is_test) + @compileError("This function is for testing only! It doesn't check bounds or anything!"); + + const glyf = Glyf.init(font.getTable("glyf").?); + const head = try @import("head.zig").Head.init(font.getTable("head").?); + const loca = font.getTable("loca").?; + + const start_offset = switch (head.indexToLocFormat) { + 0 => @as(usize, std.mem.bigToNative( + u16, + std.mem.bytesAsSlice(u16, loca)[index], + )) * 2, + 1 => @as(usize, std.mem.bigToNative( + u32, + std.mem.bytesAsSlice(u32, loca)[index], + )), + else => unreachable, + }; + + const end_offset = switch (head.indexToLocFormat) { + 0 => @as(usize, std.mem.bigToNative( + u16, + std.mem.bytesAsSlice(u16, loca)[index + 1], + )) * 2, + 1 => @as(usize, std.mem.bigToNative( + u32, + std.mem.bytesAsSlice(u32, loca)[index + 1], + )), + else => unreachable, + }; + + return .{ end_offset - start_offset, try glyf.entry(start_offset) }; +} + +test "glyf" { + const testing = std.testing; + const alloc = testing.allocator; + // Cozette because it doesn't have any hinting. + const test_font = @import("../embedded.zig").cozette; + + const font = try sfnt.SFNT.init(test_font, alloc); + defer font.deinit(alloc); + + // Cozette doesn't actually include a glyph for notdef, + // but does include a glyph for `\0` (nul), at index 1. + const len_nul, const glyph_nul = try getGlyph(font, 1); + try testing.expect(glyph_nul.entryType() == .simple); + // It is legal for there to be extra data between two entries, just + // as long as the next entry starts after the previous one ends, so + // it's okay for the parsed size of the entry to be less than the size + // determined from the difference between subsequent loca offsets. + try testing.expect(len_nul >= try glyph_nul.size()); + + // Glyph "A" is at index 66. + const len_A, const glyph_A = try getGlyph(font, 66); + try testing.expect(glyph_A.entryType() == .simple); + try testing.expect(len_A >= try glyph_A.size()); + + // Glyph "Ĩ" is at index 265. + const len_Itilde, const glyph_Itilde = try getGlyph(font, 265); + try testing.expect(glyph_Itilde.entryType() == .simple); + try testing.expect(len_Itilde >= try glyph_Itilde.size()); +} + +test "glyf: reject glyphs with instructions and composite glyphs" { + const testing = std.testing; + const alloc = testing.allocator; + const test_font = @import("../embedded.zig").jetbrains_mono; + + const font = try sfnt.SFNT.init(test_font, alloc); + defer font.deinit(alloc); + + const len_notdef, const glyph_notdef = try getGlyph(font, 0); + try testing.expectEqual(100, len_notdef); + try testing.expect(glyph_notdef.entryType() == .simple); + try testing.expectError( + Glyf.Entry.SizeError.InstructionsNotSupported, + glyph_notdef.size(), + ); + + // Glyph "Á" is at index 2. + const len_Aacute, const glyph_Aacute = try getGlyph(font, 2); + try testing.expectEqual(24, len_Aacute); + try testing.expect(glyph_Aacute.entryType() == .composite); + try testing.expectError( + Glyf.Entry.SizeError.CompositeNotSupported, + glyph_Aacute.size(), + ); +} + +test "glyf: reject truncated" { + const testing = std.testing; + const alloc = testing.allocator; + // Cozette because it doesn't have any hinting. + const test_font = @import("../embedded.zig").cozette; + + const font = try sfnt.SFNT.init(test_font, alloc); + defer font.deinit(alloc); + + _, var glyph_nul = try getGlyph(font, 1); + try testing.expect(glyph_nul.entryType() == .simple); + // Mess with the entry's data slice, truncating + // it before the full length (which is 228 bytes). + glyph_nul.data = glyph_nul.data[0 .. 227 - @sizeOf(Glyf.Entry.Header)]; + try testing.expectError(Glyf.Entry.SizeError.EndOfStream, glyph_nul.size()); +} + +test "glyf: reject endpoints out of order" { + const testing = std.testing; + const alloc = testing.allocator; + // Cozette because it doesn't have any hinting. + // + // Also we copy it with the allocator so we can mess with it. + const test_font = try alloc.dupe(u8, @import("../embedded.zig").cozette[0..]); + defer alloc.free(test_font); + + const font = try sfnt.SFNT.init(test_font, alloc); + defer font.deinit(alloc); + + _, var glyph_nul = try getGlyph(font, 1); + try testing.expect(glyph_nul.entryType() == .simple); + // Mess with the entry's data, insert a 0 in the middle of the endpoints. + // + // Because we know the underlying data is something we + // copied, we can just const cast it back to mutable lol. + std.mem.bytesAsSlice(u16, @as([]u8, @constCast(glyph_nul.data)))[3] = 0; + try testing.expectError(Glyf.Entry.SizeError.EndPointsOutOfOrder, glyph_nul.size()); +} + +test "glyf: reject too many points" { + const testing = std.testing; + const alloc = testing.allocator; + // Cozette because it doesn't have any hinting. + // + // Also we copy it with the allocator so we can mess with it. + const test_font = try alloc.dupe(u8, @import("../embedded.zig").cozette[0..]); + defer alloc.free(test_font); + + const font = try sfnt.SFNT.init(test_font, alloc); + defer font.deinit(alloc); + + _, var glyph_nul = try getGlyph(font, 1); + try testing.expect(glyph_nul.entryType() == .simple); + // Mess with the entry's data, make the final two bytes of the flags + // array be a large number repeat to exceed the correct points count. + // + // Because we know the underlying data is something we + // copied, we can just const cast it back to mutable lol. + @as([]u8, @constCast(glyph_nul.data))[107] |= 0x08; + @as([]u8, @constCast(glyph_nul.data))[108] = 0xFF; + try testing.expectError(Glyf.Entry.SizeError.TooManyPoints, glyph_nul.size()); +} + +test "glyf: zero-contour glyph can be header-only" { + const testing = std.testing; + + const header: Glyf.Entry.Header = .{ + .numberOfContours = 0, + .xMin = 0, + .yMin = 0, + .xMax = 0, + .yMax = 0, + }; + const glyph = try Glyf.Entry.init(std.mem.asBytes(&header)); + try testing.expectEqual(@sizeOf(Glyf.Entry.Header), try glyph.size()); +} diff --git a/vendor/libghostty-vt/src/font/shape.zig b/vendor/libghostty-vt/src/font/shape.zig index 864c0f01..bc19c1f2 100644 --- a/vendor/libghostty-vt/src/font/shape.zig +++ b/vendor/libghostty-vt/src/font/shape.zig @@ -19,6 +19,7 @@ pub const default_features = feature.default_features; /// Shaper implementation for our compile options. pub const Shaper = switch (options.backend) { .freetype, + .freetype_windows, .fontconfig_freetype, .coretext_freetype, .coretext_harfbuzz, diff --git a/vendor/libghostty-vt/src/font/shaper/coretext.zig b/vendor/libghostty-vt/src/font/shaper/coretext.zig index ff7c6d9d..3f69af6d 100644 --- a/vendor/libghostty-vt/src/font/shaper/coretext.zig +++ b/vendor/libghostty-vt/src/font/shaper/coretext.zig @@ -2585,7 +2585,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { }); } else { // On CoreText we want to load Apple Emoji, we should have it. - var disco = font.Discover.init(); + var disco = font.Discover.init(lib); defer disco.deinit(); var disco_it = try disco.discover(alloc, .{ .family = "Apple Color Emoji", @@ -2640,7 +2640,7 @@ fn testShaperWithDiscoveredFont(alloc: Allocator, font_req: [:0]const u8) !TestS // Discover and add our font to the collection. { - var disco = font.Discover.init(); + var disco = font.Discover.init(lib); defer disco.deinit(); var disco_it = try disco.discover(alloc, .{ .family = font_req, diff --git a/vendor/libghostty-vt/src/font/shaper/harfbuzz.zig b/vendor/libghostty-vt/src/font/shaper/harfbuzz.zig index 9c78d9de..a400ecaf 100644 --- a/vendor/libghostty-vt/src/font/shaper/harfbuzz.zig +++ b/vendor/libghostty-vt/src/font/shaper/harfbuzz.zig @@ -2071,7 +2071,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { }); } else { // On CoreText we want to load Apple Emoji, we should have it. - var disco = font.Discover.init(); + var disco = font.Discover.init(lib); defer disco.deinit(); var disco_it = try disco.discover(alloc, .{ .family = "Apple Color Emoji", @@ -2126,7 +2126,7 @@ fn testShaperWithDiscoveredFont(alloc: Allocator, font_req: [:0]const u8) !TestS // Discover and add our font to the collection. { - var disco = font.Discover.init(); + var disco = font.Discover.init(lib); defer disco.deinit(); var disco_it = try disco.discover(alloc, .{ .family = font_req, |
