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.
15 kB · 424 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-FileCopyrightText: © 2000 Keith Packard// SPDX-License-Identifier: MIT AND HPND-sell-variant
//! A pattern value, `FcValue`, and the ways fontconfig compares them.
const std = @import("std");const Allocator = std.mem.Allocator;const str = @import("str.zig");const CharSet = @import("CharSet.zig");const LangSet = @import("LangSet.zig");const object = @import("object.zig");
/// fontconfig's three-valued boolean. `dontcare` compares as larger than/// both of the others, which some comparisons rely on.pub const Bool = enum(u8) { false = 0, true = 1, dontcare = 2,
pub fn from(b: bool) Bool { return if (b) .true else .false; }};
pub const Matrix = struct { xx: f64 = 1, xy: f64 = 0, yx: f64 = 0, yy: f64 = 1,
pub const identity: Matrix = .{};
pub fn eql(a: Matrix, b: Matrix) bool { return a.xx == b.xx and a.xy == b.xy and a.yx == b.yx and a.yy == b.yy; }
/// `FcMatrixMultiply`. pub fn multiply(a: Matrix, b: Matrix) Matrix { return .{ .xx = a.xx * b.xx + a.xy * b.yx, .xy = a.xx * b.xy + a.xy * b.yy, .yx = a.yx * b.xx + a.yy * b.yx, .yy = a.yx * b.xy + a.yy * b.yy, }; }};
/// `FcRange`: a closed interval. Weight, width and size are ranges, so/// that a variable font can say it covers all of them.pub const Range = struct { begin: f64, end: f64,
pub fn point(v: f64) Range { return .{ .begin = v, .end = v }; }
/// `FcRangeIsInRange`: is `a` inside `b`? pub fn isInRange(a: Range, b: Range) bool { return a.begin >= b.begin and a.end <= b.end; }};
pub const Value = union(object.Type) { unknown, void, integer: i32, double: f64, string: []const u8, bool: Bool, matrix: Matrix, charset: *const CharSet, langset: *const LangSet, range: Range,
pub fn typ(v: Value) object.Type { return std.meta.activeTag(v); }
/// A copy of `v` whose strings and sets are allocated with `gpa`. pub fn dupe(v: Value, gpa: Allocator) Allocator.Error!Value { return switch (v) { .string => |s| .{ .string = try gpa.dupe(u8, s) }, .charset => |c| blk: { const p = try gpa.create(CharSet); errdefer gpa.destroy(p); p.* = try c.dupe(gpa); break :blk .{ .charset = p }; }, .langset => |l| blk: { const p = try gpa.create(LangSet); errdefer gpa.destroy(p); p.* = try l.dupe(gpa); break :blk .{ .langset = p }; }, else => v, }; }
/// `FcValueEqual`: integers and doubles compare as numbers, strings /// ignore case, and a range equals another it lies inside. pub fn eql(va: Value, vb: Value) bool { var a = va; var b = vb; if (a.typ() != b.typ()) { if (a == .integer) a = .{ .double = @floatFromInt(a.integer) }; if (b == .integer) b = .{ .double = @floatFromInt(b.integer) }; if (a.typ() != b.typ()) return false; } return switch (a) { .unknown => false, .void => true, .integer => |x| x == b.integer, .double => |x| x == b.double, .string => |x| str.eqlIgnoreCase(x, b.string), .bool => |x| x == b.bool, .matrix => |x| x.eql(b.matrix), .charset => |x| x.eql(b.charset.*), .langset => |x| x.eql(b.langset), .range => |x| x.isInRange(b.range), }; }
/// `FcNameUnparseValue`. With `escape`, any of those bytes is preceded /// by the first of them. pub fn unparse(v: Value, w: *std.Io.Writer, escape: ?[]const u8) std.Io.Writer.Error!void { switch (v) { .unknown, .void => {}, .integer => |i| try w.print("{d}", .{i}), .double => |d| try formatG(w, d), .string => |s| try writeEscaped(w, s, escape), .bool => |b| try w.writeAll(switch (b) { .true => "True", .false => "False", .dontcare => "DontCare", }), .matrix => |m| { try formatG(w, m.xx); try w.writeByte(' '); try formatG(w, m.xy); try w.writeByte(' '); try formatG(w, m.yx); try w.writeByte(' '); try formatG(w, m.yy); }, .charset => |c| try c.format(w), .langset => |l| try l.format(w), .range => |r| { try w.writeByte('['); try formatG(w, r.begin); try w.writeByte(' '); try formatG(w, r.end); try w.writeByte(']'); }, } }};
pub fn writeEscaped(w: *std.Io.Writer, s: []const u8, escape: ?[]const u8) std.Io.Writer.Error!void { for (s) |c| { if (escape) |e| { if (std.mem.findScalar(u8, e, c) != null) try w.writeByte(e[0]); } try w.writeByte(c); }}
/// C's `printf("%g")`: six significant digits, trailing zeros removed, and/// scientific notation when the exponent is below -4 or at least 6.pub fn formatG(w: *std.Io.Writer, d: f64) std.Io.Writer.Error!void { if (std.math.isNan(d)) return w.writeAll(if (std.math.signbit(d)) "-nan" else "nan"); if (std.math.isInf(d)) return w.writeAll(if (d < 0) "-inf" else "inf"); if (d == 0) return w.writeAll(if (std.math.signbit(d)) "-0" else "0");
// The exponent %e would print, after rounding to six digits. var buf: [64]u8 = undefined; const e_text = std.fmt.bufPrint(&buf, "{e:.5}", .{d}) catch unreachable; const e_pos = std.mem.findScalar(u8, e_text, 'e').?; const exp = std.fmt.parseInt(i32, e_text[e_pos + 1 ..], 10) catch unreachable;
if (exp < -4 or exp >= 6) { const mantissa = stripZeros(e_text[0..e_pos]); try w.writeAll(mantissa); try w.writeByte('e'); try w.writeByte(if (exp < 0) '-' else '+'); const a: u32 = @abs(exp); if (a < 10) try w.writeByte('0'); try w.print("{d}", .{a}); } else { const decimals: usize = @intCast(5 - exp); var buf2: [400]u8 = undefined; const f_text = std.fmt.bufPrint(&buf2, "{d:.[1]}", .{ d, decimals }) catch unreachable; try w.writeAll(stripZeros(f_text)); }}
fn stripZeros(s: []const u8) []const u8 { if (std.mem.findScalar(u8, s, '.') == null) return s; var end = s.len; while (end > 0 and s[end - 1] == '0') end -= 1; if (end > 0 and s[end - 1] == '.') end -= 1; return s[0..end];}
/// Storage for the temporary values `promote` makes, so that a comparison/// between a string and a language set, say, allocates nothing.pub const PromotionBuffer = struct { charset: CharSet = .empty, langset: LangSet = .{}, lang: [1][]const u8 = undefined,};
/// `FcConfigPromote`: converts `v` towards the type of `u` where fontconfig/// allows it — an integer to a double, a number to a range, nothing to an/// identity matrix or an empty set, a string to a language set.pub fn promote(v: Value, u: Value, buf: ?*PromotionBuffer) Value { switch (v) { .integer, .double => { const d: f64 = if (v == .integer) @floatFromInt(v.integer) else v.double; if (u == .range and buf != null) return .{ .range = .point(d) }; return .{ .double = d }; }, .void => { if (u == .matrix) return .{ .matrix = .identity }; if (buf) |b| { if (u == .langset) { b.langset = .{}; return .{ .langset = &b.langset }; } if (u == .charset) { b.charset = .empty; return .{ .charset = &b.charset }; } } return v; }, .string => |s| { if (u == .langset) { if (buf) |b| { b.langset = .{}; switch (LangSet.index(s)) { .found => |id| { const bit = LangSet.orths[id].bit; b.langset.map[bit >> 5] |= @as(u32, 1) << @intCast(bit & 31); }, .insert => { b.lang[0] = s; b.langset.extra = &b.lang; }, } return .{ .langset = &b.langset }; } } return v; }, else => return v, }}
/// The comparison operators of a `<test>` or an expression.pub const Compare = enum { eq, not_eq, less, less_eq, more, more_eq, contains, not_contains, /// `FcOpListing`, the comparison `FcFontList` makes: like `contains`, /// but string equality. listing,};
/// `FcConfigCompareValue`. `ignore_blanks` is the `ignore-blanks` attribute/// of a `<test>`, which makes string equality skip spaces.pub fn compare(left_in: Value, op: Compare, right_in: Value, ignore_blanks: bool) bool { var left = left_in; var right = right_in; var buf1: PromotionBuffer = .{}; var buf2: PromotionBuffer = .{}; if (left.typ() != right.typ()) { left = promote(left, right, &buf1); right = promote(right, left, &buf2); if (left.typ() != right.typ()) return op == .not_eq or op == .not_contains; } switch (left) { .unknown => return false, .integer => |l| { const r = right.integer; return switch (op) { .eq, .contains, .listing => l == r, .not_eq, .not_contains => l != r, .less => l < r, .less_eq => l <= r, .more => l > r, .more_eq => l >= r, }; }, .double => |l| { const r = right.double; return switch (op) { .eq, .contains, .listing => l == r, .not_eq, .not_contains => l != r, .less => l < r, .less_eq => l <= r, .more => l > r, .more_eq => l >= r, }; }, .bool => |lb| { const l = @backingInt(lb); const r = @backingInt(right.bool); const dc = @backingInt(Bool.dontcare); return switch (op) { .eq => l == r, .contains, .listing => l == r or l >= dc, .not_eq => l != r, .not_contains => !(l == r or l >= dc), .less => l != r and r >= dc, .less_eq => l == r or r >= dc, .more => l != r and l >= dc, .more_eq => l == r or l >= dc, }; }, .string => |l| { const r = right.string; return switch (op) { .eq, .listing => if (ignore_blanks) str.eqlIgnoreBlanksAndCase(l, r) else str.eqlIgnoreCase(l, r), .contains => str.findIgnoreCase(l, r) != null, .not_eq => if (ignore_blanks) !str.eqlIgnoreBlanksAndCase(l, r) else !str.eqlIgnoreCase(l, r), .not_contains => str.findIgnoreCase(l, r) == null, else => false, }; }, .matrix => |l| return switch (op) { .eq, .contains, .listing => l.eql(right.matrix), .not_eq, .not_contains => !l.eql(right.matrix), else => false, }, .charset => |l| { const r = right.charset; return switch (op) { // left contains right if right is a subset of left .contains, .listing => r.isSubset(l.*), .not_contains => !r.isSubset(l.*), .eq => l.eql(r.*), .not_eq => !l.eql(r.*), else => false, }; }, .langset => |l| { const r = right.langset; return switch (op) { .contains, .listing => l.contains(r), .not_contains => !l.contains(r), .eq => l.eql(r), .not_eq => !l.eql(r), else => false, }; }, .void => return switch (op) { .eq, .contains, .listing => true, else => false, }, .range => |l| { const r = right.range; return switch (op) { .eq => l.begin == r.begin and l.end == r.end, .contains, .listing => l.isInRange(r), .not_eq => l.begin != r.begin or l.end != r.end, .not_contains => !l.isInRange(r), .less => l.end < r.begin, .less_eq => l.end <= r.begin, .more => l.begin > r.end, .more_eq => l.begin >= r.end, }; }, }}
const testing = std.testing;
fn expectG(expected: []const u8, d: f64) !void { var buf: [64]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); try formatG(&w, d); try testing.expectEqualStrings(expected, w.buffered());}
test "%g formatting" { try expectG("12", 12); try expectG("10.5", 10.5); try expectG("0.1", 0.1); try expectG("1e+06", 1e6); try expectG("123457", 123456.7); try expectG("1e-05", 0.00001); try expectG("0.0001", 0.0001); try expectG("-3.5", -3.5); try expectG("0", 0); try expectG("75", 75); try expectG("1.33333", 4.0 / 3.0);}
test "compare with promotion" { try testing.expect(compare(.{ .integer = 200 }, .eq, .{ .double = 200 }, false)); try testing.expect(compare(.{ .double = 80 }, .contains, .{ .range = .{ .begin = 50, .end = 100 } }, false)); try testing.expect(compare(.{ .string = "DejaVu Sans" }, .eq, .{ .string = "dejavu sans" }, false)); try testing.expect(!compare(.{ .string = "DejaVu Sans" }, .eq, .{ .string = "dejavusans" }, false)); try testing.expect(compare(.{ .string = "DejaVu Sans" }, .eq, .{ .string = "dejavusans" }, true)); try testing.expect(compare(.{ .string = "Bold Italic" }, .contains, .{ .string = "italic" }, false)); try testing.expect(compare(.{ .integer = 1 }, .not_eq, .{ .string = "x" }, false)); try testing.expect(!compare(.{ .integer = 1 }, .eq, .{ .string = "x" }, false)); try testing.expect(compare(.{ .bool = .dontcare }, .contains, .{ .bool = .true }, false));}
test "compare a language set with a string" { const gpa = testing.allocator; var ls = try LangSet.parse(gpa, "en|de"); defer ls.deinit(gpa); try testing.expect(compare(.{ .langset = &ls }, .contains, .{ .string = "en" }, false)); try testing.expect(!compare(.{ .langset = &ls }, .contains, .{ .string = "ja" }, false));}