From 2e1bc7bb016f4b19ac06a491df68c64aef46e057 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 16 Oct 2022 20:47:21 -0700 Subject: [PATCH] Bring back freetype font bitmap conversion Monaco on Mac is mono --- pkg/freetype/bitmap.zig | 23 +++++++ pkg/freetype/main.zig | 1 + src/Window.zig | 2 +- src/font/DeferredFace.zig | 1 + src/font/face/freetype.zig | 25 +++++++- src/font/face/freetype_convert.zig | 100 +++++++++++++++++++++++++++++ 6 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 pkg/freetype/bitmap.zig create mode 100644 src/font/face/freetype_convert.zig diff --git a/pkg/freetype/bitmap.zig b/pkg/freetype/bitmap.zig new file mode 100644 index 000000000..cec0aa8ca --- /dev/null +++ b/pkg/freetype/bitmap.zig @@ -0,0 +1,23 @@ +const std = @import("std"); +const c = @import("c.zig"); +const freetype = @import("main.zig"); +const errors = @import("errors.zig"); +const Error = errors.Error; +const intToError = errors.intToError; + +/// Convert a bitmap object with depth 1bpp, 2bpp, 4bpp, 8bpp or 32bpp to a +/// bitmap object with depth 8bpp, making the number of used bytes per line +/// (a.k.a. the ‘pitch’) a multiple of alignment. +pub fn bitmapConvert( + lib: freetype.Library, + source: *const c.FT_Bitmap, + target: *c.FT_Bitmap, + alignment: u32, +) Error!void { + try intToError(c.FT_Bitmap_Convert( + lib.handle, + source, + target, + alignment, + )); +} diff --git a/pkg/freetype/main.zig b/pkg/freetype/main.zig index 4adfeeaf4..08c924d0b 100644 --- a/pkg/freetype/main.zig +++ b/pkg/freetype/main.zig @@ -1,6 +1,7 @@ pub const c = @import("c.zig"); pub const testing = @import("test.zig"); pub const Library = @import("Library.zig"); +pub usingnamespace @import("bitmap.zig"); pub usingnamespace @import("computations.zig"); pub usingnamespace @import("errors.zig"); pub usingnamespace @import("face.zig"); diff --git a/src/Window.zig b/src/Window.zig index 2680ebd27..9696af2a7 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -1902,7 +1902,7 @@ pub fn invokeCharset( self.terminal.invokeCharset(active, slot, single); } -const face_ttf = @embedFile("font/res/FiraCode-Regular.ttf"); +const face_ttf = @embedFile("font/res/Monaco-Regular.ttf"); const face_bold_ttf = @embedFile("font/res/FiraCode-Bold.ttf"); const face_emoji_ttf = @embedFile("font/res/NotoColorEmoji.ttf"); const face_emoji_text_ttf = @embedFile("font/res/NotoEmoji-Regular.ttf"); diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index 0407ec3c9..64a9f6e94 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -190,6 +190,7 @@ fn loadCoreTextFreetype( // TODO: face index 0 is not correct long term and we should switch // to using CoreText for rendering, too. + //std.log.warn("path={s}", .{path_slice}); self.face = try Face.initFile(lib, buf[0..path_slice.len :0], 0, size); } diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 5038a2c4d..e168d50da 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -16,6 +16,7 @@ const font = @import("../main.zig"); const Glyph = font.Glyph; const Library = font.Library; const Presentation = font.Presentation; +const convert = @import("freetype_convert.zig"); const log = std.log.scoped(.font_face); @@ -143,7 +144,8 @@ pub const Face = struct { // 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) { + const format: ?Atlas.Format = switch (bitmap_ft.pixel_mode) { + freetype.c.FT_PIXEL_MODE_MONO => null, freetype.c.FT_PIXEL_MODE_GRAY => .greyscale, freetype.c.FT_PIXEL_MODE_BGRA => .rgba, else => { @@ -151,9 +153,26 @@ pub const Face = struct { @panic("unsupported pixel mode"); }, }; - assert(atlas.format == format); - const bitmap = bitmap_ft; + // 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; diff --git a/src/font/face/freetype_convert.zig b/src/font/face/freetype_convert.zig new file mode 100644 index 000000000..fd819d7d8 --- /dev/null +++ b/src/font/face/freetype_convert.zig @@ -0,0 +1,100 @@ +//! Various conversions from Freetype formats to Atlas formats. These are +//! currently implemented naively. There are definitely MUCH faster ways +//! to do this (likely using SIMD), but I started simple. +const std = @import("std"); +const freetype = @import("freetype"); +const Atlas = @import("../../Atlas.zig"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +/// The mapping from freetype format to atlas format. +pub const map = genMap(); + +/// The map type. +pub const Map = [freetype.c.FT_PIXEL_MODE_MAX]AtlasArray; + +/// Conversion function type. The returning bitmap buffer is guaranteed +/// to be exactly `width * rows * depth` long for freeing it. The caller must +/// free the bitmap buffer. The depth is the depth of the atlas format in the +/// map. +pub const Func = std.meta.FnPtr(fn (Allocator, Bitmap) Allocator.Error!Bitmap); + +/// Alias for the freetype FT_Bitmap type to make it easier to type. +pub const Bitmap = freetype.c.struct_FT_Bitmap_; + +const AtlasArray = std.EnumArray(Atlas.Format, ?Func); + +fn genMap() Map { + var result: Map = undefined; + + // Initialize to no converter + var i: usize = 0; + while (i < freetype.c.FT_PIXEL_MODE_MAX) : (i += 1) { + result[i] = AtlasArray.initFill(null); + } + + // Map our converters + result[freetype.c.FT_PIXEL_MODE_MONO].set(.greyscale, monoToGreyscale); + + return result; +} + +pub fn monoToGreyscale(alloc: Allocator, bm: Bitmap) Allocator.Error!Bitmap { + var buf = try alloc.alloc(u8, bm.width * bm.rows); + errdefer alloc.free(buf); + + // width divided by 8 because each byte has 8 pixels. This is therefore + // the number of bytes in each row. + const bytes_per_row = bm.width >> 3; + + var source_i: usize = 0; + var target_i: usize = 0; + var i: usize = bm.rows; + while (i > 0) : (i -= 1) { + var j: usize = bytes_per_row; + while (j > 0) : (j -= 1) { + var bit: u4 = 8; + while (bit > 0) : (bit -= 1) { + const mask = @as(u8, 1) << @intCast(u3, bit - 1); + const bitval: u8 = if (bm.buffer[source_i + (j - 1)] & mask > 0) 0xFF else 0; + buf[target_i] = bitval; + target_i += 1; + } + } + + source_i += @intCast(usize, bm.pitch); + } + + var copy = bm; + copy.buffer = buf.ptr; + copy.pixel_mode = freetype.c.FT_PIXEL_MODE_GRAY; + copy.pitch = @intCast(c_int, bm.width); + return copy; +} + +test { + // Force comptime to run + _ = map; +} + +test "mono to greyscale" { + const testing = std.testing; + const alloc = testing.allocator; + + var mono_data = [_]u8{0b1010_0101}; + const source: Bitmap = .{ + .rows = 1, + .width = 8, + .pitch = 1, + .buffer = @ptrCast([*c]u8, &mono_data), + .num_grays = 0, + .pixel_mode = freetype.c.FT_PIXEL_MODE_MONO, + .palette_mode = 0, + .palette = null, + }; + + const result = try monoToGreyscale(alloc, source); + defer alloc.free(result.buffer[0..(result.width * result.rows)]); + try testing.expect(result.pixel_mode == freetype.c.FT_PIXEL_MODE_GRAY); + try testing.expectEqual(@as(u8, 255), result.buffer[0]); +}