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.
49 kB · 1208 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-FileCopyrightText: © 2001 Keith Packard// SPDX-License-Identifier: MIT AND HPND-sell-variant
//! What a font file says about itself, as a pattern per face: a port of//! `FcFreeTypeQueryFaceInternal` and `FcFreeTypeQueryAll` from//! `fcfreetype.c`.//!//! A file gives one pattern for each face of a collection; a variable font//! gives one for its default instance, one for each named instance that//! differs from it, and one for the font as a whole, whose weight, width//! and size are ranges.
const std = @import("std");const Allocator = std.mem.Allocator;const font = @import("font");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 Face = @import("Face.zig");const legacy = @import("legacy.zig");const bitmap = @import("bitmap.zig");const sfnt_tables = @import("../sfnt_tables.zig");const tables = @import("tables");const Builtin = object.Builtin;
pub const Error = Allocator.Error;
/// The patterns for every face of `data`, a whole font file, appended to/// `out`. `file` is recorded as each pattern's `file`. A file this cannot/// read gives no patterns. Each pattern is allocated with `gpa` and owned/// by the caller; the charset and language set the faces of a font share/// are allocated with `keep`, which must outlive them.pub fn queryAll(gpa: Allocator, keep: Allocator, data: []const u8, file: []const u8, out: *std.ArrayList(*Pattern)) Error!void { var arena_state: std.heap.ArenaAllocator = .init(gpa); defer arena_state.deinit(); const arena = arena_state.allocator();
const flavor = font.sfnt.Flavor.detect(data) orelse return bitmap.queryAll(gpa, keep, data, file, out); var sfnt_data = data; var offsets: []const u32 = &.{0}; switch (flavor) { .truetype, .apple_truetype, .cff => {}, .collection => { const header = font.sfnt.CollectionHeader.parse(arena, data) catch return; offsets = header.offsets; }, .woff => sfnt_data = font.woff.decode(arena, data) catch return, // WOFF 2.0 needs Brotli; Type 1 in an sfnt wrapper is not a face // FreeType's sfnt driver opens. .woff2, .type1 => return, } const wrapper: []const u8 = switch (flavor) { .woff => "WOFF", else => "SFNT", };
for (offsets, 0..) |offset, face_num| { var face = Face.init(arena, sfnt_data, offset) catch { // FreeType stops at the first face it cannot open. break; }; var shared: Shared = .{}; var instance: u32 = 0; // Selecting a named instance changes FreeType's face index, and // it stays changed for the rest of this face. var ftglue_index: u32 = @intCast(face_num); while (true) { const skip = instance > 0 and instance != 0x8000 and instance <= face.num_instances and face.instanceIsDefault(instance); if (!skip) { const id = (instance << 16) | @as(u32, @intCast(face_num)); if (instance > 0 and instance != 0x8000) ftglue_index = id; // Resetting the variations for the whole-font pattern does // nothing while a named instance is selected: FreeType fills // the axes it was not given from that instance, sees no // change, and keeps it. const variation_instance = if (instance == 0x8000) ftglue_index >> 16 else instance; if (try queryFace(gpa, arena, keep, &face, file, wrapper, id, ftglue_index, variation_instance, &shared)) |pat| { try out.append(gpa, pat); } else if (instance != 0x8000) { // fontconfig gives up on the file when a face fails. return; } } if (instance < face.num_instances) { instance += 1; } else if (instance == face.num_instances and face.num_instances > 0) { instance = 0x8000; } else break; } }}
/// What the faces of one font share: the charset and languages, computed/// once from the default instance.const Shared = struct { charset: ?*const CharSet = null, langset: ?*const LangSet = null, /// Every glyph the charmap maps a code point to, each once, in the /// order of the first code point that maps to it: all `spacing` needs /// to know of the charmap, gathered while the charset is. glyphs: []const u32 = &.{}, /// The parsed `gvar` and friends, for measuring named instances. variations: ?Face.Variations = null,};
fn discard(gpa: Allocator, pat: *Pattern) ?*Pattern { pat.deinit(); gpa.destroy(pat); return null;}
fn fixedToDouble(raw: i32) f64 { return @as(f64, @floatFromInt(raw)) / 65536.0;}
/// `FcWeightFromOpenTypeDouble`.pub fn weightFromOpenType(ot_in: f64) f64 { const map = [_][2]i32{ .{ 0, object.weight.thin }, .{ 100, object.weight.thin }, .{ 200, object.weight.extralight }, .{ 300, object.weight.light }, .{ 350, object.weight.demilight }, .{ 380, object.weight.book }, .{ 400, object.weight.regular }, .{ 500, object.weight.medium }, .{ 600, object.weight.demibold }, .{ 700, object.weight.bold }, .{ 800, object.weight.extrabold }, .{ 900, object.weight.black }, .{ 1000, object.weight.extrablack }, }; if (ot_in < 0) return -1; const ot = @min(ot_in, 1000); var i: usize = 1; while (ot > @as(f64, @floatFromInt(map[i][0]))) i += 1; if (ot == @as(f64, @floatFromInt(map[i][0]))) return @floatFromInt(map[i][1]); const x1: f64 = @floatFromInt(map[i - 1][0]); const dx: f64 = @floatFromInt(map[i][0] - map[i - 1][0]); const dy: f64 = @floatFromInt(map[i][1] - map[i - 1][1]); return @as(f64, @floatFromInt(map[i - 1][1])) + (ot - x1) * dy / dx;}
pub const StringConst = struct { []const u8, i32 };
pub const weight_consts = [_]StringConst{ .{ "thin", object.weight.thin }, .{ "extralight", object.weight.extralight }, .{ "ultralight", object.weight.extralight }, .{ "demilight", object.weight.demilight }, .{ "semilight", object.weight.demilight }, .{ "light", object.weight.light }, .{ "book", object.weight.book }, .{ "regular", object.weight.regular }, .{ "normal", object.weight.regular }, .{ "medium", object.weight.medium }, .{ "demibold", object.weight.demibold }, .{ "demi", object.weight.demibold }, .{ "semibold", object.weight.demibold }, .{ "extrabold", object.weight.extrabold }, .{ "superbold", object.weight.extrabold }, .{ "ultrabold", object.weight.extrabold }, .{ "bold", object.weight.bold }, .{ "ultrablack", object.weight.extrablack }, .{ "superblack", object.weight.extrablack }, .{ "extrablack", object.weight.extrablack }, .{ "<ultra", object.weight.extrabold }, .{ "black", object.weight.black }, .{ "heavy", object.weight.black },};
pub const width_consts = [_]StringConst{ .{ "ultracondensed", object.width.ultracondensed }, .{ "extracondensed", object.width.extracondensed }, .{ "semicondensed", object.width.semicondensed }, .{ "condensed", object.width.condensed }, .{ "normal", object.width.normal }, .{ "semiexpanded", object.width.semiexpanded }, .{ "extraexpanded", object.width.extraexpanded }, .{ "ultraexpanded", object.width.ultraexpanded }, .{ "expanded", object.width.expanded }, .{ "extended", object.width.expanded },};
pub const slant_consts = [_]StringConst{ .{ "italic", object.slant.italic }, .{ "kursiv", object.slant.italic }, .{ "oblique", object.slant.oblique },};
pub const decorative_consts = [_]StringConst{ .{ "shadow", 1 }, .{ "caps", 1 }, .{ "antiqua", 1 }, .{ "romansc", 1 }, .{ "embosed", 1 }, .{ "dunhill", 1 },};
/// `FcStringContainsConst`: a name starting `<` must be a whole word.pub fn containsConst(s: []const u8, consts: []const StringConst) i32 { for (consts) |c| { if (c[0][0] == '<') { if (str.containsWord(s, c[0][1..])) return c[1]; } else if (str.containsIgnoreBlanksAndCase(s, c[0])) return c[1]; } return -1;}
/// `FcLooksLikeSJIS`.fn looksLikeSjis(b: []const u8) bool { var high: usize = 0; var low: usize = 0; for (b) |c| { if (c & 0x80 != 0) high += 1 else low += 1; } return high * 2 > low;}
/// `FcSfntNameTranscode`: a name record's text as UTF-8, or null if its/// encoding is one this does not decode or it is blank. Like fontconfig's,/// the result ends at the first NUL.fn transcode(arena: Allocator, n: Face.NameRecord) Allocator.Error!?[]const u8 { const Kind = enum { utf16, mac_roman, latin1, legacy }; var legacy_kind: legacy.Kind = undefined; var kind: Kind = switch (n.platform) { 0 => .utf16, 1 => switch (n.encoding) { 0 => .mac_roman, 1 => blk: { legacy_kind = .sjis; break :blk .legacy; }, else => return null, }, 2 => switch (n.encoding) { 0, 2 => .latin1, 1 => .utf16, else => return null, }, 3 => switch (n.encoding) { 0, 1, 10 => .utf16, 2, 3, 4, 6 => blk: { legacy_kind = switch (n.encoding) { 2 => .sjis_win, 3 => .gb18030, 4 => .big5, else => .johab, }; break :blk .legacy; }, // Wansung, which fontconfig asks iconv for by a name glibc // does not know, and so never decodes. else => return null, }, else => return null, }; if (kind == .mac_roman) { if (n.language == 0 and looksLikeSjis(n.bytes)) { kind = .legacy; legacy_kind = .sjis; } else if (n.language == 11) { kind = .legacy; legacy_kind = .sjis; } else if (n.language >= 0x100) { switch (n.language) { 0x0409 => kind = .latin1, 0x0411 => { kind = .legacy; legacy_kind = .sjis_win; }, else => return null, } } }
var out: std.ArrayList(u8) = .empty; var buf: [8]u8 = undefined; switch (kind) { .legacy => { // What iconv fails on, fontconfig tries once more as UTF-16. const utf8 = try legacy.decode(arena, legacy_kind, n.bytes) orelse return transcode(arena, .{ .platform = 0, .encoding = 0, .language = n.language, .name_id = n.name_id, .bytes = n.bytes }); // The result is a C string. const end = std.mem.findScalar(u8, utf8, 0) orelse utf8.len; try out.appendSlice(arena, utf8[0..end]); }, .utf16 => { var i: usize = 0; while (i + 1 < n.bytes.len) { const a = std.mem.readInt(u16, n.bytes[i..][0..2], .big); i += 2; var cp: u32 = a; if (a & 0xfc00 == 0xd800) { if (i + 1 >= n.bytes.len) return null; const b = std.mem.readInt(u16, n.bytes[i..][0..2], .big); i += 2; if (b & 0xfc00 != 0xdc00) return null; cp = ((@as(u32, a) & 0x3ff) << 10 | (b & 0x3ff)) + 0x10000; } if (cp == 0) break; try out.appendSlice(arena, buf[0..str.ucs4ToUtf8(cp, &buf)]); } }, .latin1, .mac_roman => { for (n.bytes) |c| { if (c == 0) break; var cp: u32 = c; if (kind == .mac_roman and c >= 128) cp = sfnt_tables.mac_roman[c - 128]; try out.appendSlice(arena, buf[0..str.ucs4ToUtf8(cp, &buf)]); } }, } if (str.eqlIgnoreBlanksAndCase(out.items, "")) return null; return out.items;}
/// `FcSfntNameLanguage`.fn nameLanguage(n: Face.NameRecord) ?[]const u8 { var language = n.language; if (n.platform == 1 and n.encoding == 0 and looksLikeSjis(n.bytes)) language = 11; for (sfnt_tables.languages) |l| { if (l.platform == n.platform and (l.language == 0xffff or l.language == language)) { return if (l.lang.len == 0) null else l.lang; } } return null;}
/// `FcNoticeFoundry`.fn noticeFoundry(notice: []const u8) ?[]const u8 { for (sfnt_tables.notice_foundries) |f| { if (std.mem.find(u8, notice, f[0]) != null) return f[1]; } return null;}
/// `FcStringInPatternElement`.fn stringInElement(p: *const Pattern, b: Builtin, s: []const u8) bool { const e = p.findBuiltin(b) orelse return false; var it = e.iterator(); while (it.next()) |l| { const v = switch (l.value) { .string => |v| v, else => return false, }; if (str.eqlIgnoreBlanksAndCase(v, s)) return true; } return false;}
const platform_order = [_]u16{ 3, 0, 1, 2 };const nameid_order = [_]u16{ Face.name_ids.wws_family, Face.name_ids.typographic_family, Face.name_ids.family, Face.name_ids.mac_full_name, Face.name_ids.full_name, Face.name_ids.wws_subfamily, Face.name_ids.typographic_subfamily, Face.name_ids.subfamily, Face.name_ids.trademark, Face.name_ids.manufacturer,};
/// The value of a string object whose language is `en`, or else the first.fn englishValue(p: *const Pattern, value_obj: Builtin, lang_obj: Builtin) ?[]const u8 { var n: usize = 0; var found = false; while (p.getString(lang_obj, n)) |lang| : (n += 1) { if (std.mem.eql(u8, lang, "en")) { found = true; break; } } if (!found) n = 0; return p.getString(value_obj, n);}
/// `FcFreeTypeQueryFaceInternal`. Null if the face gives no pattern: a/// variable font's whole-font pattern when none of its axes is weight,/// width or optical size.fn queryFace( gpa: Allocator, arena: Allocator, keep: Allocator, face: *const Face, file: []const u8, wrapper: []const u8, id_in: u32, /// FreeType's `face_index` when `ftglue` reads the tables. ftglue_index: u32, /// The named instance whose coordinates FreeType has set, which is what /// advances are measured at. variation_instance: u32, shared: *Shared,) Error!?*Pattern { const pat = try gpa.create(Pattern); errdefer gpa.destroy(pat); pat.* = .init(gpa); errdefer pat.deinit(); // What a face's pattern comes to, give or take, so that its arena is // one allocation rather than a dozen. Measured: smaller ends up using // more memory, the arena growing past it by half again. try pat.preheat(4096); const a = pat.allocator();
var slant: i32 = -1; var weight: f64 = -1; var width: f64 = -1; var decorative = false; var variable = false; var variable_weight = false; var variable_width = false; var variable_size = false; var weight_mult: f64 = 1.0; var width_mult: f64 = 1.0; var instance: ?Face.Instance = null;
// FreeType's scalable flag is what fontconfig calls an outline. try pat.addBool(.outline, face.scalable); try pat.addBool(.color, face.color); try pat.addBool(.scalable, face.scalable or face.color);
var id = id_in; const instance_num = id >> 16; if (instance_num != 0) { const fvar = face.fvar orelse return discard(gpa, pat); if (instance_num == 0x8000) { for (fvar.axes) |axis| { var min = fixedToDouble(axis.min_value.raw); const def = fixedToDouble(axis.default_value.raw); var max = fixedToDouble(axis.max_value.raw); if (min > def or def > max or min == max) continue; var obj: ?Builtin = null; if (axis.tag.eql(.init("wght"))) { obj = .weight; min = weightFromOpenType(min); max = weightFromOpenType(max); variable_weight = true; weight = 0; } else if (axis.tag.eql(.init("wdth"))) { obj = .width; variable_width = true; width = 0; } else if (axis.tag.eql(.init("opsz"))) { obj = .size; variable_size = true; } if (obj) |o| { try pat.addRange(o, min, max); variable = true; } } if (!variable) return discard(gpa, pat); // The pattern for the font as a whole is indexed as its face. id &= 0xffff; } else if (instance_num - 1 < face.instances.len) { const inst = face.instances[instance_num - 1]; instance = inst; for (fvar.axes, inst.coords) |axis, coord| { const v = fixedToDouble(coord.raw); const def = fixedToDouble(axis.default_value.raw); const mult = if (def != 0) v / def else 1; if (axis.tag.eql(.init("wght"))) { weight_mult = mult; } else if (axis.tag.eql(.init("wdth"))) { width_mult = mult; } else if (axis.tag.eql(.init("opsz"))) { try pat.addDouble(.size, v); } } } else return discard(gpa, pat); } else if (face.fvar) |fvar| { for (fvar.axes) |axis| { if (axis.tag.eql(.init("opsz"))) { try pat.addDouble(.size, fixedToDouble(axis.default_value.raw)); variable_size = true; } } } try pat.addBool(.variable, variable);
// The foundry: the OS/2 vendor, else a notice in the names. var foundry: ?[]const u8 = null; if (face.os2) |os2| { if (os2.vendor[0] != 0) { const end = std.mem.findScalar(u8, &os2.vendor, 0) orelse 4; foundry = try a.dupe(u8, os2.vendor[0..end]); } }
var nfamily: usize = 0; var nstyle: usize = 0; var nfullname: usize = 0; for (platform_order) |platform| { for (nameid_order) |nameid| { var lookupid = nameid; if (instance) |inst| { if (nameid == Face.name_ids.wws_subfamily or nameid == Face.name_ids.typographic_subfamily or nameid == Face.name_ids.full_name) continue; if (nameid == Face.name_ids.subfamily) lookupid = inst.strid; } const records = try face.namesFor(arena, platform, lookupid); for (records) |rec| { var obj: ?Builtin = null; var objlang: Builtin = undefined; var count: *usize = undefined; switch (nameid) { Face.name_ids.wws_family, Face.name_ids.typographic_family, Face.name_ids.family => { obj = .family; objlang = .familylang; count = &nfamily; }, Face.name_ids.mac_full_name, Face.name_ids.full_name => if (!variable) { obj = .fullname; objlang = .fullnamelang; count = &nfullname; }, Face.name_ids.wws_subfamily, Face.name_ids.typographic_subfamily, Face.name_ids.subfamily => if (!variable) { obj = .style; objlang = .stylelang; count = &nstyle; }, Face.name_ids.trademark, Face.name_ids.manufacturer => if (foundry == null) { if (try transcode(arena, rec)) |utf8| foundry = noticeFoundry(utf8); }, else => {}, } const o = obj orelse continue; const utf8 = try transcode(a, rec) orelse continue; const lang = nameLanguage(rec); // Trim surrounding whitespace. const trimmed = std.mem.trimEnd(u8, std.mem.trimStart(u8, utf8, " "), " \r\n"); if (stringInElement(pat, o, trimmed)) continue; try pat.addString(o, trimmed); try pat.addString(objlang, lang orelse "und"); count.* += 1; } } }
if (nfamily == 0) { if (try face.familyName(arena)) |fam| { if (!str.eqlIgnoreBlanksAndCase(fam, "")) { try pat.addString(.family, fam); try pat.addString(.familylang, "en"); nfamily += 1; } } } if (!variable and nstyle == 0) { var ss: []const u8 = "Regular"; if (try face.styleName(arena)) |s| { if (!str.eqlIgnoreBlanksAndCase(s, "")) ss = s; } try pat.addString(.style, ss); try pat.addString(.stylelang, "en"); nstyle += 1; } if (nfamily == 0 and file.len > 0) { try addFamilyFromFile(pat, file); nfamily += 1; }
// The full name, made from the English family and style if the font // has none. if (!variable and nfullname == 0) { if (!try addFullnameFromFamily(pat)) return discard(gpa, pat); nfullname += 1; }
if (!variable) { const ps = if (instance_num != 0) try face.instancePostscriptName(a, instance_num) else try face.postscriptName(a); if (ps) |name| { try pat.addString(.postscriptname, name[0..@min(name.len, 255)]); } else if (!try addPostscriptNameFromFamily(pat)) return discard(gpa, pat); }
if (file.len > 0) try pat.addString(.file, file); try pat.addInteger(.index, @bitCast(id)); try pat.addInteger(.fontversion, if (face.has_head) face.font_revision else 0); try pat.addInteger(.order, 0);
var exclusive_lang: ?[]const u8 = null; if (face.os2) |os2| { if (os2.version >= 1) exclusive_lang = LangSet.exclusiveFromOs2(os2.code_page_range1, os2.code_page_range2); weight = weightFromOpenType(@as(f64, @floatFromInt(os2.weight_class)) * weight_mult); switch (os2.width_class) { 1 => width = object.width.ultracondensed, 2 => width = object.width.extracondensed, 3 => width = object.width.condensed, 4 => width = object.width.semicondensed, 5 => width = object.width.normal, 6 => width = object.width.semiexpanded, 7 => width = object.width.expanded, 8 => width = object.width.extraexpanded, 9 => width = object.width.ultraexpanded, else => {}, } width *= width_mult; if (try capabilities(a, face, ftglue_index)) |cap| try pat.addString(.capability, cap); }
try pat.addBool(.fonthashint, hasHint(face, ftglue_index));
if (!variable_size) if (face.os2) |os2| if (os2.version >= 5) { const lower = @as(f64, @floatFromInt(os2.lower_optical_point_size)) / 20.0; const upper = @as(f64, @floatFromInt(os2.upper_optical_point_size)) / 20.0; if (lower == upper) try pat.addDouble(.size, lower) else try pat.addRange(.size, lower, upper); };
// FreeType answers BDF properties from a `BDF ` table, but only for the // strike matching the size, which nothing has set yet. if (foundry == null) if (face.bdfProperty("FOUNDRY", 0)) |prop| switch (prop) { .atom => |atom| foundry = atom, else => {}, }; if (width == -1) { if (face.bdfProperty("RELATIVE_SETWIDTH", 0)) |prop| { const value: ?i32 = switch (prop) { .integer => |v| v, .cardinal => |v| @bitCast(v), .atom => null, }; if (value) |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 (face.bdfProperty("SETWIDTH_NAME", 0)) |prop| switch (prop) { .atom => |atom| width = @floatFromInt(bitmap.isConst(atom, &width_consts)), else => {}, }; }
// Weight, width and slant from the style names, where the tables said // nothing. var st: usize = 0; while (pat.getString(.style, st)) |style| : (st += 1) { if (weight == -1) weight = @floatFromInt(containsConst(style, &weight_consts)); if (width == -1) width = @floatFromInt(containsConst(style, &width_consts)); if (slant == -1) slant = containsConst(style, &slant_consts); if (!decorative) decorative = containsConst(style, &decorative_consts) > 0; } if (slant == -1) slant = if (face.italic) object.slant.italic else object.slant.roman; if (weight == -1) weight = if (face.bold) object.weight.bold else object.weight.medium; if (width == -1) width = object.width.normal;
try pat.addInteger(.slant, slant); if (!variable_weight) try pat.addDouble(.weight, weight); if (!variable_width) try pat.addDouble(.width, width); try pat.addString(.foundry, foundry orelse "unknown"); try pat.addBool(.decorative, decorative);
// The charset and languages, shared by every instance. const cs = shared.charset orelse blk: { const p = try keep.create(CharSet); p.* = try charSet(arena, face, &shared.glyphs); p.* = try p.dupe(keep); shared.charset = p; break :blk p; }; const symbol = if (face.charmap) |m| m.encoding == .ms_symbol else false; try pat.addBool(.symbol, symbol); if (shared.variations == null and variation_instance != 0) shared.variations = .init(arena, face); var spacing_value = spacing(arena, face, shared.glyphs, if (shared.variations) |*v| v else null, variation_instance); // Measuring the spacing selected a strike, which is the one the rest of // the BDF properties come from. const ppem = face.selectedPpem(); if (face.bdfProperty("SPACING", ppem)) |prop| switch (prop) { .atom => |atom| { if (std.mem.eql(u8, atom, "c") or std.mem.eql(u8, atom, "C")) spacing_value = object.spacing.charcell; if (std.mem.eql(u8, atom, "m") or std.mem.eql(u8, atom, "M")) spacing_value = object.spacing.mono; if (std.mem.eql(u8, atom, "p") or std.mem.eql(u8, atom, "P")) spacing_value = object.spacing.proportional; }, else => {}, }; // fontconfig takes a font with BDF properties and no characters for a // PCF font in a legacy encoding, and skips it. if (cs.count() == 0 and face.bdfProperty("PIXEL_SIZE", ppem) != null) return discard(gpa, pat); _ = try pat.addBorrowed(.{ .builtin = .charset }, .{ .charset = cs }, .strong, true);
if (!symbol) { const ls = shared.langset orelse blk: { const p = try keep.create(LangSet); p.* = LangSet.fromCharSet(cs.*, exclusive_lang); shared.langset = p; break :blk p; }; _ = try pat.addBorrowed(.{ .builtin = .lang }, .{ .langset = ls }, .strong, true); } else { // Symbol fonts cover no language, Latin-1 or not. const empty: LangSet = .{}; _ = try pat.addWithBinding(.{ .builtin = .lang }, .{ .langset = &empty }, .strong, true); }
if (spacing_value != object.spacing.proportional) try pat.addInteger(.spacing, spacing_value);
if (!face.scalable) { // `FcGetPixelSize`, which trusts an integer `PIXEL_SIZE` property // over the strike's own ppem when there is only one strike. const only: ?i32 = if (face.fixed_sizes.len == 1) if (face.bdfProperty("PIXEL_SIZE", ppem)) |prop| switch (prop) { .integer => |v| v, else => null, } else null else null; for (face.fixed_sizes) |strike| try pat.addDouble(.pixelsize, @floatFromInt(only orelse strike.ppem)); try pat.addBool(.antialias, false); }
try pat.addString(.fontformat, if (face.hasTable("glyf") or (!face.hasTable("CFF ") and !face.hasTable("CFF2"))) "TrueType" else "CFF"); try pat.addBool(.namedinstance, id > 0xffff); try pat.addString(.fontwrapper, wrapper);
try addGenericFamily(pat); return pat;}
/// The family for a font that names none: the file's name, without its/// directory or its last extension.pub fn addFamilyFromFile(pat: *Pattern, file: []const u8) Error!void { const start = if (std.mem.findScalarLast(u8, file, '/')) |i| i + 1 else 0; const base = file[start..]; const end = std.mem.findScalarLast(u8, base, '.') orelse base.len; try pat.addString(.family, base[0..end]); try pat.addString(.familylang, "en");}
/// The full name for a font that has none: the English family and style./// False if the pattern lacks either, which fontconfig takes as a failed/// face.pub fn addFullnameFromFamily(pat: *Pattern) Error!bool { const family = englishValue(pat, .family, .familylang) orelse return false; const style = englishValue(pat, .style, .stylelang) orelse return false; const fam = std.mem.trimEnd(u8, family, " \t\n\r\x0b\x0c"); const sty = std.mem.trimStart(u8, style, " \t\n\r\x0b\x0c"); try pat.addString(.fullname, try std.mem.concat(pat.allocator(), u8, &.{ fam, " ", sty })); try pat.addString(.fullnamelang, "en"); return true;}
/// The PostScript name for a font FreeType finds none in: the English/// family, with what PostScript forbids replaced. False if the pattern has/// no family.pub fn addPostscriptNameFromFamily(pat: *Pattern) Error!bool { const family = englishValue(pat, .family, .familylang) orelse return false; const name = try pat.allocator().dupe(u8, family[0..@min(family.len, 255)]); for (name) |*c| { if (std.mem.findScalar(u8, "\x04()/<>[]{}\t\x0c\r\n ", c.*) != null) c.* = '-'; } try pat.addString(.postscriptname, name); return true;}
/// `FcGenericAliasGetClassification`, by way of the families in/// fontconfig's lists, or failing those a guess from the family name.pub fn addGenericFamily(pat: *Pattern) Error!void { var generic: i32 = object.generic_family.unknown; if (pat.findBuiltin(.family)) |e| { var it = e.iterator(); while (it.next()) |l| { const name = switch (l.value) { .string => |s| s, else => continue, }; const field = classification(name); if (field != 0) { for (0..15) |b| { if (field & (@as(u32, 1) << @intCast(b)) != 0) try pat.addInteger(.genericfamily, @intCast(b + 1)); } return; } if (str.findIgnoreCase(name, "mono") != null) { generic = object.generic_family.mono; break; } else if (str.findIgnoreCase(name, "sans") != null) { generic = object.generic_family.sans; break; } else if (str.findIgnoreCase(name, "serif") != null) { generic = object.generic_family.serif; break; } else if (str.findIgnoreCase(name, "emoji") != null) { generic = object.generic_family.emoji; break; } else if (str.findIgnoreCase(name, "math") != null) { generic = object.generic_family.math; break; } } } try pat.addInteger(.genericfamily, generic);}
fn classification(name: []const u8) u32 { for (tables.generic_families) |entry| { if (entry[0].len == name.len and str.eqlIgnoreBlanksAndCase(entry[0], name)) return entry[1]; } return 0;}
/// `FcFontHasHint`, through `ftglue` as fontconfig reads it.fn hasHint(face: *const Face, ftglue_index: u32) bool { if (Face.ftglueTable(face.data, ftglue_index, "fpgm") != null) return true; if (Face.ftglueTable(face.data, ftglue_index, "cvt ") != null) return true; if (Face.ftglueTable(face.data, ftglue_index, "prep")) |t| return t.size > 7; return false;}
/// `FcFontCapabilities`: `ttable:Silf` for a Graphite font, then/// `otlayout:` and each script of `GSUB` and `GPOS`, merged in order.fn capabilities(a: Allocator, face: *const Face, ftglue_index: u32) Error!?[]const u8 { const silf = face.hasTable("Silf"); const gpos = try scriptTags(a, face.data, Face.ftglueTable(face.data, ftglue_index, "GPOS")); const gsub = try scriptTags(a, face.data, Face.ftglueTable(face.data, ftglue_index, "GSUB")); if (!silf and gsub.len == 0 and gpos.len == 0) return null; var out: std.ArrayList(u8) = .empty; if (silf) try out.appendSlice(a, "ttable:Silf"); var i: usize = 0; var j: usize = 0; while (i < gsub.len or j < gpos.len) { var tag: u32 = undefined; if (i == gsub.len) { tag = gpos[j]; j += 1; } else if (j == gpos.len or gsub[i] < gpos[j]) { tag = gsub[i]; i += 1; } else if (gsub[i] == gpos[j]) { tag = gsub[i]; i += 1; j += 1; } else { tag = gpos[j]; j += 1; } try addTag(a, &out, tag); } return out.items;}
fn addTag(a: Allocator, out: *std.ArrayList(u8), tag: u32) Error!void { var t: [4]u8 = undefined; std.mem.writeInt(u32, &t, tag, .big); for (t) |c| { if (!(std.ascii.isAlphanumeric(c) or c == ' ')) return; } if (out.items.len > 0) try out.append(a, ' '); try out.appendSlice(a, "otlayout:"); try out.appendSlice(a, &t);}
fn be16At(data: []const u8, pos: usize) ?u16 { if (pos + 2 > data.len) return null; return std.mem.readInt(u16, data[pos..][0..2], .big);}
/// `GetScriptTags`: the script tags of a `GSUB` or `GPOS` script list,/// sorted, read from the file as FreeType's stream reads it — so that a/// record running off the end of the file loses the whole list, and one/// pointing past it loses only itself.fn scriptTags(a: Allocator, data: []const u8, t: ?@TypeOf(Face.ftglueTable(&.{}, 0, "GSUB"))) Error![]u32 { const table = (t orelse return &.{}) orelse return &.{}; const base = table.start; const list_off = @as(usize, be16At(data, base + 4) orelse return &.{}) + base; if (list_off > data.len) return &.{}; const count = be16At(data, list_off) orelse return &.{}; var tags: std.ArrayList(u32) = .empty; for (0..count) |i| { const rec = list_off + 2 + 6 * i; if (rec + 6 > data.len) return &.{}; const tag = std.mem.readInt(u32, data[rec..][0..4], .big); const script = @as(usize, be16At(data, rec + 4).?) + list_off; if (script <= data.len) try tags.append(a, tag); } std.mem.sort(u32, tags.items, {}, std.sort.asc(u32)); return tags.items;}
/// The code point ranges the selected charmap's subtable covers, sorted.fn charRanges(a: Allocator, face: *const Face) Error![]const [2]u32 { const m = face.charmap orelse return &.{}; var ranges: std.ArrayList([2]u32) = .empty; switch (m.subtable) { .format0 => try ranges.append(a, .{ 0, 255 }), .format2 => try ranges.append(a, .{ 0, 0xffff }), .format4 => |f| for (f.segments) |seg| try ranges.append(a, .{ seg.start_code, seg.end_code }), .format6 => |f| if (f.glyph_ids.len > 0) try ranges.append(a, .{ f.first_code, f.first_code + @as(u32, @intCast(f.glyph_ids.len)) - 1 }), .format10 => |f| if (f.glyph_ids.len > 0) try ranges.append(a, .{ f.start_char, f.start_char +| @as(u32, @intCast(f.glyph_ids.len)) - 1 }), inline .format8, .format12, .format13 => |f| for (f.groups) |g| { if (g.end_char_code >= g.start_char_code) try ranges.append(a, .{ g.start_char_code, @min(g.end_char_code, 0x10ffff) }); }, .format14 => {}, } std.mem.sort([2]u32, ranges.items, {}, struct { fn lessThan(_: void, x: [2]u32, y: [2]u32) bool { return x[0] < y[0]; } }.lessThan); return ranges.items;}
/// Walks the code points the selected charmap maps to a glyph, in order,/// with their glyphs: what `FT_Get_First_Char` and `FT_Get_Next_Char` walk.const CharIterator = struct { face: *const Face, ranges: []const [2]u32, index: SequentialIndex, range: usize = 0, cp: u32 = 0,
fn init(a: Allocator, face: *const Face) Error!CharIterator { return .{ .face = face, .ranges = try charRanges(a, face), .index = .init(face) }; }
fn next(it: *CharIterator) ?[2]u32 { while (it.range < it.ranges.len) { const r = it.ranges[it.range]; if (it.cp < r[0]) it.cp = r[0]; while (it.cp <= r[1]) { const cp = it.cp; if (it.cp == std.math.maxInt(u32)) { it.range = it.ranges.len; } else it.cp += 1; const gid = it.index.get(cp); if (gid != 0) return .{ cp, gid }; } it.range += 1; } return null; }};
/// `Face.charIndex`, for code points asked about in increasing order, as/// `CharIterator` asks.////// zig-font finds a character in a format 4, 8, 12 or 13 subtable by binary/// search for the first segment whose end is at or past it. When the/// segments are in order of their ends, as a well-formed subtable's are,/// that segment can only move forward as the characters do, so it is/// followed rather than searched for, with the same answer. Any other/// subtable, or one out of order, is looked up as `charIndex` looks it up.const SequentialIndex = struct { face: *const Face, sorted: bool, /// The segment or group the last character was looked up in. seg: usize = 0,
const Group = font.tables.cmap.SequentialMapGroup;
fn init(face: *const Face) SequentialIndex { const m = face.charmap orelse return .{ .face = face, .sorted = false }; const sorted = switch (m.subtable) { .format4 => |f| inOrder(font.tables.cmap.Format4.Segment, f.segments, "end_code"), inline .format8, .format12, .format13 => |f| inOrder(Group, f.groups, "end_char_code"), else => false, }; return .{ .face = face, .sorted = sorted }; }
fn inOrder(comptime T: type, items: []const T, comptime field: []const u8) bool { if (items.len == 0) return false; for (items[0 .. items.len - 1], items[1..]) |x, y| { if (@field(x, field) > @field(y, field)) return false; } return true; }
fn get(s: *SequentialIndex, cp: u32) u32 { if (!s.sorted) return s.face.charIndex(cp); const g: u32 = switch (s.face.charmap.?.subtable) { .format4 => |f| s.format4(f, cp), inline .format8, .format12 => |f| if (s.group(f.groups, cp)) |g| g.start_glyph_id +% (cp - g.start_char_code) else 0, .format13 => |f| if (s.group(f.groups, cp)) |g| g.start_glyph_id else 0, else => unreachable, }; return if (g >= s.face.num_glyphs) 0 else g; }
/// `Format4.lookup`, with the segment followed. fn format4(s: *SequentialIndex, f: font.tables.cmap.Format4, c: u32) u32 { if (c > 0xffff) return 0; while (s.seg < f.segments.len and f.segments[s.seg].end_code < c) s.seg += 1; if (s.seg == f.segments.len) return 0; const seg = f.segments[s.seg]; if (c < seg.start_code) return 0; const delta: u16 = @bitCast(seg.id_delta); if (seg.id_range_offset == 0) return @as(u16, @truncate(c)) +% delta; const index = @as(isize, seg.id_range_offset / 2) + @as(isize, @intCast(c - seg.start_code)) + @as(isize, @intCast(s.seg)) - @as(isize, @intCast(f.segments.len)); if (index < 0 or index >= f.glyph_id_array.len) return 0; const g = f.glyph_id_array[@intCast(index)]; return if (g == 0) 0 else g +% delta; }
/// `findGroup`, with the group followed. fn group(s: *SequentialIndex, groups: []const Group, c: u32) ?Group { while (s.seg < groups.len and groups[s.seg].end_char_code < c) s.seg += 1; if (s.seg == groups.len or c < groups[s.seg].start_char_code) return null; return groups[s.seg]; }};
/// `FcFreeTypeCharSet`. Also sets `glyphs` to what `Shared.glyphs` holds.fn charSet(a: Allocator, face: *const Face, glyphs: *[]const u32) Error!CharSet { var b: CharSet.Builder = .{}; var it: CharIterator = try .init(a, face); var outlines: ?Outlines = null; var order: std.ArrayList(u32) = .empty; // `charIndex` never answers a glyph past the end of the font. var seen: std.DynamicBitSetUnmanaged = try .initEmpty(a, face.num_glyphs); while (it.next()) |c| { if (!seen.isSet(c[1])) { seen.set(c[1]); try order.append(a, c[1]); } // Adobe's CID fonts used to map the ASCII control characters to the // space glyph, so those count only with an outline. if (c[0] <= 0x1f) { if (outlines == null) outlines = .init(a, face); if (!outlines.?.hasContours(c[1])) continue; } try b.add(a, c[0]); } if (face.charmap) |m| if (m.encoding == .ms_symbol) { // A symbol font's U+F000 to U+F0FF also covers U+0000 to U+00FF. var cp: u32 = 0xf000; while (cp < 0xf100) : (cp += 1) { if (b.hasChar(cp)) try b.add(a, cp - 0xf000); } }; glyphs.* = order.items; return b.finish(a);}
/// `FcFreeTypeSpacing`: monospace if the glyphs have one advance width,/// dual if two and one is twice the other, proportional otherwise, judging/// by the first three distinct advances in code point order. `glyphs` is/// `Shared.glyphs`: a glyph mapped again has the advance it had the first/// time, which can change nothing.fn spacing(a: Allocator, face: *const Face, glyphs: []const u32, variations: ?*Face.Variations, instance: u32) i32 { var advances: [3]i32 = .{ 0, 0, 0 }; var num: usize = 0; const metrics: Face.InstanceAdvances = .init(a, face, variations, instance); for (glyphs) |gid| { if (num == 3) break; const adv = metrics.advance(gid); if (adv == 0) continue; var j: usize = 0; while (j < num) : (j += 1) { if (approxEqual(adv, advances[j])) break; } if (j == num) { advances[num] = adv; num += 1; } } if (num <= 1) return object.spacing.mono; if (num == 2 and approxEqual(@min(advances[0], advances[1]) * 2, @max(advances[0], advances[1]))) return object.spacing.dual; return object.spacing.proportional;}
pub fn approxEqual(x: i32, y: i32) bool { return @abs(x - y) * 33 <= @max(@abs(x), @abs(y));}
/// Whether a glyph has an outline with contours, as `FT_Load_Glyph` would/// report it, for the few glyphs that need asking.const Outlines = struct { a: Allocator, face: *const Face, loca: ?font.tables.loca.Loca = null, glyf: ?[]const u8 = null, cff: ?font.tables.cff.Cff = null, cff2: ?font.tables.cff2.Cff2 = null, failed: bool = false,
fn init(a: Allocator, face: *const Face) Outlines { var o: Outlines = .{ .a = a, .face = face }; if (!face.has_outline) { o.failed = true; return o; } if (face.table("glyf")) |glyf| { const head = face.table("head").?; const format: i16 = @bitCast(std.mem.readInt(u16, head[50..52], .big)); const loca_data = face.table("loca") orelse { o.failed = true; return o; }; o.loca = font.tables.loca.Loca.parse(a, loca_data, format, face.num_glyphs) catch { o.failed = true; return o; }; o.glyf = glyf; } else if (face.table("CFF2")) |b| { o.cff2 = font.tables.cff2.Cff2.parse(a, b) catch null; if (o.cff2 == null) o.failed = true; } else if (face.table("CFF ")) |b| { // Only a few glyphs are ever asked about, so none are decoded // until they are. o.cff = font.tables.cff.Cff.parseLazy(a, b) catch null; if (o.cff == null) o.failed = true; } else o.failed = true; return o; }
fn hasContours(o: *Outlines, gid: u32) bool { if (o.failed) return false; if (o.glyf) |glyf| return glyfContours(o.a, glyf, o.loca.?, gid, 0) > 0; if (o.cff) |*c| { if (gid > std.math.maxInt(u16)) return false; const g = c.decodeGlyph(o.a, @intCast(gid)) catch return false; if (g.failure != null) return false; return g.path.contourCount() > 0; } if (o.cff2) |*c| { const path = c.glyphPath(o.a, gid, &.{}) catch return false; return path.contourCount() > 0; } return false; }
fn glyfContours(a: Allocator, glyf: []const u8, loca: font.tables.loca.Loca, gid: u32, depth: usize) usize { if (depth > 8 or gid > std.math.maxInt(u16)) return 0; const r = loca.range(@intCast(gid), glyf.len) catch return 0; if (r.end <= r.start) return 0; const g = font.tables.glyf.parseGlyph(a, glyf[r.start..r.end]) catch return 0; return switch (g) { .simple => |simple| simple.end_pts.len, .composite => |c| blk: { var n: usize = 0; for (c.components) |comp| n += glyfContours(a, glyf, loca, comp.glyph_id, depth + 1); break :blk n; }, else => 0, }; }};
const testing = std.testing;
test "weight from OpenType" { try testing.expectEqual(@as(f64, 80), weightFromOpenType(400)); try testing.expectEqual(@as(f64, 200), weightFromOpenType(700)); try testing.expectEqual(@as(f64, 90), weightFromOpenType(450)); try testing.expectEqual(@as(f64, 215), weightFromOpenType(1200)); try testing.expectEqual(@as(f64, -1), weightFromOpenType(-5));}
test "style constants" { try testing.expectEqual(@as(i32, object.weight.bold), containsConst("Bold Italic", &weight_consts)); try testing.expectEqual(@as(i32, object.weight.extrabold), containsConst("Ultra", &weight_consts)); try testing.expectEqual(@as(i32, object.weight.extrabold), containsConst("Ultra Bold", &weight_consts)); try testing.expectEqual(@as(i32, object.weight.demibold), containsConst("SemiBold", &weight_consts)); try testing.expectEqual(@as(i32, object.width.condensed), containsConst("Bold Condensed", &width_consts)); try testing.expectEqual(@as(i32, object.slant.italic), containsConst("Kursiv", &slant_consts)); try testing.expectEqual(@as(i32, -1), containsConst("Regular", &slant_consts));}
test "following the cmap's segments gives what looking each character up gives" { var segments = [_]font.tables.cmap.Format4.Segment{ .{ .start_code = 0x20, .end_code = 0x7e, .id_delta = -29, .id_range_offset = 0 }, // Overlaps the one before it, which is legal and out of nothing's way. .{ .start_code = 0x40, .end_code = 0x80, .id_delta = 0, .id_range_offset = 6 }, .{ .start_code = 0x4e00, .end_code = 0x4e02, .id_delta = 1, .id_range_offset = 0 }, .{ .start_code = 0xffff, .end_code = 0xffff, .id_delta = 1, .id_range_offset = 0 }, }; var glyph_ids = [_]u16{ 7, 0, 9, 600, 3 }; var records: [0]font.sfnt.TableRecord = .{}; var face: Face = .{ .data = &.{}, .dir = .{ .sfnt_version = 0x00010000, .tables = &records }, .num_glyphs = 500, .charmap = .{ .platform = 3, .encoding_id = 1, .encoding = .unicode, .format = 4, .subtable = .{ .format4 = .{ .language = 0, .segments = &segments, .glyph_id_array = &glyph_ids } }, }, }; for (0..2) |pass| { // The second time round the segments are out of order, and each // character is looked up instead. if (pass == 1) std.mem.swap(font.tables.cmap.Format4.Segment, &segments[0], &segments[2]); var index: SequentialIndex = .init(&face); try testing.expectEqual(pass == 0, index.sorted); var cp: u32 = 0; while (cp <= 0x10000) : (cp += 1) try testing.expectEqual(face.charIndex(cp), index.get(cp)); }}