Something went wrong. Try again.
A Zig library that finds fonts as fontconfig does, without it: the same configuration, OpenType and bitmap fonts, and the same answers.
Something went wrong. Try again.
8.5 kB · 234 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-FileCopyrightText: © 2000 Keith Packard// SPDX-License-Identifier: MIT AND HPND-sell-variant
//! Listing fonts, from `fclist.c`: every font a pattern describes, with//! only the objects asked for, and each distinct combination once.
const std = @import("std");const Allocator = std.mem.Allocator;const object = @import("object.zig");const value = @import("value.zig");const str = @import("str.zig");const LangSet = @import("LangSet.zig");const Pattern = @import("Pattern.zig");const Object = object.Object;const Value = value.Value;const ValueList = Pattern.ValueList;
/// `FcListValueListMatchAny`: every value of the pattern is "contained" in/// some value of the font — equal, for strings, ignoring case and blanks.fn valueListMatchAny(pat: ?*ValueList, fnt: ?*ValueList) bool { var pi = ValueList.iterator(pat); outer: while (pi.next()) |p| { var fi = ValueList.iterator(fnt); while (fi.next()) |f| { if (value.compare(f.value, .listing, p.value, true)) continue :outer; } return false; } return true;}
/// `FcListPatternMatchAny`: does `font` have every object `p` has, with/// values that cover `p`'s? `namelang` is ignored, since fonts never have/// it.pub fn patternMatchAny(p: *const Pattern, font: *const Pattern) bool { for (p.elts.items) |pe| { if (pe.object.is(.namelang)) continue; const fe = font.find(pe.object) orelse return false; if (!valueListMatchAny(pe.values, fe.values)) return false; } return true;}
/// `FcListValueListEqual`: the same values, in any order.fn valueListEqual(a: ?*ValueList, b: ?*ValueList) bool { var ai = ValueList.iterator(a); outer: while (ai.next()) |x| { var bi = ValueList.iterator(b); while (bi.next()) |y| if (x.value.eql(y.value)) continue :outer; return false; } var bi = ValueList.iterator(b); outer: while (bi.next()) |y| { var ai2 = ValueList.iterator(a); while (ai2.next()) |x| if (x.value.eql(y.value)) continue :outer; return false; } return true;}
fn patternEqual(p1: *const Pattern, p2: *const Pattern, objects: []const Object) bool { for (objects) |o| { const e1 = p1.find(o); const e2 = p2.find(o); if (e1 == null and e2 == null) continue; if (e1 == null or e2 == null) return false; if (!valueListEqual(e1.?.values, e2.?.values)) return false; } return true;}
/// `FcListValueHash`.fn valueHash(v: Value) u32 { return switch (v) { .unknown, .void => 0, .integer => |i| @bitCast(i), .double => |d| @bitCast(cInt(d)), .string => |s| str.hashIgnoreCase(s), .bool => |b| @backingInt(b), .matrix => |m| @as(u32, @bitCast(cInt(m.xx * 100))) ^ @as(u32, @bitCast(cInt(m.xy * 100))) ^ @as(u32, @bitCast(cInt(m.yx * 100))) ^ @as(u32, @bitCast(cInt(m.yy * 100))), .charset => |c| c.count(), .langset => |l| blk: { var h: u32 = 0; for (l.map) |w| h ^= w; if (l.extra.len > 0) h ^= @intCast(l.extra.len); break :blk h; }, .range => |r| blk: { const b: u32 = @bitCast(cInt(r.begin * 100)); const e: u32 = @bitCast(cInt(r.end * 100)); break :blk b ^ (b << 1) ^ (e << 9); }, };}
/// C's conversion of a double to an int, saturating where C would be/// undefined.fn cInt(d: f64) i32 { if (std.math.isNan(d)) return 0; return std.math.lossyCast(i32, d);}
fn patternHash(font: *const Pattern, objects: []const Object) u32 { var h: u32 = 0; for (objects) |o| { const e = font.find(o) orelse continue; var it = e.iterator(); var lh: u32 = 0; while (it.next()) |l| lh ^= valueHash(l.value); h ^= lh; } return h;}
/// `FcGetDefaultObjectLangIndex`: which of a font's names to put first —/// the one in `lang`, else one in the same language elsewhere, else the/// English one.fn defaultLangIndex(font: *const Pattern, o: object.Builtin, lang: []const u8) usize { const e = font.findBuiltin(o) orelse return 0; var idx: ?usize = null; var defidx: ?usize = null; var it = e.iterator(); var i: usize = 0; while (it.next()) |l| : (i += 1) { const s = switch (l.value) { .string => |s| s, else => continue, }; const res = LangSet.compare(s, lang); if (res == .equal) return i; if (res == .different_territory and idx == null) idx = i; if (defidx == null and LangSet.compare(s, "en") == .equal) defidx = i; } if (idx) |x| if (x > 0) return x; if (defidx) |x| if (x > 0) return x; return 0;}
/// Every builtin object, in order: what a listing without an object set/// reports.pub fn allObjects() []const Object { const S = struct { const all = blk: { const names = @typeInfo(object.Builtin).@"enum".field_names; var out: [names.len]Object = undefined; for (names, &out) |n, *o| o.* = .{ .builtin = @field(object.Builtin, n) }; break :blk out; }; }; return &S.all;}
const hash_size = 4099;
/// `FcFontSetList`: a pattern for each distinct combination of `objects`/// among the fonts `p` describes, in fontconfig's order. The patterns/// borrow their values from the fonts and are owned by the caller.pub fn list( gpa: Allocator, fonts: []const *Pattern, p: *const Pattern, objects_in: ?[]const Object, default_lang: []const u8,) Allocator.Error![]*Pattern { var objects: std.ArrayList(Object) = .empty; defer objects.deinit(gpa); try objects.appendSlice(gpa, objects_in orelse allObjects()); // An object set is kept sorted. std.mem.sort(Object, objects.items, {}, struct { fn lessThan(_: void, a: Object, b: Object) bool { return a.order(b) == .lt; } }.lessThan); var unique: std.ArrayList(Object) = .empty; defer unique.deinit(gpa); for (objects.items) |o| { if (unique.items.len == 0 or !unique.items[unique.items.len - 1].eql(o)) try unique.append(gpa, o); } const os = unique.items;
const lang = p.getString(.namelang, 0) orelse default_lang;
const Bucket = struct { hash: u32, pattern: *Pattern }; var buckets: [hash_size]std.ArrayList(Bucket) = @splat(.empty); defer for (&buckets) |*b| b.deinit(gpa); errdefer for (&buckets) |*b| for (b.items) |e| { e.pattern.deinit(); gpa.destroy(e.pattern); };
for (fonts) |font| { if (!patternMatchAny(p, font)) continue; const hash = patternHash(font, os); const bucket = &buckets[hash % hash_size]; const exists = for (bucket.items) |e| { if (e.hash == hash and patternEqual(e.pattern, font, os)) break true; } else false; if (exists) continue;
const np = try gpa.create(Pattern); np.* = .init(gpa); var family_idx: ?usize = null; var fullname_idx: ?usize = null; var style_idx: ?usize = null; for (os) |o| { var defidx: usize = 0; if (o.is(.family) or o.is(.familylang)) { if (family_idx == null) family_idx = defaultLangIndex(font, .familylang, lang); defidx = family_idx.?; } else if (o.is(.fullname) or o.is(.fullnamelang)) { if (fullname_idx == null) fullname_idx = defaultLangIndex(font, .fullnamelang, lang); defidx = fullname_idx.?; } else if (o.is(.style) or o.is(.stylelang)) { if (style_idx == null) style_idx = defaultLangIndex(font, .stylelang, lang); defidx = style_idx.?; } const e = font.find(o) orelse continue; var it = e.iterator(); var idx: usize = 0; while (it.next()) |l| : (idx += 1) { // The value in the wanted language goes first. _ = try np.addBorrowed(o, l.value, .strong, defidx != idx); } } try bucket.append(gpa, .{ .hash = hash, .pattern = np }); }
var out: std.ArrayList(*Pattern) = .empty; errdefer out.deinit(gpa); for (&buckets) |*b| for (b.items) |e| try out.append(gpa, e.pattern); return out.toOwnedSlice(gpa);}