Font face handles zero-width glyphs (weird but happens)

This commit is contained in:
Mitchell Hashimoto
2022-09-05 22:53:00 -07:00
parent 90d250a3ba
commit 0d2c03c21c
3 changed files with 78 additions and 21 deletions

View File

@ -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]);

View File

@ -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);
}

View File

@ -27,4 +27,5 @@ pub const Metrics = struct {
test { test {
@import("std").testing.refAllDecls(@This()); @import("std").testing.refAllDecls(@This());
_ = @import("convert.zig");
} }