mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 16:26:08 +03:00
Merge pull request #1803 from mitchellh/mixed-font
Handle mixed-color fonts
This commit is contained in:
@ -1,7 +1,9 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const c = @import("c.zig");
|
||||
const errors = @import("errors.zig");
|
||||
const Library = @import("Library.zig");
|
||||
const Tag = @import("tag.zig").Tag;
|
||||
const Error = errors.Error;
|
||||
const intToError = errors.intToError;
|
||||
|
||||
@ -24,6 +26,12 @@ pub const Face = struct {
|
||||
return c.FT_HAS_COLOR(self.handle);
|
||||
}
|
||||
|
||||
/// A macro that returns true whenever a face object contains an ‘sbix’
|
||||
/// OpenType table and outline glyphs.
|
||||
pub fn hasSBIX(self: Face) bool {
|
||||
return c.FT_HAS_SBIX(self.handle);
|
||||
}
|
||||
|
||||
/// A macro that returns true whenever a face object contains some
|
||||
/// multiple masters.
|
||||
pub fn hasMultipleMasters(self: Face) bool {
|
||||
@ -102,6 +110,40 @@ pub const Face = struct {
|
||||
return if (intToError(res)) |_| name else |err| err;
|
||||
}
|
||||
|
||||
/// Load any SFNT font table into client memory.
|
||||
pub fn loadSfntTable(
|
||||
self: Face,
|
||||
alloc: Allocator,
|
||||
tag: Tag,
|
||||
) (Allocator.Error || Error)!?[]u8 {
|
||||
const tag_c: c_ulong = @intCast(@as(u32, @bitCast(tag)));
|
||||
|
||||
// Get the length of the table in bytes
|
||||
var len: c_ulong = 0;
|
||||
var res = c.FT_Load_Sfnt_Table(self.handle, tag_c, 0, null, &len);
|
||||
_ = intToError(res) catch |err| return err;
|
||||
|
||||
// If our length is zero we don't have a table.
|
||||
if (len == 0) return null;
|
||||
|
||||
// Allocate a buffer to hold the table and load it
|
||||
const buf = try alloc.alloc(u8, len);
|
||||
errdefer alloc.free(buf);
|
||||
res = c.FT_Load_Sfnt_Table(self.handle, tag_c, 0, buf.ptr, &len);
|
||||
_ = intToError(res) catch |err| return err;
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
/// Check whether a given SFNT table is available in a face.
|
||||
pub fn hasSfntTable(self: Face, tag: Tag) bool {
|
||||
const tag_c: c_ulong = @intCast(@as(u32, @bitCast(tag)));
|
||||
var len: c_ulong = 0;
|
||||
const res = c.FT_Load_Sfnt_Table(self.handle, tag_c, 0, null, &len);
|
||||
_ = intToError(res) catch return false;
|
||||
return len != 0;
|
||||
}
|
||||
|
||||
/// Retrieve the font variation descriptor for a font.
|
||||
pub fn getMMVar(self: Face) Error!*c.FT_MM_Var {
|
||||
var result: *c.FT_MM_Var = undefined;
|
||||
|
@ -4,6 +4,7 @@ pub const Library = @import("Library.zig");
|
||||
pub usingnamespace @import("computations.zig");
|
||||
pub usingnamespace @import("errors.zig");
|
||||
pub usingnamespace @import("face.zig");
|
||||
pub usingnamespace @import("tag.zig");
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
|
17
pkg/freetype/tag.zig
Normal file
17
pkg/freetype/tag.zig
Normal file
@ -0,0 +1,17 @@
|
||||
/// FT_Tag
|
||||
pub const Tag = packed struct(u32) {
|
||||
d: u8,
|
||||
c: u8,
|
||||
b: u8,
|
||||
a: u8,
|
||||
|
||||
pub fn init(v: *const [4]u8) Tag {
|
||||
return .{ .a = v[0], .b = v[1], .c = v[2], .d = v[3] };
|
||||
}
|
||||
|
||||
/// Converts the ID to a string. The return value is only valid
|
||||
/// for the lifetime of the self pointer.
|
||||
pub fn str(self: Tag) [4]u8 {
|
||||
return .{ self.a, self.b, self.c, self.d };
|
||||
}
|
||||
};
|
@ -16,3 +16,20 @@ pub const Range = extern struct {
|
||||
return @bitCast(c.CFRangeMake(@intCast(loc), @intCast(len)));
|
||||
}
|
||||
};
|
||||
|
||||
pub const FourCharCode = packed struct(u32) {
|
||||
d: u8,
|
||||
c: u8,
|
||||
b: u8,
|
||||
a: u8,
|
||||
|
||||
pub fn init(v: *const [4]u8) FourCharCode {
|
||||
return .{ .a = v[0], .b = v[1], .c = v[2], .d = v[3] };
|
||||
}
|
||||
|
||||
/// Converts the ID to a string. The return value is only valid
|
||||
/// for the lifetime of the self pointer.
|
||||
pub fn str(self: FourCharCode) [4]u8 {
|
||||
return .{ self.a, self.b, self.c, self.d };
|
||||
}
|
||||
};
|
||||
|
@ -20,9 +20,13 @@ pub const Data = opaque {
|
||||
foundation.CFRelease(self);
|
||||
}
|
||||
|
||||
pub fn getPointer(self: *Data) *const anyopaque {
|
||||
pub fn getPointer(self: *Data) [*]const u8 {
|
||||
return @ptrCast(c.CFDataGetBytePtr(@ptrCast(self)));
|
||||
}
|
||||
|
||||
pub fn getLength(self: *Data) usize {
|
||||
return @intCast(c.CFDataGetLength(@ptrCast(self)));
|
||||
}
|
||||
};
|
||||
|
||||
test {
|
||||
|
@ -67,6 +67,14 @@ pub const Font = opaque {
|
||||
return @ptrCast(@constCast(c.CTFontCopyDefaultCascadeListForLanguages(@ptrCast(self), null)));
|
||||
}
|
||||
|
||||
pub fn copyTable(self: *Font, tag: FontTableTag) ?*foundation.Data {
|
||||
return @constCast(@ptrCast(c.CTFontCopyTable(
|
||||
@ptrCast(self),
|
||||
@intFromEnum(tag),
|
||||
c.kCTFontTableOptionNoOptions,
|
||||
)));
|
||||
}
|
||||
|
||||
pub fn getGlyphCount(self: *Font) usize {
|
||||
return @intCast(c.CTFontGetGlyphCount(@ptrCast(self)));
|
||||
}
|
||||
@ -195,6 +203,16 @@ pub const FontOrientation = enum(c_uint) {
|
||||
vertical = c.kCTFontOrientationVertical,
|
||||
};
|
||||
|
||||
pub const FontTableTag = enum(u32) {
|
||||
svg = c.kCTFontTableSVG,
|
||||
_,
|
||||
|
||||
pub fn init(v: *const [4]u8) FontTableTag {
|
||||
const raw: u32 = @bitCast(foundation.FourCharCode.init(v));
|
||||
return @enumFromInt(raw);
|
||||
}
|
||||
};
|
||||
|
||||
test {
|
||||
const testing = std.testing;
|
||||
|
||||
|
@ -291,13 +291,17 @@ fn getIndexCodepointOverride(
|
||||
|
||||
/// Returns the presentation for a specific font index. This is useful for
|
||||
/// determining what atlas is needed.
|
||||
pub fn getPresentation(self: *CodepointResolver, index: Collection.Index) !Presentation {
|
||||
pub fn getPresentation(
|
||||
self: *CodepointResolver,
|
||||
index: Collection.Index,
|
||||
glyph_index: u32,
|
||||
) !Presentation {
|
||||
if (index.special()) |sp| return switch (sp) {
|
||||
.sprite => .text,
|
||||
};
|
||||
|
||||
const face = try self.collection.getFace(index);
|
||||
return face.presentation;
|
||||
return if (face.isColorGlyph(glyph_index)) .emoji else .text;
|
||||
}
|
||||
|
||||
/// Render a glyph by glyph index into the given font atlas and return
|
||||
|
@ -190,16 +190,23 @@ pub fn autoItalicize(self: *Collection, alloc: Allocator) !void {
|
||||
const list = self.faces.get(.regular);
|
||||
if (list.items.len == 0) return;
|
||||
|
||||
// Find our first font that is text. This will force
|
||||
// loading any deferred faces but we only load them until
|
||||
// we find a text face. A text face is almost always the
|
||||
// first face in the list.
|
||||
// Find our first regular face that has text glyphs.
|
||||
for (0..list.items.len) |i| {
|
||||
const face = try self.getFace(.{
|
||||
.style = .regular,
|
||||
.idx = @intCast(i),
|
||||
});
|
||||
if (face.presentation == .text) break :regular face;
|
||||
|
||||
// We have two conditionals here. The color check is obvious:
|
||||
// we want to auto-italicize a normal text font. The second
|
||||
// check is less obvious... for mixed color/non-color fonts, we
|
||||
// accept the regular font if it has basic ASCII. This may not
|
||||
// be strictly correct (especially with international fonts) but
|
||||
// it's a reasonable heuristic and the first case will match 99%
|
||||
// of the time.
|
||||
if (!face.hasColor() or face.glyphIndex('A') != null) {
|
||||
break :regular face;
|
||||
}
|
||||
}
|
||||
|
||||
// No regular text face found.
|
||||
@ -344,7 +351,13 @@ pub const Entry = union(enum) {
|
||||
},
|
||||
|
||||
.loaded => |face| switch (p_mode) {
|
||||
.explicit => |p| face.presentation == p and face.glyphIndex(cp) != null,
|
||||
.explicit => |p| explicit: {
|
||||
const index = face.glyphIndex(cp) orelse break :explicit false;
|
||||
break :explicit switch (p) {
|
||||
.text => !face.isColorGlyph(index),
|
||||
.emoji => face.isColorGlyph(index),
|
||||
};
|
||||
},
|
||||
.default, .any => face.glyphIndex(cp) != null,
|
||||
},
|
||||
|
||||
@ -357,7 +370,13 @@ pub const Entry = union(enum) {
|
||||
.fallback_loaded => |face| switch (p_mode) {
|
||||
.explicit,
|
||||
.default,
|
||||
=> |p| face.presentation == p and face.glyphIndex(cp) != null,
|
||||
=> |p| explicit: {
|
||||
const index = face.glyphIndex(cp) orelse break :explicit false;
|
||||
break :explicit switch (p) {
|
||||
.text => !face.isColorGlyph(index),
|
||||
.emoji => face.isColorGlyph(index),
|
||||
};
|
||||
},
|
||||
.any => face.glyphIndex(cp) != null,
|
||||
},
|
||||
};
|
||||
@ -371,7 +390,7 @@ pub const PresentationMode = union(enum) {
|
||||
explicit: Presentation,
|
||||
|
||||
/// The codepoint has no explicit presentation and we should use
|
||||
/// the presentation from the UCd.
|
||||
/// the presentation from the UCD.
|
||||
default: Presentation,
|
||||
|
||||
/// The codepoint can be any presentation.
|
||||
|
@ -281,6 +281,12 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool {
|
||||
=> {
|
||||
// If we are using coretext, we check the loaded CT font.
|
||||
if (self.ct) |ct| {
|
||||
// This presentation check isn't as detailed as isColorGlyph
|
||||
// because forced presentation modes are only used for emoji and
|
||||
// emoji should always have color glyphs set. This can be
|
||||
// more correct by using the isColorGlyph logic but I'd want
|
||||
// to find a font that actualy requires this so we can write
|
||||
// a test for it before changing it.
|
||||
if (p) |desired_p| {
|
||||
const traits = ct.font.getSymbolicTraits();
|
||||
const actual_p: Presentation = if (traits.color_glyphs) .emoji else .text;
|
||||
|
@ -253,7 +253,7 @@ pub fn renderGlyph(
|
||||
if (gop.found_existing) return gop.value_ptr.*;
|
||||
|
||||
// Get the presentation to determine what atlas to use
|
||||
const p = try self.resolver.getPresentation(index);
|
||||
const p = try self.resolver.getPresentation(index, glyph_index);
|
||||
const atlas: *font.Atlas = switch (p) {
|
||||
.text => &self.atlas_greyscale,
|
||||
.emoji => &self.atlas_color,
|
||||
|
@ -5,6 +5,7 @@ const Allocator = std.mem.Allocator;
|
||||
const macos = @import("macos");
|
||||
const harfbuzz = @import("harfbuzz");
|
||||
const font = @import("../main.zig");
|
||||
const opentype = @import("../opentype.zig");
|
||||
const quirks = @import("../../quirks.zig");
|
||||
|
||||
const log = std.log.scoped(.font_face);
|
||||
@ -17,15 +18,18 @@ pub const Face = struct {
|
||||
/// if we're using Harfbuzz.
|
||||
hb_font: if (harfbuzz_shaper) harfbuzz.Font else void,
|
||||
|
||||
/// The presentation for this font.
|
||||
presentation: font.Presentation,
|
||||
|
||||
/// Metrics for this font face. These are useful for renderers.
|
||||
metrics: font.face.Metrics,
|
||||
|
||||
/// Set quirks.disableDefaultFontFeatures
|
||||
quirks_disable_default_font_features: bool = false,
|
||||
|
||||
/// If the face can possibly be colored, then this is the state
|
||||
/// used to check for color information. This is null if the font
|
||||
/// can't possibly be colored (i.e. doesn't have SVG, sbix, etc
|
||||
/// tables).
|
||||
color: ?ColorState = null,
|
||||
|
||||
/// True if our build is using Harfbuzz. If we're not, we can avoid
|
||||
/// some Harfbuzz-specific code paths.
|
||||
const harfbuzz_shaper = font.options.backend.hasHarfbuzz();
|
||||
@ -94,28 +98,20 @@ pub const Face = struct {
|
||||
} else {};
|
||||
errdefer if (comptime harfbuzz_shaper) hb_font.destroy();
|
||||
|
||||
const color: ?ColorState = if (traits.color_glyphs)
|
||||
try ColorState.init(ct_font)
|
||||
else
|
||||
null;
|
||||
errdefer if (color) |v| v.deinit();
|
||||
|
||||
var result: Face = .{
|
||||
.font = ct_font,
|
||||
.hb_font = hb_font,
|
||||
.presentation = if (traits.color_glyphs) .emoji else .text,
|
||||
.metrics = metrics,
|
||||
.color = color,
|
||||
};
|
||||
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
|
||||
|
||||
// If our presentation is emoji, we also check for the presence of
|
||||
// emoji codepoints. This forces fonts with colorized glyphs that aren't
|
||||
// emoji font to be treated as text. Long term, this isn't what we want
|
||||
// but this fixes some bugs in the short term. See:
|
||||
// https://github.com/mitchellh/ghostty/issues/1768
|
||||
//
|
||||
// Longer term, we'd like to detect mixed color/non-color fonts and
|
||||
// handle them correctly by rendering the color glyphs as color and the
|
||||
// non-color glyphs as text.
|
||||
if (result.presentation == .emoji and result.glyphIndex('🥸') == null) {
|
||||
log.warn("font has colorized glyphs but isn't emoji, treating as text", .{});
|
||||
result.presentation = .text;
|
||||
}
|
||||
|
||||
// In debug mode, we output information about available variation axes,
|
||||
// if they exist.
|
||||
if (comptime builtin.mode == .Debug) {
|
||||
@ -167,6 +163,7 @@ pub const Face = struct {
|
||||
pub fn deinit(self: *Face) void {
|
||||
self.font.release();
|
||||
if (comptime harfbuzz_shaper) self.hb_font.destroy();
|
||||
if (self.color) |v| v.deinit();
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
@ -226,6 +223,19 @@ pub const Face = struct {
|
||||
self.* = face;
|
||||
}
|
||||
|
||||
/// Returns true if the face has any glyphs that are colorized.
|
||||
/// To determine if an individual glyph is colorized you must use
|
||||
/// isColorGlyph.
|
||||
pub fn hasColor(self: *const Face) bool {
|
||||
return self.color != null;
|
||||
}
|
||||
|
||||
/// Returns true if the given glyph ID is colorized.
|
||||
pub fn isColorGlyph(self: *const Face, glyph_id: u32) bool {
|
||||
const c = self.color orelse return false;
|
||||
return c.isColorGlyph(glyph_id);
|
||||
}
|
||||
|
||||
/// Returns the glyph index for the given Unicode code point. If this
|
||||
/// face doesn't support this glyph, null is returned.
|
||||
pub fn glyphIndex(self: Face, cp: u32) ?u32 {
|
||||
@ -296,7 +306,7 @@ pub const Face = struct {
|
||||
depth: u32,
|
||||
space: *macos.graphics.ColorSpace,
|
||||
context_opts: c_uint,
|
||||
} = if (self.presentation == .text) .{
|
||||
} = if (!self.isColorGlyph(glyph_index)) .{
|
||||
.color = false,
|
||||
.depth = 1,
|
||||
.space = try macos.graphics.ColorSpace.createDeviceGray(),
|
||||
@ -570,6 +580,91 @@ pub const Face = struct {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Copy the font table data for the given tag.
|
||||
pub fn copyTable(self: Face, alloc: Allocator, tag: *const [4]u8) !?[]u8 {
|
||||
const data = self.font.copyTable(macos.text.FontTableTag.init(tag)) orelse
|
||||
return null;
|
||||
defer data.release();
|
||||
|
||||
const buf = try alloc.alloc(u8, data.getLength());
|
||||
errdefer alloc.free(buf);
|
||||
|
||||
const ptr = data.getPointer();
|
||||
@memcpy(buf, ptr[0..buf.len]);
|
||||
|
||||
return buf;
|
||||
}
|
||||
};
|
||||
|
||||
/// The state associated with a font face that may have colorized glyphs.
|
||||
/// This is used to determine if a specific glyph ID is colorized.
|
||||
const ColorState = struct {
|
||||
/// True if there is an sbix font table. For now, the mere presence
|
||||
/// of an sbix font table causes us to assume the glyph is colored.
|
||||
/// We can improve this later.
|
||||
sbix: bool,
|
||||
|
||||
/// The SVG font table data (if any), which we can use to determine
|
||||
/// if a glyph is present in the SVG table.
|
||||
svg: ?opentype.SVG,
|
||||
svg_data: ?*macos.foundation.Data,
|
||||
|
||||
pub fn init(f: *macos.text.Font) !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.
|
||||
const sbix: bool = sbix: {
|
||||
const tag = macos.text.FontTableTag.init("sbix");
|
||||
const data = f.copyTable(tag) orelse break :sbix false;
|
||||
data.release();
|
||||
break :sbix data.getLength() > 0;
|
||||
};
|
||||
|
||||
// Read the SVG table out of the font data.
|
||||
const svg: ?struct {
|
||||
svg: opentype.SVG,
|
||||
data: *macos.foundation.Data,
|
||||
} = svg: {
|
||||
const tag = macos.text.FontTableTag.init("SVG ");
|
||||
const data = f.copyTable(tag) orelse break :svg null;
|
||||
errdefer data.release();
|
||||
const ptr = data.getPointer();
|
||||
const len = data.getLength();
|
||||
break :svg .{
|
||||
.svg = try opentype.SVG.init(ptr[0..len]),
|
||||
.data = data,
|
||||
};
|
||||
};
|
||||
|
||||
return .{
|
||||
.sbix = sbix,
|
||||
.svg = if (svg) |v| v.svg else null,
|
||||
.svg_data = if (svg) |v| v.data else null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const ColorState) void {
|
||||
if (self.svg_data) |v| v.release();
|
||||
}
|
||||
|
||||
/// Returns true if the given glyph ID is colored.
|
||||
pub fn isColorGlyph(self: *const ColorState, glyph_id: u32) bool {
|
||||
// Our font system uses 32-bit glyph IDs for special values but
|
||||
// actual fonts only contain 16-bit glyph IDs so if we can't cast
|
||||
// into it it must be false.
|
||||
const glyph_u16 = std.math.cast(u16, glyph_id) orelse return false;
|
||||
|
||||
// sbix is always true for now
|
||||
if (self.sbix) return true;
|
||||
|
||||
// if we have svg data, check it
|
||||
if (self.svg) |svg| {
|
||||
if (svg.hasGlyph(glyph_u16)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
test {
|
||||
@ -589,8 +684,6 @@ test {
|
||||
var face = try Face.initFontCopy(ct_font, .{ .size = .{ .points = 12 } });
|
||||
defer face.deinit();
|
||||
|
||||
try testing.expectEqual(font.Presentation.text, face.presentation);
|
||||
|
||||
// Generate all visible ASCII
|
||||
var i: u8 = 32;
|
||||
while (i < 127) : (i += 1) {
|
||||
@ -612,8 +705,6 @@ test "name" {
|
||||
var face = try Face.initFontCopy(ct_font, .{ .size = .{ .points = 12 } });
|
||||
defer face.deinit();
|
||||
|
||||
try testing.expectEqual(font.Presentation.text, face.presentation);
|
||||
|
||||
var buf: [1024]u8 = undefined;
|
||||
const font_name = try face.name(&buf);
|
||||
try testing.expect(std.mem.eql(u8, font_name, "Menlo"));
|
||||
@ -632,11 +723,11 @@ test "emoji" {
|
||||
var face = try Face.initFontCopy(ct_font, .{ .size = .{ .points = 18 } });
|
||||
defer face.deinit();
|
||||
|
||||
// Presentation
|
||||
try testing.expectEqual(font.Presentation.emoji, face.presentation);
|
||||
|
||||
// Glyph index check
|
||||
try testing.expect(face.glyphIndex('🥸') != null);
|
||||
{
|
||||
const id = face.glyphIndex('🥸').?;
|
||||
try testing.expect(face.isColorGlyph(id));
|
||||
}
|
||||
}
|
||||
|
||||
test "in-memory" {
|
||||
@ -653,8 +744,6 @@ test "in-memory" {
|
||||
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
|
||||
defer face.deinit();
|
||||
|
||||
try testing.expectEqual(font.Presentation.text, face.presentation);
|
||||
|
||||
// Generate all visible ASCII
|
||||
var i: u8 = 32;
|
||||
while (i < 127) : (i += 1) {
|
||||
@ -677,8 +766,6 @@ test "variable" {
|
||||
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
|
||||
defer face.deinit();
|
||||
|
||||
try testing.expectEqual(font.Presentation.text, face.presentation);
|
||||
|
||||
// Generate all visible ASCII
|
||||
var i: u8 = 32;
|
||||
while (i < 127) : (i += 1) {
|
||||
@ -701,8 +788,6 @@ test "variable set variation" {
|
||||
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
|
||||
defer face.deinit();
|
||||
|
||||
try testing.expectEqual(font.Presentation.text, face.presentation);
|
||||
|
||||
try face.setVariations(&.{
|
||||
.{ .id = font.face.Variation.Id.init("wght"), .value = 400 },
|
||||
}, .{ .size = .{ .points = 12 } });
|
||||
@ -715,7 +800,24 @@ test "variable set variation" {
|
||||
}
|
||||
}
|
||||
|
||||
test "mixed color/non-color font treated as text" {
|
||||
test "svg font table" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const testFont = @import("../test.zig").fontJuliaMono;
|
||||
|
||||
var lib = try font.Library.init();
|
||||
defer lib.deinit();
|
||||
|
||||
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
|
||||
defer face.deinit();
|
||||
|
||||
const table = (try face.copyTable(alloc, "SVG ")).?;
|
||||
defer alloc.free(table);
|
||||
|
||||
try testing.expect(table.len > 0);
|
||||
}
|
||||
|
||||
test "glyphIndex colored vs text" {
|
||||
const testing = std.testing;
|
||||
const testFont = @import("../test.zig").fontJuliaMono;
|
||||
|
||||
@ -725,5 +827,15 @@ test "mixed color/non-color font treated as text" {
|
||||
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
|
||||
defer face.deinit();
|
||||
|
||||
try testing.expect(face.presentation == .text);
|
||||
{
|
||||
const glyph = face.glyphIndex('A').?;
|
||||
try testing.expectEqual(4, glyph);
|
||||
try testing.expect(!face.isColorGlyph(glyph));
|
||||
}
|
||||
|
||||
{
|
||||
const glyph = face.glyphIndex(0xE800).?;
|
||||
try testing.expectEqual(11482, glyph);
|
||||
try testing.expect(face.isColorGlyph(glyph));
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ const Allocator = std.mem.Allocator;
|
||||
const font = @import("../main.zig");
|
||||
const Glyph = font.Glyph;
|
||||
const Library = font.Library;
|
||||
const Presentation = font.Presentation;
|
||||
const convert = @import("freetype_convert.zig");
|
||||
const fastmem = @import("../../fastmem.zig");
|
||||
const quirks = @import("../../quirks.zig");
|
||||
@ -32,10 +31,6 @@ pub const Face = struct {
|
||||
/// Harfbuzz font corresponding to this face.
|
||||
hb_font: harfbuzz.Font,
|
||||
|
||||
/// The presentation for this font. This is a heuristic since fonts don't have
|
||||
/// a way to declare this. We just assume a font with color is an emoji font.
|
||||
presentation: Presentation,
|
||||
|
||||
/// Metrics for this font face. These are useful for renderers.
|
||||
metrics: font.face.Metrics,
|
||||
|
||||
@ -67,17 +62,10 @@ pub const Face = struct {
|
||||
.lib = lib.lib,
|
||||
.face = face,
|
||||
.hb_font = hb_font,
|
||||
.presentation = if (face.hasColor()) .emoji else .text,
|
||||
.metrics = calcMetrics(face, opts.metric_modifiers),
|
||||
};
|
||||
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
|
||||
|
||||
// See coretext.zig which has a similar check for this.
|
||||
if (result.presentation == .emoji and result.glyphIndex('🥸') == null) {
|
||||
log.warn("font has colorized glyphs but isn't emoji, treating as text", .{});
|
||||
result.presentation = .text;
|
||||
}
|
||||
|
||||
// In debug mode, we output information about available variation axes,
|
||||
// if they exist.
|
||||
if (comptime builtin.mode == .Debug) mm: {
|
||||
@ -219,10 +207,34 @@ pub const Face = struct {
|
||||
|
||||
/// Returns true if this font is colored. This can be used by callers to
|
||||
/// determine what kind of atlas to pass in.
|
||||
fn hasColor(self: Face) bool {
|
||||
pub fn hasColor(self: Face) bool {
|
||||
return self.face.hasColor();
|
||||
}
|
||||
|
||||
/// Returns true if the given glyph ID is colorized.
|
||||
pub fn isColorGlyph(self: *const Face, glyph_id: u32) bool {
|
||||
// sbix table is always true for now
|
||||
if (self.face.hasSBIX()) return true;
|
||||
|
||||
// CBDT/CBLC tables always imply colorized glyphs.
|
||||
// These are used by Noto.
|
||||
if (self.face.hasSfntTable(freetype.Tag.init("CBDT"))) return true;
|
||||
if (self.face.hasSfntTable(freetype.Tag.init("CBLC"))) return true;
|
||||
|
||||
// Otherwise, load the glyph and see what format it is in.
|
||||
self.face.loadGlyph(glyph_id, .{
|
||||
.render = true,
|
||||
.color = self.face.hasColor(),
|
||||
.no_bitmap = !self.face.hasColor(),
|
||||
}) catch return false;
|
||||
|
||||
// If the glyph is SVG we assume colorized
|
||||
const glyph = self.face.handle.*.glyph;
|
||||
if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_SVG) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Render a glyph using the glyph index. The rendered glyph is stored in the
|
||||
/// given texture atlas.
|
||||
pub fn renderGlyph(
|
||||
@ -631,6 +643,11 @@ pub const Face = struct {
|
||||
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));
|
||||
}
|
||||
};
|
||||
|
||||
test {
|
||||
@ -650,8 +667,6 @@ test {
|
||||
);
|
||||
defer ft_font.deinit();
|
||||
|
||||
try testing.expectEqual(Presentation.text, ft_font.presentation);
|
||||
|
||||
// Generate all visible ASCII
|
||||
var i: u8 = 32;
|
||||
while (i < 127) : (i += 1) {
|
||||
@ -686,10 +701,15 @@ test "color emoji" {
|
||||
);
|
||||
defer ft_font.deinit();
|
||||
|
||||
try testing.expectEqual(Presentation.emoji, ft_font.presentation);
|
||||
|
||||
_ = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('🥸').?, .{});
|
||||
|
||||
// Make sure this glyph has color
|
||||
{
|
||||
try testing.expect(ft_font.hasColor());
|
||||
const glyph_id = ft_font.glyphIndex('🥸').?;
|
||||
try testing.expect(ft_font.isColorGlyph(glyph_id));
|
||||
}
|
||||
|
||||
// resize
|
||||
{
|
||||
const glyph = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('🥸').?, .{
|
||||
@ -763,3 +783,19 @@ test "mono to rgba" {
|
||||
// glyph 3 is mono in Noto
|
||||
_ = try ft_font.renderGlyph(alloc, &atlas, 3, .{});
|
||||
}
|
||||
|
||||
test "svg font table" {
|
||||
const alloc = testing.allocator;
|
||||
const testFont = @import("../test.zig").fontJuliaMono;
|
||||
|
||||
var lib = try font.Library.init();
|
||||
defer lib.deinit();
|
||||
|
||||
var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
|
||||
defer face.deinit();
|
||||
|
||||
const table = (try face.copyTable(alloc, "SVG ")).?;
|
||||
defer alloc.free(table);
|
||||
|
||||
try testing.expectEqual(430, table.len);
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ pub const DeferredFace = @import("DeferredFace.zig");
|
||||
pub const Face = face.Face;
|
||||
pub const Glyph = @import("Glyph.zig");
|
||||
pub const Metrics = face.Metrics;
|
||||
pub const opentype = @import("opentype.zig");
|
||||
pub const shape = @import("shape.zig");
|
||||
pub const Shaper = shape.Shaper;
|
||||
pub const ShaperCache = shape.Cache;
|
||||
@ -159,7 +160,7 @@ pub const Style = enum(u3) {
|
||||
bold_italic = 3,
|
||||
};
|
||||
|
||||
/// The presentation for a an emoji.
|
||||
/// The presentation for an emoji.
|
||||
pub const Presentation = enum(u1) {
|
||||
text = 0, // U+FE0E
|
||||
emoji = 1, // U+FEOF
|
||||
|
7
src/font/opentype.zig
Normal file
7
src/font/opentype.zig
Normal file
@ -0,0 +1,7 @@
|
||||
const svg = @import("opentype/svg.zig");
|
||||
|
||||
pub const SVG = svg.SVG;
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
}
|
113
src/font/opentype/svg.zig
Normal file
113
src/font/opentype/svg.zig
Normal file
@ -0,0 +1,113 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const font = @import("../main.zig");
|
||||
|
||||
/// SVG glyphs description table.
|
||||
///
|
||||
/// This struct is focused purely on the operations we need for Ghostty,
|
||||
/// namely to be able to look up whether an glyph ID is present in the SVG
|
||||
/// table or not. This struct isn't meant to be a general purpose SVG table
|
||||
/// reader.
|
||||
///
|
||||
/// References:
|
||||
/// - https://www.w3.org/2013/10/SVG_in_OpenType/#thesvg
|
||||
/// - https://learn.microsoft.com/en-us/typography/opentype/spec/svg
|
||||
pub const SVG = struct {
|
||||
/// The start and end glyph IDs (inclusive) that are present in the
|
||||
/// table. This is used to very quickly include/exclude a glyph from
|
||||
/// the table.
|
||||
start_glyph_id: u16,
|
||||
end_glyph_id: u16,
|
||||
|
||||
/// All records in the table.
|
||||
records: []const [12]u8,
|
||||
|
||||
pub fn init(data: []const u8) !SVG {
|
||||
var fbs = std.io.fixedBufferStream(data);
|
||||
const reader = fbs.reader();
|
||||
|
||||
// Version
|
||||
if (try reader.readInt(u16, .big) != 0) {
|
||||
return error.SVGVersionNotSupported;
|
||||
}
|
||||
|
||||
// Offset
|
||||
const offset = try reader.readInt(u32, .big);
|
||||
|
||||
// Seek to the offset to get our document list
|
||||
try fbs.seekTo(offset);
|
||||
|
||||
// Get our document records along with the start/end glyph range.
|
||||
const len = try reader.readInt(u16, .big);
|
||||
const records: [*]const [12]u8 = @ptrCast(data[try fbs.getPos()..]);
|
||||
const start_range = try glyphRange(&records[0]);
|
||||
const end_range = if (len == 1) start_range else try glyphRange(&records[(len - 1)]);
|
||||
|
||||
return .{
|
||||
.start_glyph_id = start_range[0],
|
||||
.end_glyph_id = end_range[1],
|
||||
.records = records[0..len],
|
||||
};
|
||||
}
|
||||
|
||||
pub fn hasGlyph(self: SVG, glyph_id: u16) bool {
|
||||
// Fast path: outside the table range
|
||||
if (glyph_id < self.start_glyph_id or glyph_id > self.end_glyph_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fast path, matches the start/end glyph IDs
|
||||
if (glyph_id == self.start_glyph_id or glyph_id == self.end_glyph_id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Slow path: binary search our records
|
||||
return std.sort.binarySearch(
|
||||
[12]u8,
|
||||
glyph_id,
|
||||
self.records,
|
||||
{},
|
||||
compareGlyphId,
|
||||
) != null;
|
||||
}
|
||||
|
||||
fn compareGlyphId(_: void, glyph_id: u16, record: [12]u8) std.math.Order {
|
||||
const start, const end = glyphRange(&record) catch return .lt;
|
||||
if (glyph_id < start) {
|
||||
return .lt;
|
||||
} else if (glyph_id > end) {
|
||||
return .gt;
|
||||
} else {
|
||||
return .eq;
|
||||
}
|
||||
}
|
||||
|
||||
fn glyphRange(record: []const u8) !struct { u16, u16 } {
|
||||
var fbs = std.io.fixedBufferStream(record);
|
||||
const reader = fbs.reader();
|
||||
return .{
|
||||
try reader.readInt(u16, .big),
|
||||
try reader.readInt(u16, .big),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
test "SVG" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const testFont = @import("../test.zig").fontJuliaMono;
|
||||
|
||||
var lib = try font.Library.init();
|
||||
defer lib.deinit();
|
||||
|
||||
var face = try font.Face.init(lib, testFont, .{ .size = .{ .points = 12 } });
|
||||
defer face.deinit();
|
||||
|
||||
const table = (try face.copyTable(alloc, "SVG ")).?;
|
||||
defer alloc.free(table);
|
||||
|
||||
const svg = try SVG.init(table);
|
||||
try testing.expectEqual(11482, svg.start_glyph_id);
|
||||
try testing.expectEqual(11482, svg.end_glyph_id);
|
||||
try testing.expect(svg.hasGlyph(11482));
|
||||
}
|
Reference in New Issue
Block a user