font: introduce Group which will eventually replace FallbackSet

This is more oriented around glyph indexes and also introduces an
important concept in the FontIndex which can be cached ahead of time so
that we can eventually break down text into runs for text shaping.
This commit is contained in:
Mitchell Hashimoto
2022-08-29 10:16:53 -07:00
parent 985b329c8a
commit a75035c042
3 changed files with 204 additions and 13 deletions

View File

@ -17,9 +17,6 @@ const Library = @import("main.zig").Library;
const log = std.log.scoped(.font_face);
/// The core library
library: Library,
/// Our font face.
face: freetype.Face,
@ -51,10 +48,7 @@ pub fn init(lib: Library, source: [:0]const u8, size: DesiredSize) !Face {
try face.selectCharmap(.unicode);
try setSize_(face, size);
return Face{
.library = lib,
.face = face,
};
return Face{ .face = face };
}
pub fn deinit(self: *Face) void {
@ -102,13 +96,21 @@ pub fn glyphIndex(self: Face, cp: u32) ?u32 {
return self.face.getCharIndex(cp);
}
/// Returns true if this font is colored. This can be used by callers to
/// determine what kind of atlas to pass in.
pub fn hasColor(self: Face) bool {
return self.face.hasColor();
}
/// 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 {
// We need a UTF32 codepoint for freetype
const glyph_index = self.glyphIndex(cp) orelse return error.GlyphNotFound;
//log.warn("glyph index: {}", .{glyph_index});
return self.renderGlyph(alloc, atlas, glyph_index);
}
pub fn renderGlyph(self: Face, alloc: Allocator, atlas: *Atlas, glyph_index: u32) !Glyph {
// If our glyph has color, we want to render the color
try self.face.loadGlyph(glyph_index, .{
.render = true,

188
src/font/Group.zig Normal file
View File

@ -0,0 +1,188 @@
//! A font group is a a set of multiple font faces of potentially different
//! styles that are used together to find glyphs. They usually share sizing
//! properties so that they can be used interchangably with each other in cases
//! a codepoint doesn't map cleanly. For example, if a user requests a bold
//! char and it doesn't exist we can fallback to a regular non-bold char so
//! we show SOMETHING.
const Group = @This();
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Atlas = @import("../Atlas.zig");
const Face = @import("main.zig").Face;
const Library = @import("main.zig").Library;
const Glyph = @import("main.zig").Glyph;
const Style = @import("main.zig").Style;
const codepoint = @import("main.zig").codepoint;
const log = std.log.scoped(.font_fallback);
/// Packed array to map our styles to a set of faces.
// Note: this is not the most efficient way to store these, but there is
// usually only one font group for the entire process so this isn't the
// most important memory efficiency we can look for. This is totally opaque
// to the user so we can change this later.
const StyleArray = std.EnumArray(Style, std.ArrayListUnmanaged(Face));
/// The available faces we have. This shouldn't be modified manually.
/// Instead, use the functions available on Group.
faces: StyleArray,
pub fn init(alloc: Allocator) !Group {
var result = Group{ .faces = undefined };
// Initialize all our styles to initially sized lists.
var i: usize = 0;
while (i < StyleArray.len) : (i += 1) {
result.faces.values[i] = .{};
try result.faces.values[i].ensureTotalCapacityPrecise(alloc, 2);
}
return result;
}
pub fn deinit(self: *Group, alloc: Allocator) void {
var it = self.faces.iterator();
while (it.next()) |entry| {
for (entry.value.items) |*item| item.deinit();
entry.value.deinit(alloc);
}
}
/// Add a face to the list for the given style. This face will be added as
/// next in priority if others exist already, i.e. it'll be the _last_ to be
/// searched for a glyph in that list.
///
/// The group takes ownership of the face. The face will be deallocated when
/// the group is deallocated.
pub fn addFace(self: *Group, alloc: Allocator, style: Style, face: Face) !void {
try self.faces.getPtr(style).append(alloc, face);
}
/// This represents a specific font in the group.
pub const FontIndex = packed struct {
/// The number of bits we use for the index.
const idx_bits = 8 - StyleArray.len;
const IndexInt = @Type(.{ .Int = .{ .signedness = .unsigned, .bits = idx_bits } });
style: Style,
idx: IndexInt,
test {
// We never want to take up more than a byte since font indexes are
// everywhere so if we increase the size of this we'll dramatically
// increase our memory usage.
try std.testing.expectEqual(@sizeOf(u8), @sizeOf(FontIndex));
}
};
/// Looks up the font that should be used for a specific codepoint.
/// The font index is valid as long as font faces aren't removed. This
/// isn't cached; it is expected that downstream users handle caching if
/// that is important.
pub fn indexForCodepoint(self: Group, style: Style, cp: u32) ?FontIndex {
// If we can find the exact value, then return that.
if (self.indexForCodepointExact(style, cp)) |value| return value;
// If this is already regular, we're done falling back.
if (style == .regular) return null;
// For non-regular fonts, we fall back to regular.
return self.indexForCodepointExact(.regular, cp);
}
fn indexForCodepointExact(self: Group, style: Style, cp: u32) ?FontIndex {
for (self.faces.get(style).items) |face, i| {
if (face.glyphIndex(cp) != null) {
return FontIndex{
.style = style,
.idx = @intCast(FontIndex.IndexInt, i),
};
}
}
// Not found
return null;
}
/// Returns true if the glyph pointed to by the index requires color.
/// This is used to determine the proper atlas to pass in for rendering
/// the glyph.
pub fn indexRequiresColor(self: Group, index: FontIndex) bool {
return self.faces.get(index.style).items[@intCast(usize, index.idx)].hasColor();
}
/// Return the Face represented by a given FontIndex.
pub fn faceFromIndex(self: Group, index: FontIndex) Face {
return self.faces.get(index.style).items[@intCast(usize, index.idx)];
}
/// Render a glyph by glyph index into the given font atlas and return
/// metadata about it.
///
/// This performs no caching, it is up to the caller to cache calls to this
/// if they want. This will also not resize the atlas if it is full.
///
/// IMPORTANT: this renders by /glyph index/ and not by /codepoint/. The caller
/// is expected to translate codepoints to glyph indexes in some way. The most
/// trivial way to do this is to get the Face and call glyphIndex. If you're
/// doing text shaping, the text shaping library (i.e. HarfBuzz) will automatically
/// determine glyph indexes for a text run.
pub fn renderGlyph(
self: Group,
alloc: Allocator,
atlas: *Atlas,
index: FontIndex,
glyph_index: u32,
) !Glyph {
const face = self.faces.get(index.style).items[@intCast(usize, index.idx)];
return try face.renderGlyph(alloc, atlas, glyph_index);
}
test {
const testing = std.testing;
const alloc = testing.allocator;
const testFont = @import("test.zig").fontRegular;
const testEmoji = @import("test.zig").fontEmoji;
var atlas_greyscale = try Atlas.init(alloc, 512, .greyscale);
defer atlas_greyscale.deinit(alloc);
var lib = try Library.init();
defer lib.deinit();
var group = try init(alloc);
defer group.deinit(alloc);
try group.addFace(alloc, .regular, try Face.init(lib, testFont, .{ .points = 12 }));
try group.addFace(alloc, .regular, try Face.init(lib, testEmoji, .{ .points = 12 }));
// Should find all visible ASCII
var i: u32 = 32;
while (i < 127) : (i += 1) {
const idx = group.indexForCodepoint(.regular, i).?;
try testing.expectEqual(Style.regular, idx.style);
try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx);
try testing.expect(!group.indexRequiresColor(idx));
// Render it
const face = group.faceFromIndex(idx);
const glyph_index = face.glyphIndex(i).?;
_ = try group.renderGlyph(
alloc,
&atlas_greyscale,
idx,
glyph_index,
);
}
// Try emoji
{
const idx = group.indexForCodepoint(.regular, '🥸').?;
try testing.expectEqual(Style.regular, idx.style);
try testing.expectEqual(@as(FontIndex.IndexInt, 1), idx.idx);
try testing.expect(group.indexRequiresColor(idx));
}
}

View File

@ -2,16 +2,17 @@ const std = @import("std");
pub const Face = @import("Face.zig");
pub const Family = @import("Family.zig");
pub const Group = @import("Group.zig");
pub const Glyph = @import("Glyph.zig");
pub const FallbackSet = @import("FallbackSet.zig");
pub const Library = @import("Library.zig");
/// The styles that a family can take.
pub const Style = enum {
regular,
bold,
italic,
bold_italic,
pub const Style = enum(u2) {
regular = 0,
bold = 1,
italic = 2,
bold_italic = 3,
};
/// Returns the UTF-32 codepoint for the given value.