This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539//! CodepointResolver maps a codepoint to a font. It is more dynamic//! than "Collection" since it supports mapping codepoint ranges to//! specific fonts, searching for fallback fonts, and more.//!//! To initialize the codepoint resolver, manually initialize using//! Zig initialization syntax: .{}-style. Set the fields you want set,//! and begin using the resolver.//!//! Deinit must still be called on the resolver to free any memory//! allocated during use. All functions that take allocators should use//! the same allocator.const CodepointResolver = @This();
const std = @import("std");const Allocator = std.mem.Allocator;const ziglyph = @import("ziglyph");const font = @import("main.zig");const Atlas = font.Atlas;const CodepointMap = font.CodepointMap;const Collection = font.Collection;const Discover = font.Discover;const DiscoveryDescriptor = font.discovery.Descriptor;const Face = font.Face;const Glyph = font.Glyph;const Library = font.Library;const Presentation = font.Presentation;const RenderOptions = font.face.RenderOptions;const SpriteFace = font.SpriteFace;const Style = font.Style;
const log = std.log.scoped(.font_codepoint_resolver);
/// The underlying collection of fonts. This will be modified as/// new fonts are found via the resolver. The resolver takes ownership/// of the collection and will deinit it when it is deinitialized.collection: Collection,
/// The set of statuses and whether they're enabled or not. This defaults/// to true. This can be changed at runtime with no ill effect.styles: StyleStatus = .initFill(true),
/// If discovery is available, we'll look up fonts where we can't find/// the codepoint. This can be set after initialization.discover: ?*Discover = null,
/// A map of codepoints to font requests for codepoint-level overrides./// The memory associated with the map is owned by the caller and is not/// modified or freed by Group.codepoint_map: ?CodepointMap = null,
/// The descriptor cache is used to cache the descriptor to font face/// mapping for codepoint maps.descriptor_cache: DescriptorCache = .{},
/// Set this to a non-null value to enable sprite glyph drawing. If this/// isn't enabled we'll just fall through to trying to use regular fonts/// to render sprite glyphs. But more than likely, if this isn't set then/// terminal rendering will look wrong.sprite: ?SpriteFace = null,
pub fn deinit(self: *CodepointResolver, alloc: Allocator) void { self.collection.deinit(alloc); self.descriptor_cache.deinit(alloc);}
/// 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.////// Optionally, a presentation format can be specified. This presentation/// format will be preferred but if it can't be found in this format,/// any format will be accepted. If presentation is null, the UCD/// (Unicode Character Database) will be used to determine the default/// presentation for the codepoint./// a code point.////// An allocator is required because certain functionality (codepoint/// mapping, fallback fonts, etc.) may require memory allocation. Curiously,/// this function cannot error! If an error occurs for any reason, including/// memory allocation, the associated functionality is ignored and the/// resolver attempts to use a different method to satisfy the codepoint./// This behavior is intentional to make the resolver apply best-effort/// logic to satisfy the codepoint since its better to render something/// than nothing.////// This logic is relatively complex so the exact algorithm is documented/// here. If this gets out of sync with the code, ask questions.////// 1. If a font style is requested that is disabled (i.e. bold),/// we start over with the regular font style. The regular font style/// cannot be disabled, but it can be replaced with a stylized font/// face.////// 2. If there is a codepoint override for the codepoint, we satisfy/// that requirement if we can, no matter what style or presentation.////// 3. If this is a sprite codepoint (such as an underline), then the/// sprite font always is the result.////// 4. If the exact style and presentation request can be satisfied by/// one of our loaded fonts, we return that value. We search loaded/// fonts in the order they're added to the group, so the caller must/// set the priority order.////// 5. If the style isn't regular, we restart this process at this point/// but with the regular style. This lets us fall back to regular with/// our loaded fonts before trying a fallback. We'd rather show a regular/// version of a codepoint from a loaded font than find a new font in/// the correct style because styles in other fonts often change/// metrics like glyph widths.////// 6. If the style is regular, and font discovery is enabled, we look/// for a fallback font to satisfy our request.////// 7. Finally, as a last resort, we fall back to restarting this whole/// process with a regular font face satisfying ANY presentation for/// the codepoint. If this fails, we return null.///pub fn getIndex( self: *CodepointResolver, alloc: Allocator, cp: u32, style: Style, p: ?Presentation,) ?Collection.Index { // If we've disabled a font style, then fall back to regular. if (style != .regular and !self.styles.get(style)) { return self.getIndex(alloc, cp, .regular, p); }
// Codepoint overrides. if (self.getIndexCodepointOverride(alloc, cp)) |idx_| { if (idx_) |idx| return idx; } else |err| { log.warn("codepoint override failed codepoint={} err={}", .{ cp, err }); }
// If we have sprite drawing enabled, check if our sprite face can // handle this. if (self.sprite) |sprite| { if (sprite.hasCodepoint(cp, p)) { return .initSpecial(.sprite); } }
// Build our presentation mode. If we don't have an explicit presentation // given then we use the UCD (Unicode Character Database) to determine // the default presentation. Note there is some inefficiency here because // we'll do this multiple times if we recurse, but this is a cached function // call higher up (GroupCache) so this should be rare. const p_mode: Collection.PresentationMode = if (p) |v| .{ .explicit = v } else .{ .default = if (ziglyph.emoji.isEmojiPresentation(@intCast(cp))) .emoji else .text, };
// If we can find the exact value, then return that. if (self.collection.getIndex(cp, style, p_mode)) |value| return value;
// If we're not a regular font style, try looking for a regular font // that will satisfy this request. Blindly looking for unmatched styled // fonts to satisfy one codepoint results in some ugly rendering. if (style != .regular) { if (self.getIndex(alloc, cp, .regular, p)) |value| return value; }
// If we are regular, try looking for a fallback using discovery. if (style == .regular and font.Discover != void) { log.debug("searching for a fallback font for cp={X}", .{cp}); if (self.discover) |disco| discover: { const load_opts = self.collection.load_options orelse break :discover; var disco_it = disco.discoverFallback(alloc, &self.collection, .{ .codepoint = cp, .size = load_opts.size.points, .bold = style == .bold or style == .bold_italic, .italic = style == .italic or style == .bold_italic, .monospace = false, }) catch break :discover; defer disco_it.deinit();
while (true) { var deferred_face = (disco_it.next() catch |err| { log.warn("fallback search failed with error err={}", .{err}); break; }) orelse break;
// Discovery is supposed to only return faces that have our // codepoint but we can't search presentation in discovery so // we have to check it here. const face: Collection.Entry = .{ .fallback_deferred = deferred_face }; if (!face.hasCodepoint(cp, p_mode)) { deferred_face.deinit(); continue; }
var buf: [256]u8 = undefined; log.info("found codepoint 0x{X} in fallback face={s}", .{ cp, deferred_face.name(&buf) catch "<error>", }); return self.collection.add(alloc, style, face) catch { deferred_face.deinit(); break :discover; }; }
log.debug("no fallback face found for cp={X}", .{cp}); } }
// If this is regular with any matching presentation, then we are done // there is nothing more we can do. Otherwise we fall through and do // an any presentation search. if (style == .regular and p_mode == .any) return null;
// For non-regular fonts, we fall back to regular with any presentation return self.collection.getIndex(cp, .regular, .{ .any = {} });}
/// Checks if the codepoint is in the map of codepoint overrides,/// finds the override font, and returns it.fn getIndexCodepointOverride( self: *CodepointResolver, alloc: Allocator, cp: u32,) !?Collection.Index { // If discovery is disabled then we can't do codepoint overrides // since the override is based on discovery to find the font. if (comptime font.Discover == void) return null;
// Get our codepoint map. If we have no map set then we have no // codepoint overrides and we're done. const map = self.codepoint_map orelse return null;
// If we have a codepoint too large or isn't in the map, then we // don't have an override. The map returns a descriptor that can be // used with font discovery to search for a matching font. const cp_u21 = std.math.cast(u21, cp) orelse return null; const desc = map.get(cp_u21) orelse return null;
// Fast path: the descriptor is already loaded. This means that we // already did the search before and we have an exact font for this // codepoint. const idx_: ?Collection.Index = self.descriptor_cache.get(desc) orelse idx: { // Slow path: we have to find this descriptor and load the font const discover = self.discover orelse return null; var disco_it = try discover.discover(alloc, desc); defer disco_it.deinit();
const face = (try disco_it.next()) orelse { log.warn( "font lookup for codepoint map failed codepoint={} err=FontNotFound", .{cp}, );
// Add null to the cache so we don't do a lookup again later. try self.descriptor_cache.put(alloc, desc, null); return null; };
// Add the font to our list of fonts so we can get an index for it, // and ensure the index is stored in the descriptor cache for next time. const idx = try self.collection.add( alloc, .regular, .{ .deferred = face }, ); try self.descriptor_cache.put(alloc, desc, idx);
break :idx idx; };
// The descriptor cache will populate null if the descriptor is not found // to avoid expensive discoveries later, so if it is null then we already // searched and found nothing. const idx = idx_ orelse return null;
// We need to verify that this index has the codepoint we want. if (self.collection.hasCodepoint(idx, cp, .{ .any = {} })) { log.debug("codepoint override based on config codepoint={} family={s}", .{ cp, desc.family orelse "", });
return idx; }
return null;}
/// Returns the presentation for a specific font index. This is useful for/// determining what atlas is needed.pub fn getPresentation( self: *CodepointResolver, index: Collection.Index, glyph_index: u32,) !Presentation { if (index.special()) |sp| return switch (sp) { .sprite => .text, };
const face = try self.collection.getFace(index); return if (face.isColorGlyph(glyph_index)) .emoji else .text;}
/// 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: *CodepointResolver, alloc: Allocator, atlas: *Atlas, index: Collection.Index, glyph_index: u32, opts: RenderOptions,) !Glyph { // Special-case fonts are rendered directly. if (index.special()) |sp| switch (sp) { .sprite => return try self.sprite.?.renderGlyph( alloc, atlas, glyph_index, opts, ), };
const face = try self.collection.getFace(index); const glyph = try face.renderGlyph(alloc, atlas, glyph_index, opts); // log.warn("GLYPH={}", .{glyph}); return glyph;}
/// Packed array of booleans to indicate if a style is enabled or not.pub const StyleStatus = std.EnumArray(Style, bool);
/// Map of descriptors to faces. This is used with manual codepoint maps/// to ensure that we don't load the same font multiple times.////// Note that the current implementation will load the same font multiple/// times if the font used for a codepoint map is identical to a font used/// for a regular style. That's just an inefficient choice made now because/// the implementation is simpler and codepoint maps matching a regular/// font is a rare case.const DescriptorCache = std.HashMapUnmanaged( DiscoveryDescriptor, ?Collection.Index, struct { const KeyType = DiscoveryDescriptor;
pub fn hash(ctx: @This(), k: KeyType) u64 { _ = ctx; return k.hashcode(); }
pub fn eql(ctx: @This(), a: KeyType, b: KeyType) bool { // Note that this means its possible to have two different // descriptors match when there is a hash collision so we // should button this up later. return ctx.hash(a) == ctx.hash(b); } }, std.hash_map.default_max_load_percentage,);
test getIndex { const testing = std.testing; const alloc = testing.allocator; const testFont = font.embedded.regular; const testEmoji = font.embedded.emoji; const testEmojiText = font.embedded.emoji_text;
var lib = try Library.init(alloc); defer lib.deinit();
var c = Collection.init(); c.load_options = .{ .library = lib };
{ errdefer c.deinit(alloc); _ = try c.add(alloc, .regular, .{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, ) }); if (comptime !font.options.backend.hasCoretext()) { // Coretext doesn't support Noto's format _ = try c.add( alloc, .regular, .{ .loaded = try .init( lib, testEmoji, .{ .size = .{ .points = 12 } }, ) }, ); } _ = try c.add( alloc, .regular, .{ .loaded = try .init( lib, testEmojiText, .{ .size = .{ .points = 12 } }, ) }, ); }
var r: CodepointResolver = .{ .collection = c }; defer r.deinit(alloc);
// Should find all visible ASCII var i: u32 = 32; while (i < 127) : (i += 1) { const idx = r.getIndex(alloc, i, .regular, null).?; try testing.expectEqual(Style.regular, idx.style); try testing.expectEqual(@as(Collection.Index.IndexInt, 0), idx.idx); }
// Try emoji { const idx = r.getIndex(alloc, '🥸', .regular, null).?; try testing.expectEqual(Style.regular, idx.style); try testing.expectEqual(@as(Collection.Index.IndexInt, 1), idx.idx); }
// Try text emoji { const idx = r.getIndex(alloc, 0x270C, .regular, .text).?; try testing.expectEqual(Style.regular, idx.style); const text_idx = if (comptime font.options.backend.hasCoretext()) 1 else 2; try testing.expectEqual(@as(Collection.Index.IndexInt, text_idx), idx.idx); } { const idx = r.getIndex(alloc, 0x270C, .regular, .emoji).?; try testing.expectEqual(Style.regular, idx.style); try testing.expectEqual(@as(Collection.Index.IndexInt, 1), idx.idx); }
// Box glyph should be null since we didn't set a box font { try testing.expect(r.getIndex(alloc, 0x1FB00, .regular, null) == null); }}
test "getIndex disabled font style" { const testing = std.testing; const alloc = testing.allocator; const testFont = font.embedded.regular;
var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale); defer atlas_grayscale.deinit(alloc);
var lib = try Library.init(alloc); defer lib.deinit();
var c = Collection.init(); c.load_options = .{ .library = lib };
_ = try c.add(alloc, .regular, .{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, ) }); _ = try c.add(alloc, .bold, .{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, ) }); _ = try c.add(alloc, .italic, .{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, ) });
var r: CodepointResolver = .{ .collection = c }; defer r.deinit(alloc); r.styles.set(.bold, false); // Disable bold
// Regular should work fine { const idx = r.getIndex(alloc, 'A', .regular, null).?; try testing.expectEqual(Style.regular, idx.style); try testing.expectEqual(@as(Collection.Index.IndexInt, 0), idx.idx); }
// Bold should go to regular { const idx = r.getIndex(alloc, 'A', .bold, null).?; try testing.expectEqual(Style.regular, idx.style); try testing.expectEqual(@as(Collection.Index.IndexInt, 0), idx.idx); }
// Italic should still work { const idx = r.getIndex(alloc, 'A', .italic, null).?; try testing.expectEqual(Style.italic, idx.style); try testing.expectEqual(@as(Collection.Index.IndexInt, 0), idx.idx); }}
test "getIndex box glyph" { const testing = std.testing; const alloc = testing.allocator;
var lib = try Library.init(alloc); defer lib.deinit();
const c = Collection.init();
var r: CodepointResolver = .{ .collection = c, .sprite = .{ .metrics = font.Metrics.calc(.{ .cell_width = 18.0, .ascent = 30.0, .descent = -6.0, .line_gap = 0.0, }), }, }; defer r.deinit(alloc);
// Should find a box glyph const idx = r.getIndex(alloc, 0x2500, .regular, null).?; try testing.expectEqual(Style.regular, idx.style); try testing.expectEqual(@intFromEnum(Collection.Index.Special.sprite), idx.idx);}