convert src/font to use new pkg/freetype

This commit is contained in:
Mitchell Hashimoto
2022-08-28 22:22:16 -07:00
parent 28e9619361
commit f1abca51f7
8 changed files with 109 additions and 118 deletions

View File

@ -106,6 +106,7 @@ pub const LoadFlags = packed struct {
monochrome: bool = false,
linear_design: bool = false,
no_autohint: bool = false,
_padding1: u1 = 0,
target_normal: bool = false,
target_light: bool = false,
target_mono: bool = false,
@ -114,7 +115,7 @@ pub const LoadFlags = packed struct {
color: bool = false,
compute_metrics: bool = false,
bitmap_metrics_only: bool = false,
_padding: u10 = 0,
_padding2: u9 = 0,
test {
// This must always be an i32 size so we can bitcast directly.
@ -124,11 +125,12 @@ pub const LoadFlags = packed struct {
test "bitcast" {
const testing = std.testing;
const cval: i32 = c.FT_LOAD_RENDER | c.FT_LOAD_PEDANTIC;
const cval: i32 = c.FT_LOAD_RENDER | c.FT_LOAD_PEDANTIC | c.FT_LOAD_COLOR;
const flags = @bitCast(LoadFlags, cval);
try testing.expect(!flags.no_hinting);
try testing.expect(flags.render);
try testing.expect(flags.pedantic);
try testing.expect(flags.color);
}
};

View File

@ -44,6 +44,7 @@ texture: gl.Texture,
texture_color: gl.Texture,
/// The font atlas.
font_lib: font.Library,
font_set: font.FallbackSet,
/// Whether the cursor is visible or not. This is used to control cursor
@ -151,9 +152,12 @@ pub fn init(
try font_set.families.ensureTotalCapacity(alloc, 2);
errdefer font_set.deinit(alloc);
var font_lib = try font.Library.init();
errdefer font_lib.deinit();
// Regular text
font_set.families.appendAssumeCapacity(fam: {
var fam = try font.Family.init(atlas);
var fam = font.Family.init(font_lib, atlas);
errdefer fam.deinit(alloc);
try fam.loadFaceFromMemory(.regular, face_ttf, font_size);
try fam.loadFaceFromMemory(.bold, face_bold_ttf, font_size);
@ -162,7 +166,7 @@ pub fn init(
// Emoji
font_set.families.appendAssumeCapacity(fam: {
var fam_emoji = try font.Family.init(atlas_color);
var fam_emoji = font.Family.init(font_lib, atlas_color);
errdefer fam_emoji.deinit(alloc);
try fam_emoji.loadFaceFromMemory(.regular, face_emoji_ttf, font_size);
break :fam fam_emoji;
@ -189,11 +193,11 @@ pub fn init(
const fam = &font_set.families.items[0];
// This is the height reported by the font face
const face_height: i32 = fam.regular.?.unitsToPxY(fam.regular.?.ft_face.*.height);
const face_height: i32 = fam.regular.?.unitsToPxY(fam.regular.?.face.?.handle.*.height);
// Determine the height of the underscore char
const glyph = font_set.families.items[0].getGlyph('_', .regular).?;
var res: i32 = fam.regular.?.unitsToPxY(fam.regular.?.ft_face.*.ascender);
var res: i32 = fam.regular.?.unitsToPxY(fam.regular.?.face.?.handle.*.ascender);
res -= glyph.offset_y;
res += @intCast(i32, glyph.height);
@ -207,7 +211,7 @@ pub fn init(
const fam = &font_set.families.items[0];
break :cell_baseline cell_height - @intToFloat(
f32,
fam.regular.?.unitsToPxY(fam.regular.?.ft_face.*.ascender),
fam.regular.?.unitsToPxY(fam.regular.?.face.?.handle.*.ascender),
);
};
log.debug("cell dimensions w={d} h={d} baseline={d}", .{ cell_width, cell_height, cell_baseline });
@ -331,6 +335,7 @@ pub fn init(
.vbo = vbo,
.texture = tex,
.texture_color = tex_color,
.font_lib = font_lib,
.font_set = font_set,
.cursor_visible = true,
.cursor_style = .box,
@ -345,6 +350,7 @@ pub fn deinit(self: *Grid) void {
family.deinit(self.alloc);
}
self.font_set.deinit(self.alloc);
self.font_lib.deinit();
self.texture.destroy();
self.texture_color.destroy();

View File

@ -240,8 +240,8 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo
const window_size = try window.getSize();
var grid = try Grid.init(alloc, .{
.points = config.@"font-size",
.xdpi = @floatToInt(u32, x_dpi),
.ydpi = @floatToInt(u32, y_dpi),
.xdpi = @floatToInt(u16, x_dpi),
.ydpi = @floatToInt(u16, y_dpi),
});
try grid.setScreenSize(.{ .width = window_size.width, .height = window_size.height });
grid.background = .{

View File

@ -7,53 +7,49 @@ const Face = @This();
const std = @import("std");
const builtin = @import("builtin");
const freetype = @import("freetype");
const assert = std.debug.assert;
const testing = std.testing;
const Allocator = std.mem.Allocator;
const ftc = @import("freetype").c;
const Atlas = @import("../Atlas.zig");
const Glyph = @import("main.zig").Glyph;
const Library = @import("main.zig").Library;
const ftok = ftc.FT_Err_Ok;
const log = std.log.scoped(.font_face);
/// The FreeType library
ft_library: ftc.FT_Library,
/// The core library
library: Library,
/// Our font face.
ft_face: ftc.FT_Face = null,
face: ?freetype.Face = null,
/// If a DPI can't be calculated, this DPI is used. This is probably
/// wrong on modern devices so it is highly recommended you get the DPI
/// using whatever platform method you can.
pub const default_dpi = if (builtin.os.tag == .macos) 72 else 96;
pub fn init(lib: ftc.FT_Library) !Face {
pub fn init(lib: Library) !Face {
return Face{
.ft_library = lib,
.library = lib,
};
}
pub fn deinit(self: *Face) void {
if (self.ft_face != null) {
if (ftc.FT_Done_Face(self.ft_face) != ftok)
log.err("failed to clean up font face", .{});
}
if (self.face) |face| face.deinit();
self.* = undefined;
}
/// The desired size for loading a font.
pub const DesiredSize = struct {
// Desired size in points
points: u32,
points: u16,
// The DPI of the screen so we can convert points to pixels.
xdpi: u32 = default_dpi,
ydpi: u32 = default_dpi,
xdpi: u16 = default_dpi,
ydpi: u16 = default_dpi,
// Converts points to pixels
pub fn pixels(self: DesiredSize) u32 {
pub fn pixels(self: DesiredSize) u16 {
// 1 point = 1/72 inch
return (self.points * self.ydpi) / 72;
}
@ -65,86 +61,66 @@ pub fn loadFaceFromMemory(
source: [:0]const u8,
size: DesiredSize,
) !void {
assert(self.ft_face == null);
assert(self.face == null);
if (ftc.FT_New_Memory_Face(
self.ft_library,
source.ptr,
@intCast(c_long, source.len),
0,
&self.ft_face,
) != ftok) return error.FaceLoadFailed;
errdefer {
_ = ftc.FT_Done_Face(self.ft_face);
self.ft_face = null;
}
const face = try self.library.lib.initMemoryFace(source, 0);
errdefer face.deinit();
if (ftc.FT_Select_Charmap(self.ft_face, ftc.FT_ENCODING_UNICODE) != ftok)
return error.FaceLoadFailed;
try face.selectCharmap(.unicode);
// If we have fixed sizes, we just have to try to pick the one closest
// to what the user requested. Otherwise, we can choose an arbitrary
// pixel size.
if (!ftc.FT_HAS_FIXED_SIZES(self.ft_face)) {
const size_26dot6 = size.points << 6; // mult by 64
if (ftc.FT_Set_Char_Size(self.ft_face, 0, size_26dot6, size.xdpi, size.ydpi) != ftok)
return error.FaceLoadFailed;
} else try self.selectSizeNearest(size.pixels());
if (!face.hasFixedSizes()) {
const size_26dot6 = @intCast(i32, size.points << 6); // mult by 64
try face.setCharSize(0, size_26dot6, size.xdpi, size.ydpi);
} else try selectSizeNearest(face, size.pixels());
// Success, persist
self.face = face;
}
/// Selects the fixed size in the loaded face that is closest to the
/// requested pixel size.
fn selectSizeNearest(self: *Face, size: u32) !void {
var i: usize = 0;
var best_i: usize = 0;
fn selectSizeNearest(face: freetype.Face, size: u32) !void {
var i: i32 = 0;
var best_i: i32 = 0;
var best_diff: i32 = 0;
while (i < self.ft_face.*.num_fixed_sizes) : (i += 1) {
const diff = @intCast(i32, size) - @intCast(i32, self.ft_face.*.available_sizes[i].width);
while (i < face.handle.*.num_fixed_sizes) : (i += 1) {
const width = face.handle.*.available_sizes[@intCast(usize, i)].width;
const diff = @intCast(i32, size) - @intCast(i32, width);
if (i == 0 or diff < best_diff) {
best_diff = diff;
best_i = i;
}
}
if (ftc.FT_Select_Size(self.ft_face, @intCast(c_int, best_i)) != ftok)
return error.FaceSelectSizeFailed;
try face.selectSize(best_i);
}
/// Load a glyph for this face. The codepoint can be either a u8 or
/// []const u8 depending on if you know it is ASCII or must be UTF-8 decoded.
pub fn loadGlyph(self: Face, alloc: Allocator, atlas: *Atlas, cp: u32) !Glyph {
assert(self.ft_face != null);
const face = self.face.?;
// We need a UTF32 codepoint for freetype
const glyph_index = glyph_index: {
//log.warn("glyph load: {x}", .{cp});
const idx = ftc.FT_Get_Char_Index(self.ft_face, cp);
if (idx > 0) break :glyph_index idx;
// Unknown glyph.
//log.warn("glyph not found: {x}", .{cp});
return error.GlyphNotFound;
};
const glyph_index = face.getCharIndex(cp) orelse return error.GlyphNotFound;
//log.warn("glyph index: {}", .{glyph_index});
// If our glyph has color, we want to render the color
var load_flags: c_int = ftc.FT_LOAD_RENDER;
if (ftc.FT_HAS_COLOR(self.ft_face)) load_flags |= @intCast(c_int, ftc.FT_LOAD_COLOR);
try face.loadGlyph(glyph_index, .{
.render = true,
.color = face.hasColor(),
});
if (ftc.FT_Load_Glyph(
self.ft_face,
glyph_index,
load_flags,
) != ftok) return error.LoadGlyphFailed;
const glyph = self.ft_face.*.glyph;
const glyph = face.handle.*.glyph;
const bitmap = glyph.*.bitmap;
// 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) {
ftc.FT_PIXEL_MODE_GRAY => .greyscale,
ftc.FT_PIXEL_MODE_BGRA => .rgba,
freetype.c.FT_PIXEL_MODE_GRAY => .greyscale,
freetype.c.FT_PIXEL_MODE_BGRA => .rgba,
else => {
log.warn("pixel mode={}", .{bitmap.pixel_mode});
@panic("unsupported pixel mode");
@ -204,27 +180,28 @@ pub fn loadGlyph(self: Face, alloc: Allocator, atlas: *Atlas, cp: u32) !Glyph {
/// Convert 16.6 pixel format to pixels based on the scale factor of the
/// current font size.
pub fn unitsToPxY(self: Face, units: i32) i32 {
return @intCast(i32, ftc.FT_MulFix(units, self.ft_face.*.size.*.metrics.y_scale) >> 6);
return @intCast(i32, freetype.mulFix(
units,
@intCast(i32, self.face.?.handle.*.size.*.metrics.y_scale),
) >> 6);
}
/// Convert 26.6 pixel format to f32
fn f26dot6ToFloat(v: ftc.FT_F26Dot6) f32 {
fn f26dot6ToFloat(v: freetype.c.FT_F26Dot6) f32 {
return @intToFloat(f32, v >> 6);
}
test {
const testFont = @import("test.zig").fontRegular;
var ft_lib: ftc.FT_Library = undefined;
if (ftc.FT_Init_FreeType(&ft_lib) != ftok)
return error.FreeTypeInitFailed;
defer _ = ftc.FT_Done_FreeType(ft_lib);
const alloc = testing.allocator;
var lib = try Library.init();
defer lib.deinit();
var atlas = try Atlas.init(alloc, 512, .greyscale);
defer atlas.deinit(alloc);
var font = try init(ft_lib);
var font = try init(lib);
defer font.deinit();
try font.loadFaceFromMemory(testFont, .{ .points = 12 });
@ -237,18 +214,16 @@ test {
}
test "color emoji" {
const alloc = testing.allocator;
const testFont = @import("test.zig").fontEmoji;
var ft_lib: ftc.FT_Library = undefined;
if (ftc.FT_Init_FreeType(&ft_lib) != ftok)
return error.FreeTypeInitFailed;
defer _ = ftc.FT_Done_FreeType(ft_lib);
var lib = try Library.init();
defer lib.deinit();
const alloc = testing.allocator;
var atlas = try Atlas.init(alloc, 512, .rgba);
defer atlas.deinit(alloc);
var font = try init(ft_lib);
var font = try init(lib);
defer font.deinit();
try font.loadFaceFromMemory(testFont, .{ .points = 12 });

View File

@ -9,6 +9,7 @@ const Allocator = std.mem.Allocator;
const Atlas = @import("../Atlas.zig");
const Family = @import("main.zig").Family;
const Library = @import("main.zig").Library;
const Glyph = @import("main.zig").Glyph;
const Style = @import("main.zig").Style;
const codepoint = @import("main.zig").codepoint;
@ -123,14 +124,17 @@ test {
const testing = std.testing;
const alloc = testing.allocator;
var lib = try Library.init();
defer lib.deinit();
var set: FallbackSet = .{};
try set.families.append(alloc, fam: {
var fam = try Family.init(try Atlas.init(alloc, 512, .greyscale));
var fam = Family.init(lib, try Atlas.init(alloc, 512, .greyscale));
try fam.loadFaceFromMemory(.regular, fontRegular, .{ .points = 48 });
break :fam fam;
});
try set.families.append(alloc, fam: {
var fam = try Family.init(try Atlas.init(alloc, 512, .rgba));
var fam = Family.init(lib, try Atlas.init(alloc, 512, .rgba));
try fam.loadFaceFromMemory(.regular, fontEmoji, .{ .points = 48 });
break :fam fam;
});

View File

@ -5,27 +5,22 @@ const Family = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const Atlas = @import("../Atlas.zig");
const ftc = @import("freetype").c;
const ftok = ftc.FT_Err_Ok;
const Face = @import("main.zig").Face;
const Glyph = @import("main.zig").Glyph;
const Style = @import("main.zig").Style;
const testFont = @import("test.zig").fontRegular;
const codepoint = @import("main.zig").codepoint;
const Library = @import("main.zig").Library;
const log = std.log.scoped(.font_family);
// NOTE(mitchellh): I think eventually atlas and the freetype lib fields
// move even higher up into another struct that manages sets of font families
// in order to support fallback and so on.
/// The texture atlas where all the font glyphs are rendered.
/// This is NOT owned by the Family, deinitialization must
/// be manually done.
atlas: Atlas,
/// The FreeType library, initialized by this init func.
ft_library: ftc.FT_Library,
/// The library shared state.
lib: Library,
/// The glyphs that are loaded into the atlas, keyed by codepoint.
glyphs: std.AutoHashMapUnmanaged(GlyphKey, Glyph) = .{},
@ -41,16 +36,11 @@ const GlyphKey = struct {
codepoint: u32,
};
pub fn init(atlas: Atlas) !Family {
var res = Family{
pub fn init(lib: Library, atlas: Atlas) Family {
return .{
.lib = lib,
.atlas = atlas,
.ft_library = undefined,
};
if (ftc.FT_Init_FreeType(&res.ft_library) != ftok)
return error.FreeTypeInitFailed;
return res;
}
pub fn deinit(self: *Family, alloc: Allocator) void {
@ -59,9 +49,6 @@ pub fn deinit(self: *Family, alloc: Allocator) void {
if (self.regular) |*face| face.deinit();
if (self.bold) |*face| face.deinit();
if (ftc.FT_Done_FreeType(self.ft_library) != ftok)
log.err("failed to clean up FreeType", .{});
self.* = undefined;
}
@ -74,7 +61,7 @@ pub fn loadFaceFromMemory(
source: [:0]const u8,
size: Face.DesiredSize,
) !void {
var face = try Face.init(self.ft_library);
var face = try Face.init(self.lib);
errdefer face.deinit();
try face.loadFaceFromMemory(source, size);
@ -145,7 +132,11 @@ pub fn addGlyph(self: *Family, alloc: Allocator, v: anytype, style: Style) !*Gly
test {
const testing = std.testing;
const alloc = testing.allocator;
var fam = try init(try Atlas.init(alloc, 512, .greyscale));
var lib = try Library.init();
defer lib.deinit();
var fam = init(lib, try Atlas.init(alloc, 512, .greyscale));
defer fam.deinit(alloc);
defer fam.atlas.deinit(alloc);
try fam.loadFaceFromMemory(.regular, testFont, .{ .points = 12 });

19
src/font/Library.zig Normal file
View File

@ -0,0 +1,19 @@
//! A library represents the shared state that the underlying font
//! library implementation(s) require per-process.
//!
//! In the future, this will be abstracted so that the underlying text
//! engine might not be Freetype and may be something like Core Text,
//! but the API will remain the same.
const Library = @This();
const freetype = @import("freetype");
lib: freetype.Library,
pub fn init() freetype.Error!Library {
return Library{ .lib = try freetype.Library.init() };
}
pub fn deinit(self: *Library) void {
self.lib.deinit();
}

View File

@ -4,10 +4,7 @@ pub const Face = @import("Face.zig");
pub const Family = @import("Family.zig");
pub const Glyph = @import("Glyph.zig");
pub const FallbackSet = @import("FallbackSet.zig");
/// Embedded fonts (for now)
pub const fontRegular = @import("test.zig").fontRegular;
pub const fontBold = @import("test.zig").fontBold;
pub const Library = @import("Library.zig");
/// The styles that a family can take.
pub const Style = enum {
@ -29,8 +26,5 @@ pub fn codepoint(v: anytype) u32 {
}
test {
_ = Face;
_ = Family;
_ = Glyph;
_ = FallbackSet;
@import("std").testing.refAllDecls(@This());
}