// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // 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 }, .{ " 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)); } }