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.
14 kB · 407 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-FileCopyrightText: © 2000 Keith Packard// SPDX-License-Identifier: MIT AND HPND-sell-variant
//! A pattern, fontconfig's `FcPattern`: a set of objects, each with an//! ordered list of values, and each value bound weakly or strongly.//!//! Patterns are both the questions and the answers. A query is a pattern//! saying what is wanted; every font is a pattern saying what it is; a//! match is the font's pattern with the query's other wishes merged in.//!//! Everything a pattern holds is allocated in its own arena, so editing one//! never frees anything and a value read out of it stays valid as long as//! the pattern does. A value may also *borrow* memory that outlives the//! pattern — the configuration's expressions, or a scanned font's charset —//! which is how a match result avoids copying a font's charset; such a//! result is valid only as long as the `Config` it came from.
const std = @import("std");const Allocator = std.mem.Allocator;const object = @import("object.zig");const value = @import("value.zig");const Object = object.Object;const Builtin = object.Builtin;const Value = value.Value;
const Pattern = @This();
arena: std.heap.ArenaAllocator,/// Sorted by object, as `Object.order` orders them.elts: std.ArrayList(*Elt) = .empty,
/// How strongly a value holds: a strong value of a query outranks/// differences that a weak one does not. `same` takes the binding of the/// value it is placed next to, and is only seen while editing.pub const Binding = enum { weak, strong, same };
pub const ValueList = struct { next: ?*ValueList = null, value: Value, binding: Binding,
pub fn iterator(l: ?*ValueList) Iterator { return .{ .cur = l }; }};
pub const Iterator = struct { cur: ?*ValueList,
pub fn next(it: *Iterator) ?*ValueList { const c = it.cur orelse return null; it.cur = c.next; return c; }};
pub const Elt = struct { object: Object, values: ?*ValueList = null,
pub fn iterator(e: *const Elt) Iterator { return .{ .cur = e.values }; }
pub fn count(e: *const Elt) usize { var n: usize = 0; var it = e.iterator(); while (it.next()) |_| n += 1; return n; }};
pub fn init(gpa: Allocator) Pattern { return .{ .arena = .init(gpa) };}
pub fn deinit(p: *Pattern) void { p.arena.deinit(); p.* = undefined;}
/// The allocator everything in the pattern is allocated with./// Makes room for about `bytes` in the pattern's arena with one allocation,/// rather than the many it would grow by one value at a time. A pattern/// built in one go whose size is known roughly — one read from the cache —/// is worth this, most of all in a Debug build, where the backing allocator/// records a stack trace for every allocation it makes.pub fn preheat(p: *Pattern, bytes: usize) Allocator.Error!void { if (bytes == 0) return; const a = p.allocator(); // Freeing an arena's last allocation gives the room back to the arena // and keeps it, which is the whole point. a.free(try a.alloc(u8, bytes));}
pub fn allocator(p: *Pattern) Allocator { return p.arena.allocator();}
fn position(p: *const Pattern, o: Object) union(enum) { found: usize, insert: usize } { var low: usize = 0; var high: usize = p.elts.items.len; while (low < high) { const mid = (low + high) / 2; switch (p.elts.items[mid].object.order(o)) { .eq => return .{ .found = mid }, .lt => low = mid + 1, .gt => high = mid, } } return .{ .insert = low };}
/// `FcPatternObjectFindElt`.pub fn find(p: *const Pattern, o: Object) ?*Elt { return switch (p.position(o)) { .found => |i| p.elts.items[i], .insert => null, };}
pub fn findBuiltin(p: *const Pattern, b: Builtin) ?*Elt { return p.find(.{ .builtin = b });}
/// `FcPatternObjectInsertElt`: the element for `o`, made empty if there/// was none.pub fn insertElt(p: *Pattern, o: Object) Allocator.Error!*Elt { switch (p.position(o)) { .found => |i| return p.elts.items[i], .insert => |i| { const a = p.allocator(); const e = try a.create(Elt); e.* = .{ .object = switch (o) { .builtin => o, .custom => |n| .{ .custom = try a.dupe(u8, n) }, } }; try p.elts.insert(a, i, e); return e; }, }}
/// `FcPatternObjectDel`: removes every value of `o`. Returns whether there/// were any.pub fn del(p: *Pattern, o: Object) bool { switch (p.position(o)) { .found => |i| { _ = p.elts.orderedRemove(i); return true; }, .insert => return false, }}
/// `FcPatternObjectAddWithBinding`: adds a copy of `v` to the front or the/// back of `o`'s values. A void value, or one of the wrong type for a/// builtin object, is refused and `false` returned, as fontconfig refuses/// it.pub fn addWithBinding(p: *Pattern, o: Object, v: Value, binding: Binding, append: bool) Allocator.Error!bool { return p.addInternal(o, try v.dupe(p.allocator()), binding, append);}
/// Like `addWithBinding`, without copying: `v` must outlive the pattern.pub fn addBorrowed(p: *Pattern, o: Object, v: Value, binding: Binding, append: bool) Allocator.Error!bool { return p.addInternal(o, v, binding, append);}
fn addInternal(p: *Pattern, o: Object, v: Value, binding: Binding, append: bool) Allocator.Error!bool { if (v == .void) return false; if (!o.acceptsType(v.typ())) return false; const e = try p.insertElt(o); const node = try p.allocator().create(ValueList); node.* = .{ .value = v, .binding = binding }; if (append) { var prev = &e.values; while (prev.*) |n| prev = &n.next; prev.* = node; } else { node.next = e.values; e.values = node; } return true;}
/// `FcPatternAdd`: a strongly bound copy of `v`, at the back.pub fn add(p: *Pattern, o: Object, v: Value) Allocator.Error!bool { return p.addWithBinding(o, v, .strong, true);}
pub fn addBuiltin(p: *Pattern, b: Builtin, v: Value) Allocator.Error!bool { return p.add(.{ .builtin = b }, v);}
pub fn addInteger(p: *Pattern, b: Builtin, i: i32) Allocator.Error!void { _ = try p.add(.{ .builtin = b }, .{ .integer = i });}
pub fn addDouble(p: *Pattern, b: Builtin, d: f64) Allocator.Error!void { _ = try p.add(.{ .builtin = b }, .{ .double = d });}
pub fn addString(p: *Pattern, b: Builtin, s: []const u8) Allocator.Error!void { _ = try p.add(.{ .builtin = b }, .{ .string = s });}
pub fn addBool(p: *Pattern, b: Builtin, v: bool) Allocator.Error!void { _ = try p.add(.{ .builtin = b }, .{ .bool = .from(v) });}
pub fn addRange(p: *Pattern, b: Builtin, begin: f64, end: f64) Allocator.Error!void { _ = try p.add(.{ .builtin = b }, .{ .range = .{ .begin = begin, .end = end } });}
/// `FcPatternObjectListAdd`: links a list of values in, refusing all of/// them if any has the wrong type.pub fn addList(p: *Pattern, o: Object, list: ?*ValueList, append: bool) Allocator.Error!bool { var it = ValueList.iterator(list); while (it.next()) |l| if (!o.acceptsType(l.value.typ())) return false; const head = list orelse return true; const e = try p.insertElt(o); if (append) { var prev = &e.values; while (prev.*) |n| prev = &n.next; prev.* = head; } else { var last = head; while (last.next) |n| last = n; last.next = e.values; e.values = head; } return true;}
/// `FcPatternObjectGet`: the `n`th value of `o`.pub fn get(p: *const Pattern, o: Object, n: usize) ?Value { const l = p.getList(o, n) orelse return null; return l.value;}
pub fn getList(p: *const Pattern, o: Object, n: usize) ?*ValueList { const e = p.find(o) orelse return null; var it = e.iterator(); var i: usize = 0; while (it.next()) |l| : (i += 1) { if (i == n) return l; } return null;}
pub fn getBuiltin(p: *const Pattern, b: Builtin, n: usize) ?Value { return p.get(.{ .builtin = b }, n);}
/// `FcPatternObjectGetString`: the `n`th value of `b` if it is a string.pub fn getString(p: *const Pattern, b: Builtin, n: usize) ?[]const u8 { const v = p.getBuiltin(b, n) orelse return null; return switch (v) { .string => |s| s, else => null, };}
/// `FcPatternObjectGetInteger`: an integer, or a double truncated to one.pub fn getInteger(p: *const Pattern, b: Builtin, n: usize) ?i32 { const v = p.getBuiltin(b, n) orelse return null; return switch (v) { .integer => |i| i, .double => |d| if (std.math.isFinite(d)) std.math.lossyCast(i32, d) else null, else => null, };}
/// `FcPatternObjectGetDouble`: a double, or an integer as one.pub fn getDouble(p: *const Pattern, b: Builtin, n: usize) ?f64 { const v = p.getBuiltin(b, n) orelse return null; return switch (v) { .double => |d| d, .integer => |i| @floatFromInt(i), else => null, };}
/// `FcPatternObjectGetBool`.pub fn getBool(p: *const Pattern, b: Builtin, n: usize) ?value.Bool { const v = p.getBuiltin(b, n) orelse return null; return switch (v) { .bool => |x| x, else => null, };}
/// `FcPatternDuplicate`: a deep copy, sharing nothing with `p` except what/// `p` itself borrowed.pub fn duplicate(p: *const Pattern, gpa: Allocator) Allocator.Error!Pattern { var out: Pattern = .init(gpa); errdefer out.deinit(); for (p.elts.items) |e| { var it = e.iterator(); while (it.next()) |l| _ = try out.addBorrowed(e.object, l.value, l.binding, true); } // The values were borrowed from `p`; give the copy its own. for (out.elts.items) |e| { var it = e.iterator(); while (it.next()) |l| l.value = try l.value.dupe(out.allocator()); } return out;}
/// `FcPatternEqual`: the same objects, with equal values in the same order.pub fn eql(a: *const Pattern, b: *const Pattern) bool { if (a.elts.items.len != b.elts.items.len) return false; for (a.elts.items, b.elts.items) |ea, eb| { if (!ea.object.eql(eb.object)) return false; var ia = ea.iterator(); var ib = eb.iterator(); while (true) { const la = ia.next(); const lb = ib.next(); if (la == null and lb == null) break; if (la == null or lb == null) return false; if (!la.?.value.eql(lb.?.value)) return false; } } return true;}
pub fn count(p: *const Pattern) usize { return p.elts.items.len;}
const escape_fixed = "\\-:,";const escape_variable = "\\=_:,";
/// `FcNameUnparse`: the pattern as a font name, `Family-size:object=value`.pub fn format(p: *const Pattern, w: *std.Io.Writer) std.Io.Writer.Error!void { return p.unparse(w, true);}
/// `FcNameUnparseEscaped`.pub fn unparse(p: *const Pattern, w: *std.Io.Writer, escape: bool) std.Io.Writer.Error!void { if (p.findBuiltin(.family)) |e| try unparseList(w, e, if (escape) escape_fixed else null); if (p.findBuiltin(.size)) |e| { // fontconfig writes the size only when its list came out // non-empty. var buf: [256]u8 = undefined; var sw: std.Io.Writer = .fixed(&buf); unparseList(&sw, e, if (escape) escape_fixed else null) catch {}; if (sw.buffered().len > 0) { try w.writeByte('-'); try w.writeAll(sw.buffered()); } } for (p.elts.items) |e| { const b = switch (e.object) { .builtin => |b| b, // fontconfig writes only its own objects. .custom => continue, }; if (b == .family or b == .size) continue; try w.writeByte(':'); try value.writeEscaped(w, @tagName(b), if (escape) escape_variable else null); try w.writeByte('='); try unparseList(w, e, if (escape) escape_variable else null); }}
fn unparseList(w: *std.Io.Writer, e: *const Elt, escape: ?[]const u8) std.Io.Writer.Error!void { var it = e.iterator(); var first = true; while (it.next()) |l| { if (!first) try w.writeByte(','); first = false; try l.value.unparse(w, escape); }}
const testing = std.testing;
test "add, get and order" { var p: Pattern = .init(testing.allocator); defer p.deinit(); try p.addString(.style, "Bold"); try p.addString(.family, "DejaVu Sans"); try p.addString(.family, "Verdana"); _ = try p.addWithBinding(.{ .builtin = .family }, .{ .string = "Sans" }, .weak, false); try p.addInteger(.weight, 200); try testing.expect(!try p.addBuiltin(.family, .{ .integer = 3 })); try testing.expect(!try p.addBuiltin(.family, .void));
try testing.expectEqualStrings("Sans", p.getString(.family, 0).?); try testing.expectEqualStrings("DejaVu Sans", p.getString(.family, 1).?); try testing.expectEqual(@as(?i32, 200), p.getInteger(.weight, 0)); try testing.expectEqual(Builtin.family, p.elts.items[0].object.builtin);
var out: std.Io.Writer.Allocating = .init(testing.allocator); defer out.deinit(); try p.format(&out.writer); try testing.expectEqualStrings("Sans,DejaVu Sans,Verdana:style=Bold:weight=200", out.written());
var copy = try p.duplicate(testing.allocator); defer copy.deinit(); try testing.expect(copy.eql(&p)); try testing.expect(p.del(.{ .builtin = .style })); try testing.expect(!copy.eql(&p));}