mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 08:46:08 +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.
|
||||
resized: bool = false,
|
||||
|
||||
pub const Format = enum(u3) {
|
||||
greyscale = 1,
|
||||
rgb = 3,
|
||||
rgba = 4,
|
||||
pub const Format = enum {
|
||||
greyscale,
|
||||
rgb,
|
||||
rgba,
|
||||
|
||||
pub fn depth(self: Format) u8 {
|
||||
return switch (self) {
|
||||
.greyscale => 1,
|
||||
.rgb => 3,
|
||||
.rgba => 4,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const Node = struct {
|
||||
@ -76,7 +84,7 @@ pub const Region = struct {
|
||||
|
||||
pub fn init(alloc: Allocator, size: u32, format: Format) !Atlas {
|
||||
var result = Atlas{
|
||||
.data = try alloc.alloc(u8, size * size * @enumToInt(format)),
|
||||
.data = try alloc.alloc(u8, size * size * format.depth()),
|
||||
.size = size,
|
||||
.nodes = .{},
|
||||
.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 + reg.height) <= (self.size - 1));
|
||||
|
||||
const depth = @enumToInt(self.format);
|
||||
const depth = self.format.depth();
|
||||
var i: u32 = 0;
|
||||
while (i < reg.height) : (i += 1) {
|
||||
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;
|
||||
|
||||
// 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);
|
||||
errdefer {
|
||||
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
|
||||
.width = size_old,
|
||||
.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
|
||||
self.modified = true;
|
||||
@ -380,7 +388,7 @@ test "writing RGB data" {
|
||||
});
|
||||
|
||||
// 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, 2), atlas.data[33 * depth + 1]);
|
||||
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
|
||||
// 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;
|
||||
try testing.expectEqual(@as(u8, 10), atlas.data[tl]);
|
||||
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 Glyph = @import("main.zig").Glyph;
|
||||
const Library = @import("main.zig").Library;
|
||||
const convert = @import("convert.zig");
|
||||
|
||||
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 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
|
||||
// or color depth is as expected on the texture atlas.
|
||||
const format: Atlas.Format = switch (bitmap.pixel_mode) {
|
||||
// or color depth is as expected on the texture atlas. If format is null
|
||||
// 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_BGRA => .rgba,
|
||||
else => {
|
||||
log.warn("pixel mode={}", .{bitmap.pixel_mode});
|
||||
log.warn("glyph={} pixel mode={}", .{ glyph_index, bitmap_ft.pixel_mode });
|
||||
@panic("unsupported pixel mode");
|
||||
},
|
||||
};
|
||||
assert(atlas.format == format);
|
||||
|
||||
const src_w = bitmap.width;
|
||||
const src_h = bitmap.rows;
|
||||
const tgt_w = src_w;
|
||||
const tgt_h = src_h;
|
||||
// If our atlas format doesn't match, look for conversions if possible.
|
||||
const bitmap_converted = if (format == null or atlas.format != format.?) blk: {
|
||||
const func = convert.map[bitmap_ft.pixel_mode].get(atlas.format) orelse {
|
||||
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);
|
||||
|
||||
// If we have data, copy it into the atlas
|
||||
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
|
||||
// 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 src_ptr = bitmap.buffer;
|
||||
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]);
|
||||
dst_ptr = dst_ptr[tgt_w * depth ..];
|
||||
src_ptr += @intCast(usize, bitmap.pitch);
|
||||
@ -232,3 +263,20 @@ test "color emoji" {
|
||||
|
||||
_ = 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 {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
_ = @import("convert.zig");
|
||||
}
|
||||
|
Reference in New Issue
Block a user