ghostty/src/font/Face.zig
2022-08-19 14:37:22 -07:00

165 lines
4.7 KiB
Zig

//! Face represents a single font face. A single font face has a single set
//! of properties associated with it such as style, weight, etc.
//!
//! A Face isn't typically meant to be used directly. It is usually used
//! via a Family in order to store it in an Atlas.
const Face = @This();
const std = @import("std");
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 ftok = ftc.FT_Err_Ok;
const log = std.log.scoped(.font_face);
/// The FreeType library
ft_library: ftc.FT_Library,
/// Our font face.
ft_face: ftc.FT_Face = null,
pub fn init(lib: ftc.FT_Library) !Face {
return Face{
.ft_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", .{});
}
self.* = undefined;
}
/// Loads a font to use.
///
/// This can only be called if a font is not already loaded.
pub fn loadFaceFromMemory(self: *Face, source: [:0]const u8, size: u32) !void {
assert(self.ft_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;
}
if (ftc.FT_Select_Charmap(self.ft_face, ftc.FT_ENCODING_UNICODE) != ftok)
return error.FaceLoadFailed;
if (ftc.FT_Set_Pixel_Sizes(self.ft_face, size, size) != ftok)
return error.FaceLoadFailed;
}
/// 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);
// 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});
// TODO: render something more identifiable than a space
break :glyph_index ftc.FT_Get_Char_Index(self.ft_face, ' ');
};
if (ftc.FT_Load_Glyph(
self.ft_face,
glyph_index,
ftc.FT_LOAD_RENDER,
) != ftok) return error.LoadGlyphFailed;
const glyph = self.ft_face.*.glyph;
const bitmap = glyph.*.bitmap;
const src_w = bitmap.width;
const src_h = bitmap.rows;
const tgt_w = src_w;
const tgt_h = src_h;
const region = try atlas.reserve(alloc, tgt_w, tgt_h);
// Build our buffer
//
// TODO(perf): we can avoid a buffer copy here in some cases where
// tgt_w == bitmap.width and bitmap.width == bitmap.pitch
const buffer = try alloc.alloc(u8, tgt_w * tgt_h);
defer alloc.free(buffer);
var dst_ptr = buffer;
var src_ptr = bitmap.buffer;
var i: usize = 0;
while (i < src_h) : (i += 1) {
std.mem.copy(u8, dst_ptr, src_ptr[0..bitmap.width]);
dst_ptr = dst_ptr[tgt_w..];
src_ptr += @intCast(usize, bitmap.pitch);
}
// Write the glyph information into the atlas
assert(region.width == tgt_w);
assert(region.height == tgt_h);
atlas.set(region, buffer);
// Store glyph metadata
return Glyph{
.width = tgt_w,
.height = tgt_h,
.offset_x = glyph.*.bitmap_left,
.offset_y = glyph.*.bitmap_top,
.atlas_x = region.x,
.atlas_y = region.y,
.advance_x = f26dot6ToFloat(glyph.*.advance.x),
};
}
/// 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);
}
/// Convert 26.6 pixel format to f32
fn f26dot6ToFloat(v: ftc.FT_F26Dot6) f32 {
return @intToFloat(f32, v >> 6);
}
test {
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 atlas = try Atlas.init(alloc, 512, .greyscale);
defer atlas.deinit(alloc);
var font = try init(ft_lib);
defer font.deinit();
try font.loadFaceFromMemory(testFont, 48);
// Generate all visible ASCII
var i: u8 = 32;
while (i < 127) : (i += 1) {
_ = try font.loadGlyph(alloc, &atlas, i);
}
}
const testFont = @embedFile("res/Inconsolata-Regular.ttf");