diff --git a/src/Atlas.zig b/src/Atlas.zig index 101339c20..a910d16a9 100644 --- a/src/Atlas.zig +++ b/src/Atlas.zig @@ -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]); diff --git a/src/font/Face.zig b/src/font/Face.zig index c90ae41dc..2e82f1f23 100644 --- a/src/font/Face.zig +++ b/src/font/Face.zig @@ -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); +} diff --git a/src/font/main.zig b/src/font/main.zig index e4469eb2e..690f535fa 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -27,4 +27,5 @@ pub const Metrics = struct { test { @import("std").testing.refAllDecls(@This()); + _ = @import("convert.zig"); }