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.
9.5 kB · 237 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-FileCopyrightText: © 2000 Keith Packard// SPDX-License-Identifier: MIT AND HPND-sell-variant
//! Font names, the strings `fc-match` takes: `DejaVu Sans-12:bold`, from//! `fcname.c`.//!//! A name is a comma-separated list of families, then optionally a dash//! and a comma-separated list of sizes, then any number of `:object=value`//! (or `:object_value`) elements and bare `:constant`s. A backslash escapes//! the next character.
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 CharSet = @import("CharSet.zig");const LangSet = @import("LangSet.zig");const Pattern = @import("Pattern.zig");const Object = object.Object;const Value = value.Value;
pub const ParseError = Allocator.Error || error{InvalidName};
/// `FcNameBool`: `t`, `y`, `1`, `on` are true; `f`, `n`, `0`, `off` false;/// `d`, `x`, `2`, `or` don't-care. Only the first one or two characters/// are looked at.pub fn parseBool(v: []const u8) ?value.Bool { if (v.len == 0) return null; const c0 = str.toLower(v[0]); if (c0 == 't' or c0 == 'y' or c0 == '1') return .true; if (c0 == 'f' or c0 == 'n' or c0 == '0') return .false; if (c0 == 'd' or c0 == 'x' or c0 == '2') return .dontcare; if (c0 == 'o' and v.len > 1) { const c1 = str.toLower(v[1]); if (c1 == 'n') return .true; if (c1 == 'f') return .false; if (c1 == 'r') return .dontcare; } return null;}
/// `FcNameConvert`: the text of a value, read as the type `o` holds. The/// result may point into `arena`, and is `.void` if the text does not read/// as that type.pub fn convert(arena: Allocator, o: Object, text: []const u8) Allocator.Error!Value { switch (o.valueType()) { .integer => { if (object.constantFor(text, o)) |c| return .{ .integer = c.value }; return .{ .integer = str.atoi(text) }; }, .string => return .{ .string = try arena.dupe(u8, text) }, .bool => return .{ .bool = parseBool(text) orelse .false }, .double => return .{ .double = str.strtod(text).value }, .matrix => { // sscanf("%lg %lg %lg %lg"): as many as parse, the rest left // at the identity. var m: value.Matrix = .identity; var rest = text; const fields = [_]*f64{ &m.xx, &m.xy, &m.yx, &m.yy }; for (fields) |f| { const r = str.strtod(rest); if (r.len == 0) break; f.* = r.value; rest = rest[r.len..]; } return .{ .matrix = m }; }, .charset => { const cs = try CharSet.parse(arena, text) orelse return .void; const p = try arena.create(CharSet); p.* = cs; return .{ .charset = p }; }, .langset => { const p = try arena.create(LangSet); p.* = try LangSet.parse(arena, text); return .{ .langset = p }; }, .range => return convertRange(o, text), .unknown, .void => return .void, }}
/// A range is written `[begin end]`, with numbers or constants for the/// ends; anything else is read as a single number or constant.fn convertRange(o: Object, text: []const u8) Value { if (text.len > 0 and text[0] == '[') { const b = str.strtod(text[1..]); if (b.len > 0) { var rest = text[1 + b.len ..]; rest = std.mem.trimStart(u8, rest, " \t\n\r"); const e = str.strtod(rest); if (e.len > 0 and e.len < rest.len and rest[e.len] == ']') return .{ .range = .{ .begin = b.value, .end = e.value } }; // sscanf's "[%lg %lg]" stops matching at the "]", but has // already read both numbers, which is all it reports. if (e.len > 0) return .{ .range = .{ .begin = b.value, .end = e.value } }; } // "[%s %[^]]]": two words, both constants. const inner = text[1..]; var words = std.mem.tokenizeAny(u8, inner, " \t\n\r"); if (words.next()) |first| { if (words.next()) |second_raw| { const close = std.mem.findScalar(u8, second_raw, ']'); const second = if (close) |c| second_raw[0..c] else second_raw; const sc = object.constantFor(first, o); const ec = object.constantFor(second, o); if (sc != null and ec != null) return .{ .range = .{ .begin = @floatFromInt(sc.?.value), .end = @floatFromInt(ec.?.value) } }; } } } if (object.constantFor(text, o)) |c| return .{ .double = @floatFromInt(c.value) }; const d = str.strtod(text); if (d.len != text.len) return .void; return .{ .double = d.value };}
/// `FcNameFindNext`: skips leading blanks, then copies up to the next/// unescaped delimiter, dropping backslashes. Returns the token, the/// delimiter that ended it (0 at the end), and the rest.fn findNext(buf: []u8, s: []const u8, delims: []const u8) struct { token: []const u8, delim: u8, rest: []const u8 } { var i: usize = 0; while (i < s.len and std.ascii.isWhitespace(s[i])) i += 1; var n: usize = 0; var delim: u8 = 0; while (i < s.len) { var c = s[i]; if (c == '\\') { i += 1; if (i >= s.len) break; c = s[i]; } else if (std.mem.findScalar(u8, delims, c) != null) { delim = c; i += 1; break; } i += 1; buf[n] = c; n += 1; } return .{ .token = buf[0..n], .delim = delim, .rest = s[i..] };}
/// `FcNameParse`. Fails, as fontconfig does, if an element names an object/// whose value cannot be stored — any object fontconfig does not know, for/// one.pub fn parse(gpa: Allocator, name: []const u8) ParseError!Pattern { var pat: Pattern = .init(gpa); errdefer pat.deinit(); const arena = pat.allocator(); const buf = try arena.alloc(u8, name.len);
var rest = name; var delim: u8 = 0; while (true) { const t = findNext(buf, rest, "-,:"); rest = t.rest; delim = t.delim; if (t.token.len > 0) try pat.addString(.family, t.token); if (delim != ',') break; } if (delim == '-') { while (true) { const t = findNext(buf, rest, "-,:"); rest = t.rest; delim = t.delim; const d = str.strtod(t.token); if (d.len > 0) try pat.addDouble(.size, d.value); if (delim != ',') break; } } while (delim == ':') { const t = findNext(buf, rest, "=_:"); rest = t.rest; delim = t.delim; if (t.token.len == 0) continue; if (delim == '=' or delim == '_') { const o = Object.fromName(try arena.dupe(u8, t.token)); while (true) { const v = findNext(buf, rest, ":,"); rest = v.rest; delim = v.delim; const converted = try convert(arena, o, v.token); if (!try pat.addBorrowed(o, converted, .strong, true)) return error.InvalidName; if (delim != ',') break; } } else if (object.constant(t.token)) |c| { const o: Object = .{ .builtin = c.object }; switch (c.object.valueType()) { .integer, .double, .range => try pat.addInteger(c.object, c.value), .bool => _ = try pat.add(o, .{ .bool = @fromBackingInt(@intCast(c.value)) }), else => {}, } } } return pat;}
const testing = std.testing;
fn expectRoundTrip(name: []const u8, expected: []const u8) !void { var p = try parse(testing.allocator, name); defer p.deinit(); var out: std.Io.Writer.Allocating = .init(testing.allocator); defer out.deinit(); try p.format(&out.writer); try testing.expectEqualStrings(expected, out.written());}
test "parse names" { try expectRoundTrip("DejaVu Sans-12:bold", "DejaVu Sans-12:weight=200"); try expectRoundTrip("Times,Liberation Serif:italic", "Times,Liberation Serif:slant=100"); try expectRoundTrip("monospace:weight=bold:slant=italic", "monospace:slant=100:weight=200"); try expectRoundTrip(":lang=ja", ":lang=ja"); try expectRoundTrip("Foo\\-Bar", "Foo\\-Bar"); try expectRoundTrip("sans:weight=[light bold]", "sans:weight=[50 200]"); try expectRoundTrip("sans:weight=[50 100]", "sans:weight=[50 100]"); try expectRoundTrip("sans:antialias=false:pixelsize=10.5", "sans:pixelsize=10.5:antialias=False"); try expectRoundTrip("sans:charset=41-43", "sans:charset=41-43"); try expectRoundTrip("sans:normal", "sans"); try expectRoundTrip(":outline", ":outline=True");}
test "unknown objects make the name invalid" { try testing.expectError(error.InvalidName, parse(testing.allocator, "sans:frobnicate=1")); try testing.expectError(error.InvalidName, parse(testing.allocator, "sans:weight=heavyish"));}
test "parse booleans" { try testing.expectEqual(value.Bool.true, parseBool("on").?); try testing.expectEqual(value.Bool.false, parseBool("Off").?); try testing.expectEqual(value.Bool.dontcare, parseBool("dontcare").?); try testing.expectEqual(@as(?value.Bool, null), parseBool("maybe"));}