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.
16 kB · 499 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-FileCopyrightText: © 2000 Keith Packard// SPDX-License-Identifier: MIT AND HPND-sell-variant
//! fontconfig's string comparisons, from `fcstr.c`.//!//! Every comparison fontconfig makes between names — families, styles,//! constants, languages — folds case first, and it does so by walking the//! *folded UTF-8* of each string one byte at a time. Comparing those bytes//! gives an ordering, not just equality, and the ordering matters: constants//! are found by binary search with it. So the walk is reproduced exactly,//! table and quirks included, rather than replaced by a general Unicode//! case-insensitive compare.
const std = @import("std");const tables = @import("tables");
/// Walks the case-folded UTF-8 bytes of a string. A port of/// `FcCaseWalker`.const CaseWalker = struct { src: []const u8, pos: usize = 0, /// Folded bytes of the current character not yet handed out. pending: [tables.max_case_fold_chars + 4]u8 = undefined, pending_len: usize = 0, pending_pos: usize = 0,
fn init(src: []const u8) CaseWalker { return .{ .src = src }; }
/// The byte at the current position, or 0 past the end, which is how a /// C string ends. fn raw(w: *CaseWalker) u8 { if (w.pos >= w.src.len) { w.pos += 1; return 0; } const c = w.src[w.pos]; w.pos += 1; return c; }
fn fromPending(w: *CaseWalker) ?u8 { if (w.pending_pos < w.pending_len) { const c = w.pending[w.pending_pos]; w.pending_pos += 1; return c; } w.pending_len = 0; w.pending_pos = 0; return null; }
fn next(w: *CaseWalker) u8 { if (w.fromPending()) |c| return c; return w.fold(w.raw()); }
fn nextNonBlank(w: *CaseWalker) u8 { if (w.fromPending()) |c| return c; var r = w.raw(); while (r == ' ') r = w.raw(); return w.fold(r); }
fn nextNonDelim(w: *CaseWalker, delims: []const u8) u8 { if (w.fromPending()) |c| return c; var r = w.raw(); while (r != 0 and std.mem.findScalar(u8, delims, r) != null) r = w.raw(); return w.fold(r); }
fn fold(w: *CaseWalker, r: u8) u8 { if ((r & 0xc0) == 0xc0) return w.foldLong(r); if ('A' <= r and r <= 'Z') return r - 'A' + 'a'; return r; }
/// `FcStrCaseWalkerLong`: `r` is the lead byte of a multi-byte /// character, already consumed. fn foldLong(w: *CaseWalker, r: u8) u8 { const start = w.pos - 1; const decoded = utf8ToUcs4(w.src[start..]) orelse return r; const ucs4 = decoded.cp; if (ucs4 < tables.min_fold_char or ucs4 > tables.max_fold_char) return r;
var min: isize = 0; var max: isize = tables.case_fold.len; while (min <= max) { const mid: usize = @intCast((min + max) >> 1); // fontconfig's search can look one past the end of the table; // there it reads whatever follows, here it finds nothing. if (mid >= tables.case_fold.len) return r; const f = tables.case_fold[mid]; const low = f.upper; const high = low + (if (f.method == .full) 1 else f.count); if (high <= ucs4) { min = @as(isize, @intCast(mid)) + 1; } else if (ucs4 < low) { max = @as(isize, @intCast(mid)) - 1; } else { var len: usize = 0; switch (f.method) { .even_odd, .range => { if (f.method == .even_odd and (ucs4 & 1) != (f.upper & 1)) return r; // The offset is a C short added to an unsigned // 32-bit value, wrapping as it does there. const folded = ucs4 +% @as(u32, @bitCast(@as(i32, f.offset))); len = ucs4ToUtf8(folded, &w.pending); }, .full => { const off: usize = @intCast(@as(u16, @bitCast(f.offset))); len = f.count; @memcpy(w.pending[0..len], tables.case_fold_chars[off..][0..len]); }, } w.pos = start + decoded.len; w.pending_len = len; w.pending_pos = 1; return w.pending[0]; } } return r; }};
const Decoded = struct { cp: u32, len: usize };
/// `FcUtf8ToUcs4`: fontconfig's decoder, which accepts overlong forms and/// sequences up to six bytes long, and fails only on a truncated or/// malformed sequence.pub fn utf8ToUcs4(src: []const u8) ?Decoded { if (src.len == 0) return null; const s = src[0]; var result: u32 = undefined; var extra: usize = undefined; if (s & 0x80 == 0) { return .{ .cp = s, .len = 1 }; } else if (s & 0x40 == 0) { return null; } else if (s & 0x20 == 0) { result = s & 0x1f; extra = 1; } else if (s & 0x10 == 0) { result = s & 0x0f; extra = 2; } else if (s & 0x08 == 0) { result = s & 0x07; extra = 3; } else if (s & 0x04 == 0) { result = s & 0x03; extra = 4; } else if (s & 0x02 == 0) { result = s & 0x01; extra = 5; } else return null; if (extra >= src.len) return null; for (src[1 .. extra + 1]) |c| { if (c & 0xc0 != 0x80) return null; result = (result << 6) | (c & 0x3f); } return .{ .cp = result, .len = extra + 1 };}
/// `FcUcs4ToUtf8`, including its five- and six-byte forms for values past/// Unicode, which the case-fold wraparound can produce.pub fn ucs4ToUtf8(ucs4: u32, dest: []u8) usize { var bits: u5 = undefined; var n: usize = 0; if (ucs4 < 0x80) { dest[0] = @intCast(ucs4); return 1; } else if (ucs4 < 0x800) { dest[0] = @intCast(((ucs4 >> 6) & 0x1f) | 0xc0); bits = 0; } else if (ucs4 < 0x10000) { dest[0] = @intCast(((ucs4 >> 12) & 0x0f) | 0xe0); bits = 6; } else if (ucs4 < 0x200000) { dest[0] = @intCast(((ucs4 >> 18) & 0x07) | 0xf0); bits = 12; } else if (ucs4 < 0x4000000) { dest[0] = @intCast(((ucs4 >> 24) & 0x03) | 0xf8); bits = 18; } else { dest[0] = @intCast(((ucs4 >> 30) & 0x01) | 0xfc); bits = 24; } n = 1; while (true) { dest[n] = @intCast(((ucs4 >> bits) & 0x3f) | 0x80); n += 1; if (bits < 6) break; bits -= 6; } return n;}
fn sign(c1: u8, c2: u8) std.math.Order { return std.math.order(c1, c2);}
/// `FcStrCmpIgnoreCase`.pub fn cmpIgnoreCase(s1: []const u8, s2: []const u8) std.math.Order { var w1: CaseWalker = .init(s1); var w2: CaseWalker = .init(s2); while (true) { const c1 = w1.next(); const c2 = w2.next(); if (c1 == 0 or c1 != c2) return sign(c1, c2); }}
pub fn eqlIgnoreCase(s1: []const u8, s2: []const u8) bool { return cmpIgnoreCase(s1, s2) == .eq;}
/// `FcStrCmpIgnoreBlanksAndCase`: also skips every space.pub fn cmpIgnoreBlanksAndCase(s1: []const u8, s2: []const u8) std.math.Order { var w1: CaseWalker = .init(s1); var w2: CaseWalker = .init(s2); while (true) { const c1 = w1.nextNonBlank(); const c2 = w2.nextNonBlank(); if (c1 == 0 or c1 != c2) return sign(c1, c2); }}
pub fn eqlIgnoreBlanksAndCase(s1: []const u8, s2: []const u8) bool { return cmpIgnoreBlanksAndCase(s1, s2) == .eq;}
/// `FcStrHashIgnoreCase`.pub fn hashIgnoreCase(s: []const u8) u32 { var h: u32 = 0; var w: CaseWalker = .init(s); while (true) { const c = w.next(); if (c == 0) return h; h = ((h << 3) ^ (h >> 3)) ^ c; }}
/// `FcStrHashIgnoreBlanksAndCase`.pub fn hashIgnoreBlanksAndCase(s: []const u8) u32 { var h: u32 = 0; var w: CaseWalker = .init(s); while (true) { const c = w.nextNonBlank(); if (c == 0) return h; h = ((h << 3) ^ (h >> 3)) ^ c; }}
/// `FcStrDowncase`.pub fn downcase(gpa: std.mem.Allocator, s: []const u8) ![]u8 { var out: std.ArrayList(u8) = .empty; errdefer out.deinit(gpa); var w: CaseWalker = .init(s); while (true) { const c = w.next(); if (c == 0) break; try out.append(gpa, c); } return out.toOwnedSlice(gpa);}
/// `FcStrStrIgnoreCase`: the offset in `s1` where `s2` starts, ignoring/// case.pub fn findIgnoreCase(s1: []const u8, s2: []const u8) ?usize { var w1: CaseWalker = .init(s1); var w2: CaseWalker = .init(s2); const c2 = w2.next(); while (true) { const cur = w1.pos; const c1 = w1.next(); if (c1 == 0) return null; if (c1 == c2) { var w1t = w1; var w2t = w2; while (true) { const c1t = w1t.next(); const c2t = w2t.next(); if (c2t == 0) return cur; if (c2t != c1t) break; } } }}
fn isAtIgnoreCase(s1: []const u8, s2: []const u8) bool { var w1: CaseWalker = .init(s1); var w2: CaseWalker = .init(s2); while (true) { const c1 = w1.next(); const c2 = w2.next(); if (c1 == 0 or c1 != c2) return c1 == c2 or c2 == 0; }}
fn isAtIgnoreBlanksAndCase(s1: []const u8, s2: []const u8) bool { var w1: CaseWalker = .init(s1); var w2: CaseWalker = .init(s2); while (true) { const c1 = w1.nextNonBlank(); const c2 = w2.nextNonBlank(); if (c1 == 0 or c1 != c2) return c1 == c2 or c2 == 0; }}
/// `FcStrContainsIgnoreCase`.pub fn containsIgnoreCase(s1: []const u8, s2: []const u8) bool { for (0..s1.len) |i| { if (isAtIgnoreCase(s1[i..], s2)) return true; } return false;}
/// `FcStrContainsIgnoreBlanksAndCase`.pub fn containsIgnoreBlanksAndCase(s1: []const u8, s2: []const u8) bool { for (0..s1.len) |i| { if (isAtIgnoreBlanksAndCase(s1[i..], s2)) return true; } return false;}
fn isPunct(c: u8) bool { if (c < '0') return true; if (c <= '9') return false; if (c < 'A') return true; if (c <= 'Z') return false; if (c < 'a') return true; if (c <= 'z') return false; if (c <= '~') return true; return false;}
/// `FcStrContainsWord`: does `s1` contain `s2`, ignoring case, starting and/// ending on a word boundary?pub fn containsWord(s1: []const u8, s2: []const u8) bool { var word_start = true; var i: usize = 0; while (s1.len - i >= s2.len) { const rest = s1[i..]; if (word_start and isAtIgnoreCase(rest, s2) and (rest.len == s2.len or isPunct(rest[s2.len]))) return true; word_start = isPunct(rest[0]); i += 1; if (i > s1.len) break; } return false;}
/// `FcStrMatchIgnoreCaseAndDelims`: how many bytes of `s1` match `s2`,/// ignoring case and any of `delims`.pub fn matchIgnoreCaseAndDelims(s1: []const u8, s2: []const u8, delims: []const u8) usize { var w1: CaseWalker = .init(s1); var w2: CaseWalker = .init(s2); while (true) { const c1 = w1.nextNonDelim(delims); const c2 = w2.nextNonDelim(delims); if (c1 == 0 or c1 != c2) break; } return w1.pos - 1;}
/// `FcStrGlobMatch`: `*` and `?` only, matched byte for byte.pub fn globMatch(glob: []const u8, string: []const u8) bool { var g = glob; var s = string; while (g.len > 0) { const c = g[0]; g = g[1..]; switch (c) { '*' => { if (g.len == 0) return true; if (std.mem.findScalar(u8, g, '*') == null) { if (s.len < g.len) return false; s = s[s.len - g.len ..]; } while (s.len > 0) : (s = s[1..]) { if (globMatch(g, s)) return true; } return false; }, '?' => { if (s.len == 0) return false; s = s[1..]; }, else => { if (s.len == 0 or s[0] != c) return false; s = s[1..]; }, } } return s.len == 0;}
/// `FcToLower`: ASCII only.pub fn toLower(c: u8) u8 { return std.ascii.toLower(c);}
/// `FcStrtod`: a C `strtod`, returning the value and how many bytes it/// read. Zero bytes read means there was no number.pub fn strtod(s: []const u8) struct { value: f64, len: usize } { var i: usize = 0; while (i < s.len and std.ascii.isWhitespace(s[i])) i += 1; const start = i; if (i < s.len and (s[i] == '+' or s[i] == '-')) i += 1; const digits_start = i; var saw_digit = false; while (i < s.len and std.ascii.isDigit(s[i])) : (i += 1) saw_digit = true; if (i < s.len and s[i] == '.') { i += 1; while (i < s.len and std.ascii.isDigit(s[i])) : (i += 1) saw_digit = true; } if (!saw_digit) { // inf and nan, which strtod accepts too. const rest = s[digits_start..]; for ([_][]const u8{ "infinity", "inf", "nan" }) |word| { if (std.ascii.startsWithIgnoreCase(rest, word)) { const end = digits_start + word.len; const v = std.fmt.parseFloat(f64, s[start..end]) catch return .{ .value = 0, .len = 0 }; return .{ .value = v, .len = end }; } } return .{ .value = 0, .len = 0 }; } if (i < s.len and (s[i] == 'e' or s[i] == 'E')) { var j = i + 1; if (j < s.len and (s[j] == '+' or s[j] == '-')) j += 1; if (j < s.len and std.ascii.isDigit(s[j])) { while (j < s.len and std.ascii.isDigit(s[j])) j += 1; i = j; } } const v = std.fmt.parseFloat(f64, s[start..i]) catch return .{ .value = 0, .len = 0 }; return .{ .value = v, .len = i };}
/// C's `atoi`: leading blanks, an optional sign, then decimal digits, and/// zero if there are none.pub fn atoi(s: []const u8) i32 { var i: usize = 0; while (i < s.len and std.ascii.isWhitespace(s[i])) i += 1; var negative = false; if (i < s.len and (s[i] == '+' or s[i] == '-')) { negative = s[i] == '-'; i += 1; } var v: i64 = 0; while (i < s.len and std.ascii.isDigit(s[i])) : (i += 1) { v = v *% 10 +% (s[i] - '0'); } if (negative) v = -%v; return @truncate(v);}
const testing = std.testing;
test "case-insensitive compare folds ASCII and beyond" { try testing.expect(eqlIgnoreCase("DejaVu Sans", "dejavu sans")); try testing.expect(!eqlIgnoreCase("DejaVu Sans", "dejavusans")); try testing.expect(eqlIgnoreBlanksAndCase("DejaVu Sans", "dejavusans")); try testing.expect(eqlIgnoreCase("ÉCOLE", "école")); // ß folds to "ss" in full case folding. try testing.expect(eqlIgnoreCase("STRASSE", "straße")); try testing.expectEqual(std.math.Order.lt, cmpIgnoreCase("a", "B")); try testing.expectEqual(std.math.Order.gt, cmpIgnoreCase("bold", "Black"));}
test "find and contains" { try testing.expectEqual(@as(?usize, 5), findIgnoreCase("Bold Italic", "ITALIC")); try testing.expectEqual(@as(?usize, null), findIgnoreCase("Bold", "Italic")); try testing.expect(containsWord("Semi Bold", "bold")); try testing.expect(!containsWord("Boldface", "bold")); try testing.expect(containsWord("Bold-Italic", "bold")); try testing.expect(containsIgnoreBlanksAndCase("Extra Bold", "extrabold"));}
test "glob" { try testing.expect(globMatch("*.pcf.gz", "/usr/share/fonts/misc/6x13.pcf.gz")); try testing.expect(!globMatch("*.pcf.gz", "/usr/share/fonts/misc/6x13.ttf")); try testing.expect(globMatch("/usr/share/fonts/*", "/usr/share/fonts/a/b.ttf")); try testing.expect(globMatch("a?c", "abc")); try testing.expect(!globMatch("a?c", "ac")); try testing.expect(globMatch("*", ""));}
test "strtod and atoi" { try testing.expectEqual(@as(f64, 10.5), strtod("10.5pt").value); try testing.expectEqual(@as(usize, 4), strtod("10.5pt").len); try testing.expectEqual(@as(usize, 0), strtod("pt").len); try testing.expectEqual(@as(i32, -12), atoi(" -12abc")); try testing.expectEqual(@as(i32, 0), atoi("bold"));}