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.
19 kB · 460 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-FileCopyrightText: © 2001 Keith Packard// SPDX-License-Identifier: MIT AND HPND-sell-variant
//! What a BDF or PCF font says about itself, as `FcFreeTypeQueryFaceInternal`//! reads it through FreeType's `bdf` and `pcf` drivers.//!//! These fonts have no sfnt tables, so nearly everything comes from their//! XLFD properties, read as `FT_Get_BDF_Property` hands them out, and from//! what the drivers make of them: the family from `FAMILY_NAME`, the style//! from `WEIGHT_NAME`, `SLANT`, `SETWIDTH_NAME` and `ADD_STYLE_NAME`, the//! pixel size from `PIXEL_SIZE`, and a Unicode charmap only for a font//! encoded in ISO 10646, ISO 8859-1 or ASCII.//!//! FreeType opens a gzip-compressed PCF font but not a gzip-compressed BDF//! one, and neither compressed with `compress` or `bzip2`, which zig-font//! does not read either.
const std = @import("std");const Allocator = std.mem.Allocator;const font = @import("font");const BitmapFont = font.BitmapFont;const str = @import("../str.zig");const object = @import("../object.zig");const CharSet = @import("../CharSet.zig");const LangSet = @import("../LangSet.zig");const Pattern = @import("../Pattern.zig");const query = @import("query.zig");
pub const Error = Allocator.Error;
/// The pattern for `data`, a whole BDF or PCF file, possibly/// gzip-compressed, appended to `out`; nothing if FreeType would not open/// it or fontconfig would skip it. As `query.queryAll`.pub fn queryAll(gpa: Allocator, keep: Allocator, data: []const u8, file: []const u8, out: *std.ArrayList(*Pattern)) Error!void { const kind = BitmapFont.Kind.detect(data) orelse return; var bf = BitmapFont.parse(gpa, data, .{ .copy = false }) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return, }; defer bf.deinit(); // Only the PCF driver undoes compression. if (kind == .gzip and bf.format != .pcf) return; if (bf.pcf) |p| { // The tables FreeType cannot open a PCF font without, where zig-font // makes do. if (p.accelerators == null and p.bdf_accelerators == null) return; for (p.tables) |t| { if (t.type == .properties) break; } else return; } if (try queryFace(gpa, keep, &bf, file)) |pat| try out.append(gpa, pat);}
/// The properties as `FT_Get_BDF_Property` answers for them.const Properties = struct { font: *const BitmapFont, arena: Allocator,
/// The PCF driver takes a property's first value; BDF's parser /// replaces it with each one it reads, so a BDF font's last value is /// the one it keeps. fn find(p: Properties, name: []const u8) ?BitmapFont.Value { switch (p.font.format) { .pcf => return p.font.property(name), .bdf => { var found: ?BitmapFont.Value = null; for (p.font.properties) |prop| { if (std.mem.eql(u8, prop.name, name)) found = prop.value; } return found; }, } }
fn has(p: Properties, name: []const u8) bool { return p.find(name) != null; }
/// A `BDF_PROPERTY_TYPE_ATOM`. A PCF property is one if the file says /// it is a string. A BDF property is one if it is a string property in /// XLFD, which everything asked for here is, whatever its value looks /// like, and has no atom if its value is empty. fn atom(p: Properties, name: []const u8) Error!?[]const u8 { const v = p.find(name) orelse return null; return switch (p.font.format) { .pcf => switch (v) { .string => |s| s, .integer => null, }, .bdf => switch (v) { .string => |s| if (s.len == 0) null else s, .integer => |i| try std.fmt.allocPrint(p.arena, "{d}", .{i}), }, }; }
/// A `BDF_PROPERTY_TYPE_INTEGER` or `_CARDINAL`, as a signed 32-bit /// value, which is all fontconfig makes of either. A BDF property that /// XLFD makes a number is one whatever its value looks like; `cardinal` /// says it is unsigned, and so reads no sign. FreeType reads the value /// as far as its digits go, and zig-font keeps as a string either a /// quoted value, whose quote stops FreeType at once, or one that is no /// 32-bit number, which is far rarer; a string is taken to be the /// first, and so 0. fn integer(p: Properties, name: []const u8, cardinal: bool) ?i32 { const v = p.find(name) orelse return null; return switch (p.font.format) { .pcf => switch (v) { .integer => |i| i, .string => null, }, .bdf => switch (v) { .integer => |i| if (cardinal and i < 0) 0 else i, .string => 0, }, }; }};
/// `FcStringIsConst`: the whole of `s` naming one of `consts`.pub fn isConst(s: []const u8, consts: []const query.StringConst) i32 { for (consts) |c| { if (str.eqlIgnoreBlanksAndCase(s, c[0])) return c[1]; } return -1;}
/// `bdf_interpret_style` and `pcf_interpret_style`: the style name, and/// whether the face is bold and italic.const Style = struct { name: []const u8, bold: bool = false, italic: bool = false,
fn init(a: Allocator, props: Properties) Error!Style { var style: Style = .{ .name = "" }; var parts: [4]?[]const u8 = @splat(null); if (try props.atom("SLANT")) |s| if (s.len > 0) switch (s[0]) { 'O', 'o' => { style.italic = true; parts[2] = "Oblique"; }, 'I', 'i' => { style.italic = true; parts[2] = "Italic"; }, else => {}, }; if (try props.atom("WEIGHT_NAME")) |s| if (s.len > 0 and (s[0] == 'B' or s[0] == 'b')) { style.bold = true; parts[1] = "Bold"; }; // A set width or added style starting with N is taken to be // "Normal", and left out. if (try props.atom("SETWIDTH_NAME")) |s| if (s.len > 0 and s[0] != 'N' and s[0] != 'n') { parts[3] = s; }; if (try props.atom("ADD_STYLE_NAME")) |s| if (s.len > 0 and s[0] != 'N' and s[0] != 'n') { parts[0] = s; };
var name: std.ArrayList(u8) = .empty; for (parts, 0..) |part, i| { const s = part orelse continue; if (name.items.len > 0) try name.append(a, ' '); const start = name.items.len; try name.appendSlice(a, s); // Spaces in the two names taken whole become dashes. if (i == 0 or i == 3) std.mem.replaceScalar(u8, name.items[start..], ' ', '-'); } style.name = if (name.items.len == 0) "Regular" else name.items; return style; }};
/// The codes of the font's charmap, in order, each with its glyph's place/// in `glyphs`. Empty unless the charmap is Unicode.fn charmap(a: Allocator, bf: *const BitmapFont, props: Properties) Error![]const BitmapFont.Mapping { if (!try isUnicode(props)) return &.{}; var out: std.ArrayList(BitmapFont.Mapping) = .empty; switch (bf.format) { .pcf => { // The PCF driver keeps at most 65,534 glyphs, and a code whose // glyph is past them has none. for (bf.encodings) |m| { if (m.glyph < 65534) try out.append(a, m); } }, .bdf => { // A glyph given only an alternative encoding, `ENCODING -1 n`, // is encoded at `n`; one outside Unicode is not encoded. for (bf.glyphs, 0..) |g, i| { const code = g.encoding orelse g.alternate_encoding orelse continue; if (code >= 0x110000) continue; try out.append(a, .{ .code = code, .glyph = @intCast(i) }); } std.mem.sort(BitmapFont.Mapping, out.items, {}, struct { fn lessThan(_: void, x: BitmapFont.Mapping, y: BitmapFont.Mapping) bool { return x.code < y.code; } }.lessThan); // The BDF driver's glyph index is the place in this sorted list // plus one, since FreeType keeps glyph 0 for the default // character, and in 16 bits; the 65,536th wraps to 0, which // ends a walk of the charmap. var n: usize = 0; for (out.items, 0..) |m, place| { if (place + 1 == 0x10000) break; if (n > 0 and out.items[n - 1].code == m.code) continue; out.items[n] = m; n += 1; } out.items.len = n; }, } return out.items;}
/// Whether the driver gives the font a Unicode charmap: `ISO10646` in any/// encoding, `ISO8859` `1`, or `ISO646.1991` `IRV`, the registry's `ISO`/// in either case.fn isUnicode(props: Properties) Error!bool { const registry = try props.atom("CHARSET_REGISTRY") orelse return false; const encoding = try props.atom("CHARSET_ENCODING") orelse return false; if (registry.len < 3 or !std.ascii.eqlIgnoreCase(registry[0..3], "iso")) return false; const rest = registry[3..]; return std.mem.eql(u8, rest, "10646") or (std.mem.eql(u8, rest, "8859") and std.mem.eql(u8, encoding, "1")) or (std.mem.eql(u8, rest, "646.1991") and std.mem.eql(u8, encoding, "IRV"));}
/// The advance `FT_Get_Advance` reports for a glyph, in pixels.fn advance(bf: *const BitmapFont, glyph: u32) i32 { const g = bf.glyphs[glyph]; return switch (bf.format) { .pcf => g.dwidth.?[0], // `bdf_atous_` reads no sign, and a glyph without `DWIDTH` advances // by the width of its box. .bdf => if (g.dwidth) |d| @max(d[0], 0) else @intCast(g.bbx.width), };}
/// `FcFreeTypeSpacing`, as `query.spacing` but over the charmap.fn spacing(bf: *const BitmapFont, map: []const BitmapFont.Mapping) i32 { var advances: [3]i32 = .{ 0, 0, 0 }; var num: usize = 0; for (map) |m| { if (num == 3) break; const adv = advance(bf, m.glyph); if (adv == 0) continue; for (advances[0..num]) |seen| { if (query.approxEqual(adv, seen)) break; } else { advances[num] = adv; num += 1; } } if (num <= 1) return object.spacing.mono; if (num == 2 and query.approxEqual(@min(advances[0], advances[1]) * 2, @max(advances[0], advances[1]))) return object.spacing.dual; return object.spacing.proportional;}
/// `FT_MulDiv` for operands that are not negative.fn mulDiv(a: i64, b: i64, c: i64) i64 { return @divTrunc(a * b + @divTrunc(c, 2), c);}
/// A property as the drivers read sizes from it: truncated to 16 bits, then/// made positive.fn short(v: i32) i64 { return @abs(@as(i16, @truncate(v)));}
/// `FcGetPixelSize`: `PIXEL_SIZE` if it is a number, otherwise the size the/// driver worked out for the font's one strike.fn pixelSize(bf: *const BitmapFont, props: Properties) f64 { if (props.integer("PIXEL_SIZE", false)) |v| return @floatFromInt(v); // The PCF driver reads a `PIXEL_SIZE` that is a string as a number // anyway, getting the string's address, which is always far past the // largest size it allows and so clamped to it. if (bf.format == .pcf and props.has("PIXEL_SIZE")) return 0x7fff;
// `bsize->size`, in 26.6 points. var size: i64 = 0; var resolution_y: i64 = 0; if (props.integer("POINT_SIZE", false)) |v| { // From decipoints at 72.27 to the inch to points at 72. size = if (@abs(v) > 0x504c2) 0x7fff else mulDiv(@abs(v), 64 * 7200, 72270); } else if (bf.bdf) |h| { if (h.size.point_size != 0) { size = if (h.size.point_size > 0x7fff) 0x7fff else @as(i64, h.size.point_size) << 6; } else { size = averageWidth(bf, props) * 64; } } if (props.integer("RESOLUTION_Y", true)) |v| { resolution_y = if (@abs(v) > 0x7fff) 0x7fff else short(v); } else if (bf.bdf) |h| { resolution_y = if (@abs(h.size.resolution_y) > 0x7fff) 0x7fff else short(h.size.resolution_y); } const y_ppem = if (resolution_y != 0) mulDiv(size, resolution_y, 72) else size; return @as(f64, @floatFromInt(y_ppem)) / 64.0;}
/// `bsize->width` in the BDF driver, for a font with no point size at all.fn averageWidth(bf: *const BitmapFont, props: Properties) i64 { if (props.integer("AVERAGE_WIDTH", false)) |v| { if (@abs(v) > 0x7fff * 10 - 5) return 0x7fff; return @abs(@as(i16, @truncate(@divTrunc(v + 5, 10)))); } const box = bf.bounding_box; const clamp = struct { fn f(v: i64) i64 { return std.math.clamp(v, 0, 0x7fff); } }.f; const ascent = clamp(props.integer("FONT_ASCENT", false) orelse @as(i64, box.height) + box.y); const descent = clamp(props.integer("FONT_DESCENT", false) orelse -@as(i64, box.y)); const height: i64 = @as(i16, @truncate(ascent + descent)); return @divTrunc(height * 2 + 1, 3);}
/// `FcFreeTypeQueryFaceInternal` for a bitmap font. Null if fontconfig/// skips it: a PCF or BDF font with no Unicode characters, which is/// usually a Unicode font recoded into some legacy encoding.fn queryFace(gpa: Allocator, keep: Allocator, bf: *const BitmapFont, file: []const u8) Error!?*Pattern { var arena_state: std.heap.ArenaAllocator = .init(gpa); defer arena_state.deinit(); const arena = arena_state.allocator(); const props: Properties = .{ .font = bf, .arena = arena };
const map = try charmap(arena, bf, props); var b: CharSet.Builder = .{}; for (map) |m| try b.add(arena, m.code); const charset = try b.finish(arena); if (charset.count() == 0 and props.has("PIXEL_SIZE")) return null;
const pat = try gpa.create(Pattern); errdefer gpa.destroy(pat); pat.* = .init(gpa); errdefer pat.deinit(); try pat.preheat(4096); const a = pat.allocator();
try pat.addBool(.outline, false); try pat.addBool(.color, false); try pat.addBool(.scalable, false); try pat.addBool(.variable, false);
var has_family = false; if (try props.atom("FAMILY_NAME")) |family| { if (!str.eqlIgnoreBlanksAndCase(family, "")) { try pat.addString(.family, try a.dupe(u8, family)); try pat.addString(.familylang, "en"); has_family = true; } } const style: Style = try .init(arena, props); try pat.addString(.style, if (str.eqlIgnoreBlanksAndCase(style.name, "")) "Regular" else try a.dupe(u8, style.name)); try pat.addString(.stylelang, "en"); if (!has_family and file.len > 0) { try query.addFamilyFromFile(pat, file); has_family = true; } const discard = struct { fn f(g: Allocator, p: *Pattern) ?*Pattern { p.deinit(); g.destroy(p); return null; } }.f; if (!try query.addFullnameFromFamily(pat)) return discard(gpa, pat); // Neither driver knows a PostScript name. if (!try query.addPostscriptNameFromFamily(pat)) return discard(gpa, pat);
if (file.len > 0) try pat.addString(.file, file); try pat.addInteger(.index, 0); try pat.addInteger(.fontversion, 0); try pat.addInteger(.order, 0); try pat.addBool(.fonthashint, false);
const foundry = try props.atom("FOUNDRY"); var width: f64 = -1; if (props.integer("RELATIVE_SETWIDTH", true)) |v| { width = switch (@divTrunc(v +% 5, 10)) { 1 => object.width.ultracondensed, 2 => object.width.extracondensed, 3 => object.width.condensed, 4 => object.width.semicondensed, 5 => object.width.normal, 6 => object.width.semiexpanded, 7 => object.width.expanded, 8 => object.width.extraexpanded, 9 => object.width.ultraexpanded, else => -1, }; } if (width == -1) if (try props.atom("SETWIDTH_NAME")) |s| { width = @floatFromInt(isConst(s, &query.width_consts)); };
var weight: f64 = -1; var slant: i32 = -1; var decorative = false; var st: usize = 0; while (pat.getString(.style, st)) |s| : (st += 1) { if (weight == -1) weight = @floatFromInt(query.containsConst(s, &query.weight_consts)); if (width == -1) width = @floatFromInt(query.containsConst(s, &query.width_consts)); if (slant == -1) slant = query.containsConst(s, &query.slant_consts); if (!decorative) decorative = query.containsConst(s, &query.decorative_consts) > 0; } if (slant == -1) slant = if (style.italic) object.slant.italic else object.slant.roman; if (weight == -1) weight = if (style.bold) object.weight.bold else object.weight.medium; if (width == -1) width = object.width.normal;
try pat.addInteger(.slant, slant); try pat.addDouble(.weight, weight); try pat.addDouble(.width, width); try pat.addString(.foundry, if (foundry) |f| try a.dupe(u8, f) else "unknown"); try pat.addBool(.decorative, decorative);
const cs = try keep.create(CharSet); cs.* = try charset.dupe(keep); try pat.addBool(.symbol, false); var spacing_value = spacing(bf, map); // The `SPACING` property, where it says, rather than the advances. if (try props.atom("SPACING")) |s| { if (std.mem.eql(u8, s, "c") or std.mem.eql(u8, s, "C")) { spacing_value = object.spacing.charcell; } else if (std.mem.eql(u8, s, "m") or std.mem.eql(u8, s, "M")) { spacing_value = object.spacing.mono; } else if (std.mem.eql(u8, s, "p") or std.mem.eql(u8, s, "P")) { spacing_value = object.spacing.proportional; } } _ = try pat.addBorrowed(.{ .builtin = .charset }, .{ .charset = cs }, .strong, true); const ls = try keep.create(LangSet); ls.* = LangSet.fromCharSet(cs.*, null); _ = try pat.addBorrowed(.{ .builtin = .lang }, .{ .langset = ls }, .strong, true); if (spacing_value != object.spacing.proportional) try pat.addInteger(.spacing, spacing_value);
try pat.addDouble(.pixelsize, pixelSize(bf, props)); try pat.addBool(.antialias, false); try pat.addString(.fontformat, switch (bf.format) { .bdf => "BDF", .pcf => "PCF", }); try pat.addBool(.namedinstance, false); try query.addGenericFamily(pat); return pat;}
const testing = std.testing;
test "a set width named in full" { try testing.expectEqual(@as(i32, object.width.semicondensed), isConst("SemiCondensed", &query.width_consts)); try testing.expectEqual(@as(i32, -1), isConst("Semi Condensed Bold", &query.width_consts));}