mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
Font face handles zero-width glyphs (weird but happens)
This commit is contained in:
@ -48,10 +48,18 @@ modified: bool = false,
|
|||||||
/// updated in-place.
|
/// updated in-place.
|
||||||
resized: bool = false,
|
resized: bool = false,
|
||||||
|
|
||||||
pub const Format = enum(u3) {
|
pub const Format = enum {
|
||||||
greyscale = 1,
|
greyscale,
|
||||||
rgb = 3,
|
rgb,
|
||||||
rgba = 4,
|
rgba,
|
||||||
|
|
||||||
|
pub fn depth(self: Format) u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.greyscale => 1,
|
||||||
|
.rgb => 3,
|
||||||
|
.rgba => 4,
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Node = struct {
|
const Node = struct {
|
||||||
@ -76,7 +84,7 @@ pub const Region = struct {
|
|||||||
|
|
||||||
pub fn init(alloc: Allocator, size: u32, format: Format) !Atlas {
|
pub fn init(alloc: Allocator, size: u32, format: Format) !Atlas {
|
||||||
var result = Atlas{
|
var result = Atlas{
|
||||||
.data = try alloc.alloc(u8, size * size * @enumToInt(format)),
|
.data = try alloc.alloc(u8, size * size * format.depth()),
|
||||||
.size = size,
|
.size = size,
|
||||||
.nodes = .{},
|
.nodes = .{},
|
||||||
.format = format,
|
.format = format,
|
||||||
@ -220,7 +228,7 @@ pub fn set(self: *Atlas, reg: Region, data: []const u8) void {
|
|||||||
assert(reg.y < (self.size - 1));
|
assert(reg.y < (self.size - 1));
|
||||||
assert((reg.y + reg.height) <= (self.size - 1));
|
assert((reg.y + reg.height) <= (self.size - 1));
|
||||||
|
|
||||||
const depth = @enumToInt(self.format);
|
const depth = self.format.depth();
|
||||||
var i: u32 = 0;
|
var i: u32 = 0;
|
||||||
while (i < reg.height) : (i += 1) {
|
while (i < reg.height) : (i += 1) {
|
||||||
const tex_offset = (((reg.y + i) * self.size) + reg.x) * depth;
|
const tex_offset = (((reg.y + i) * self.size) + reg.x) * depth;
|
||||||
@ -245,7 +253,7 @@ pub fn grow(self: *Atlas, alloc: Allocator, size_new: u32) Allocator.Error!void
|
|||||||
const size_old = self.size;
|
const size_old = self.size;
|
||||||
|
|
||||||
// Allocate our new data
|
// Allocate our new data
|
||||||
self.data = try alloc.alloc(u8, size_new * size_new * @enumToInt(self.format));
|
self.data = try alloc.alloc(u8, size_new * size_new * self.format.depth());
|
||||||
defer alloc.free(data_old);
|
defer alloc.free(data_old);
|
||||||
errdefer {
|
errdefer {
|
||||||
alloc.free(self.data);
|
alloc.free(self.data);
|
||||||
@ -270,7 +278,7 @@ pub fn grow(self: *Atlas, alloc: Allocator, size_new: u32) Allocator.Error!void
|
|||||||
.y = 1, // skip the first border row
|
.y = 1, // skip the first border row
|
||||||
.width = size_old,
|
.width = size_old,
|
||||||
.height = size_old - 2, // skip the last border row
|
.height = size_old - 2, // skip the last border row
|
||||||
}, data_old[size_old * @enumToInt(self.format) ..]);
|
}, data_old[size_old * self.format.depth() ..]);
|
||||||
|
|
||||||
// We are both modified and resized
|
// We are both modified and resized
|
||||||
self.modified = true;
|
self.modified = true;
|
||||||
@ -380,7 +388,7 @@ test "writing RGB data" {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 33 because of the 1px border and so on
|
// 33 because of the 1px border and so on
|
||||||
const depth = @intCast(usize, @enumToInt(atlas.format));
|
const depth = @intCast(usize, atlas.format.depth());
|
||||||
try testing.expectEqual(@as(u8, 1), atlas.data[33 * depth]);
|
try testing.expectEqual(@as(u8, 1), atlas.data[33 * depth]);
|
||||||
try testing.expectEqual(@as(u8, 2), atlas.data[33 * depth + 1]);
|
try testing.expectEqual(@as(u8, 2), atlas.data[33 * depth + 1]);
|
||||||
try testing.expectEqual(@as(u8, 3), atlas.data[33 * depth + 2]);
|
try testing.expectEqual(@as(u8, 3), atlas.data[33 * depth + 2]);
|
||||||
@ -410,7 +418,7 @@ test "grow RGB" {
|
|||||||
|
|
||||||
// Our top left skips the first row (size * depth) and the first
|
// Our top left skips the first row (size * depth) and the first
|
||||||
// column (depth) for the 1px border.
|
// column (depth) for the 1px border.
|
||||||
const depth = @intCast(usize, @enumToInt(atlas.format));
|
const depth = @intCast(usize, atlas.format.depth());
|
||||||
var tl = (atlas.size * depth) + depth;
|
var tl = (atlas.size * depth) + depth;
|
||||||
try testing.expectEqual(@as(u8, 10), atlas.data[tl]);
|
try testing.expectEqual(@as(u8, 10), atlas.data[tl]);
|
||||||
try testing.expectEqual(@as(u8, 11), atlas.data[tl + 1]);
|
try testing.expectEqual(@as(u8, 11), atlas.data[tl + 1]);
|
||||||
|
@ -15,6 +15,7 @@ const Allocator = std.mem.Allocator;
|
|||||||
const Atlas = @import("../Atlas.zig");
|
const Atlas = @import("../Atlas.zig");
|
||||||
const Glyph = @import("main.zig").Glyph;
|
const Glyph = @import("main.zig").Glyph;
|
||||||
const Library = @import("main.zig").Library;
|
const Library = @import("main.zig").Library;
|
||||||
|
const convert = @import("convert.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.font_face);
|
const log = std.log.scoped(.font_face);
|
||||||
|
|
||||||
@ -120,30 +121,60 @@ pub fn renderGlyph(self: Face, alloc: Allocator, atlas: *Atlas, glyph_index: u32
|
|||||||
});
|
});
|
||||||
|
|
||||||
const glyph = self.face.handle.*.glyph;
|
const glyph = self.face.handle.*.glyph;
|
||||||
const bitmap = glyph.*.bitmap;
|
const bitmap_ft = glyph.*.bitmap;
|
||||||
|
|
||||||
|
// This bitmap is blank. I've seen it happen in a font, I don't know why.
|
||||||
|
// If it is empty, we just return a valid glyph struct that does nothing.
|
||||||
|
if (bitmap_ft.rows == 0) return Glyph{
|
||||||
|
.width = 0,
|
||||||
|
.height = 0,
|
||||||
|
.offset_x = 0,
|
||||||
|
.offset_y = 0,
|
||||||
|
.atlas_x = 0,
|
||||||
|
.atlas_y = 0,
|
||||||
|
.advance_x = 0,
|
||||||
|
};
|
||||||
|
|
||||||
// Ensure we know how to work with the font format. And assure that
|
// Ensure we know how to work with the font format. And assure that
|
||||||
// or color depth is as expected on the texture atlas.
|
// or color depth is as expected on the texture atlas. If format is null
|
||||||
const format: Atlas.Format = switch (bitmap.pixel_mode) {
|
// it means there is no native color format for our Atlas and we must try
|
||||||
|
// conversion.
|
||||||
|
const format: ?Atlas.Format = switch (bitmap_ft.pixel_mode) {
|
||||||
freetype.c.FT_PIXEL_MODE_GRAY => .greyscale,
|
freetype.c.FT_PIXEL_MODE_GRAY => .greyscale,
|
||||||
freetype.c.FT_PIXEL_MODE_BGRA => .rgba,
|
freetype.c.FT_PIXEL_MODE_BGRA => .rgba,
|
||||||
else => {
|
else => {
|
||||||
log.warn("pixel mode={}", .{bitmap.pixel_mode});
|
log.warn("glyph={} pixel mode={}", .{ glyph_index, bitmap_ft.pixel_mode });
|
||||||
@panic("unsupported pixel mode");
|
@panic("unsupported pixel mode");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
assert(atlas.format == format);
|
|
||||||
|
|
||||||
const src_w = bitmap.width;
|
// If our atlas format doesn't match, look for conversions if possible.
|
||||||
const src_h = bitmap.rows;
|
const bitmap_converted = if (format == null or atlas.format != format.?) blk: {
|
||||||
const tgt_w = src_w;
|
const func = convert.map[bitmap_ft.pixel_mode].get(atlas.format) orelse {
|
||||||
const tgt_h = src_h;
|
log.warn("glyph={} pixel mode={}", .{ glyph_index, bitmap_ft.pixel_mode });
|
||||||
|
return error.UnsupportedPixelMode;
|
||||||
|
};
|
||||||
|
|
||||||
|
log.warn("converting from pixel_mode={} to atlas_format={}", .{
|
||||||
|
bitmap_ft.pixel_mode,
|
||||||
|
atlas.format,
|
||||||
|
});
|
||||||
|
break :blk try func(alloc, bitmap_ft);
|
||||||
|
} else null;
|
||||||
|
defer if (bitmap_converted) |bm| {
|
||||||
|
const len = bm.width * bm.rows * atlas.format.depth();
|
||||||
|
alloc.free(bm.buffer[0..len]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const bitmap = bitmap_converted orelse bitmap_ft;
|
||||||
|
const tgt_w = bitmap.width;
|
||||||
|
const tgt_h = bitmap.rows;
|
||||||
|
|
||||||
const region = try atlas.reserve(alloc, tgt_w, tgt_h);
|
const region = try atlas.reserve(alloc, tgt_w, tgt_h);
|
||||||
|
|
||||||
// If we have data, copy it into the atlas
|
// If we have data, copy it into the atlas
|
||||||
if (region.width > 0 and region.height > 0) {
|
if (region.width > 0 and region.height > 0) {
|
||||||
const depth = @enumToInt(format);
|
const depth = atlas.format.depth();
|
||||||
|
|
||||||
// We can avoid a buffer copy if our atlas width and bitmap
|
// We can avoid a buffer copy if our atlas width and bitmap
|
||||||
// width match and the bitmap pitch is just the width (meaning
|
// width match and the bitmap pitch is just the width (meaning
|
||||||
@ -156,7 +187,7 @@ pub fn renderGlyph(self: Face, alloc: Allocator, atlas: *Atlas, glyph_index: u32
|
|||||||
var dst_ptr = temp;
|
var dst_ptr = temp;
|
||||||
var src_ptr = bitmap.buffer;
|
var src_ptr = bitmap.buffer;
|
||||||
var i: usize = 0;
|
var i: usize = 0;
|
||||||
while (i < src_h) : (i += 1) {
|
while (i < bitmap.rows) : (i += 1) {
|
||||||
std.mem.copy(u8, dst_ptr, src_ptr[0 .. bitmap.width * depth]);
|
std.mem.copy(u8, dst_ptr, src_ptr[0 .. bitmap.width * depth]);
|
||||||
dst_ptr = dst_ptr[tgt_w * depth ..];
|
dst_ptr = dst_ptr[tgt_w * depth ..];
|
||||||
src_ptr += @intCast(usize, bitmap.pitch);
|
src_ptr += @intCast(usize, bitmap.pitch);
|
||||||
@ -232,3 +263,20 @@ test "color emoji" {
|
|||||||
|
|
||||||
_ = try font.renderGlyph(alloc, &atlas, font.glyphIndex('🥸').?);
|
_ = try font.renderGlyph(alloc, &atlas, font.glyphIndex('🥸').?);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "mono to rgba" {
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
const testFont = @import("test.zig").fontEmoji;
|
||||||
|
|
||||||
|
var lib = try Library.init();
|
||||||
|
defer lib.deinit();
|
||||||
|
|
||||||
|
var atlas = try Atlas.init(alloc, 512, .rgba);
|
||||||
|
defer atlas.deinit(alloc);
|
||||||
|
|
||||||
|
var font = try init(lib, testFont, .{ .points = 12 });
|
||||||
|
defer font.deinit();
|
||||||
|
|
||||||
|
// glyph 3 is mono in Noto
|
||||||
|
_ = try font.renderGlyph(alloc, &atlas, 3);
|
||||||
|
}
|
||||||
|
@ -27,4 +27,5 @@ pub const Metrics = struct {
|
|||||||
|
|
||||||
test {
|
test {
|
||||||
@import("std").testing.refAllDecls(@This());
|
@import("std").testing.refAllDecls(@This());
|
||||||
|
_ = @import("convert.zig");
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user