@hyperframes/core 0.9.12 → 0.9.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fonts/deterministicFonts.d.ts +2 -0
- package/dist/fonts/deterministicFonts.js +21 -30
- package/dist/fonts/sfntFixture.d.ts +14 -0
- package/dist/fonts/sfntFixture.js +103 -0
- package/dist/fonts/sfntNames.d.ts +14 -0
- package/dist/fonts/sfntNames.js +115 -0
- package/dist/fonts/systemFontLocator.d.ts +5 -2
- package/dist/fonts/systemFontLocator.js +46 -4
- package/dist/generated/runtime-inline.js +1 -1
- package/dist/hyperframe.runtime.iife.js +7 -7
- package/package.json +4 -4
|
@@ -142,6 +142,7 @@ export interface InjectDeterministicFontFacesOptions {
|
|
|
142
142
|
* planning; tests may lower delays and timeouts without replacing timers.
|
|
143
143
|
*/
|
|
144
144
|
fontFetchRetryPolicy?: Partial<FontFetchRetryPolicy>;
|
|
145
|
+
projectFontFamilies?: readonly string[];
|
|
145
146
|
/**
|
|
146
147
|
* When `true` (default for local renders), fonts that aren't resolved by
|
|
147
148
|
* the bundled alias map or Google Fonts are located on the local filesystem,
|
|
@@ -150,5 +151,6 @@ export interface InjectDeterministicFontFacesOptions {
|
|
|
150
151
|
* to contain the same fonts as the authoring machine.
|
|
151
152
|
*/
|
|
152
153
|
allowSystemFontCapture?: boolean;
|
|
154
|
+
systemFontsByInnerName?: boolean;
|
|
153
155
|
}
|
|
154
156
|
export declare function injectDeterministicFontFaces(html: string, options?: InjectDeterministicFontFacesOptions): Promise<string>;
|
|
@@ -2,7 +2,9 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { fontShorthandFamilies } from "@hyperframes/parsers/composition";
|
|
5
6
|
import { FONT_ALIAS_MAP, resolveAliasDisplayName } from "./aliases.js";
|
|
7
|
+
import { extractSfntFace } from "./sfntNames.js";
|
|
6
8
|
import { locateSystemFontVariants, SYSTEM_FONT_SIZE_LIMIT } from "./systemFontLocator.js";
|
|
7
9
|
import { parseHTML } from "linkedom";
|
|
8
10
|
import postcss from "postcss";
|
|
@@ -214,30 +216,6 @@ function collectCssCustomProperties(css, customProperties) {
|
|
|
214
216
|
customProperties.set(decl.prop, decl.value);
|
|
215
217
|
});
|
|
216
218
|
}
|
|
217
|
-
const FONT_SIZE_KEYWORD = /^(?:xx-small|x-small|small|medium|large|x-large|xx-large|xxx-large|smaller|larger)$/i;
|
|
218
|
-
const FONT_SIZE_TOKEN = /^(?:\d*\.?\d+[a-z%]+|(?:calc|min|max|clamp|var)\(.*\))(?:\/\S+)?$/i;
|
|
219
|
-
function maskParens(value) {
|
|
220
|
-
let depth = 0;
|
|
221
|
-
return value.replace(/[\s\S]/g, (char) => {
|
|
222
|
-
if (char === "(")
|
|
223
|
-
depth += 1;
|
|
224
|
-
const inside = depth > 0;
|
|
225
|
-
if (char === ")")
|
|
226
|
-
depth -= 1;
|
|
227
|
-
return inside ? "_" : char;
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
function fontShorthandFamilies(value) {
|
|
231
|
-
for (const { 0: masked, index } of maskParens(value).matchAll(/\S+/g)) {
|
|
232
|
-
const end = index + masked.length;
|
|
233
|
-
const token = value.slice(index, end);
|
|
234
|
-
const size = token.split("/")[0] ?? "";
|
|
235
|
-
if (FONT_SIZE_KEYWORD.test(size) || FONT_SIZE_TOKEN.test(token)) {
|
|
236
|
-
return value.slice(end).trim();
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
return "";
|
|
240
|
-
}
|
|
241
219
|
function* iterateCssRootFontFamilyDeclarations(root) {
|
|
242
220
|
const declarations = [];
|
|
243
221
|
root.walkDecls((decl) => {
|
|
@@ -656,16 +634,17 @@ async function googleFamilyFaceRules(lookupFamily, emitFamily, optional, options
|
|
|
656
634
|
return googleFaces.map((face) => buildFontFaceRule(emitFamily, face.dataUri, face.weight, face.style, face.unicodeRange));
|
|
657
635
|
}
|
|
658
636
|
// Path 3: locate font on the local filesystem, compress, and embed.
|
|
659
|
-
async function systemFamilyFaceRules(lookupFamily, emitFamily, log) {
|
|
660
|
-
const variants = locateSystemFontVariants(lookupFamily);
|
|
637
|
+
async function systemFamilyFaceRules(lookupFamily, emitFamily, log, byInnerName) {
|
|
638
|
+
const variants = locateSystemFontVariants(lookupFamily, { byInnerName });
|
|
661
639
|
if (variants.length === 0)
|
|
662
640
|
return null;
|
|
663
641
|
const rules = [];
|
|
664
642
|
let totalBytes = 0;
|
|
665
643
|
for (const variant of variants) {
|
|
666
|
-
const
|
|
644
|
+
const single = variant.face === undefined ? null : extractSfntFace(variant.path, variant.face);
|
|
645
|
+
const fontBuffer = single?.bytes ?? readFileSync(variant.path);
|
|
667
646
|
totalBytes += fontBuffer.length;
|
|
668
|
-
const dataUri = await fontToDataUri(fontBuffer, variant.format);
|
|
647
|
+
const dataUri = await fontToDataUri(fontBuffer, single?.format ?? variant.format);
|
|
669
648
|
rules.push(buildFontFaceRule(emitFamily, dataUri, variant.weight, variant.style));
|
|
670
649
|
}
|
|
671
650
|
if (totalBytes > SYSTEM_FONT_SIZE_LIMIT) {
|
|
@@ -685,7 +664,7 @@ async function resolveFamilyFaceRules(normalizedFamily, lookupFamily, emitFamily
|
|
|
685
664
|
}
|
|
686
665
|
return ((await googleFamilyFaceRules(lookupFamily, emitFamily, optional, options, fontText)) ??
|
|
687
666
|
(options.allowSystemFontCapture && !pageNamedThisFile(lookupFamily, options)
|
|
688
|
-
? await systemFamilyFaceRules(lookupFamily, emitFamily, options.log)
|
|
667
|
+
? await systemFamilyFaceRules(lookupFamily, emitFamily, options.log, options.systemFontsByInnerName)
|
|
689
668
|
: null));
|
|
690
669
|
}
|
|
691
670
|
// css2 repeats `family=Name:axes`; the legacy css API packs `family=Name:400|Other`.
|
|
@@ -817,6 +796,14 @@ function findDeclaredFamilyAlias(authoredFamily, declaredFamilies) {
|
|
|
817
796
|
}
|
|
818
797
|
return match;
|
|
819
798
|
}
|
|
799
|
+
// Design exports name a family by class, `font-<Family>-<weight>`; serve all <Family> faces under that name.
|
|
800
|
+
const WEIGHTED_CLASS_FAMILY = /^font-(.+)-[1-9]00$/i;
|
|
801
|
+
async function resolveWeightedClassFamily(authoredFamily, optional, options, fontText) {
|
|
802
|
+
const family = WEIGHTED_CLASS_FAMILY.exec(authoredFamily)?.[1];
|
|
803
|
+
if (!family)
|
|
804
|
+
return null;
|
|
805
|
+
return resolveFamilyFaceRules(normalizeFamilyName(family), family, authoredFamily, optional, options, fontText);
|
|
806
|
+
}
|
|
820
807
|
// Emits under the authored name instead of rewriting usages, as FONT_ALIASES does,
|
|
821
808
|
// so script-assigned and var()-indirected usages match too.
|
|
822
809
|
async function resolveDeclaredFamilyAlias(authoredFamily, declaredFamilies, optional, options, fontText) {
|
|
@@ -853,7 +840,8 @@ async function buildFontFaceCss(requestedFamilies, options, declaredFamilies, fo
|
|
|
853
840
|
options.diag.beginFamily(!optional);
|
|
854
841
|
const familyRules = (await resolveFamilyFaceRules(normalizedFamily, originalCaseFamily, originalCaseFamily, optional, options, fontText)) ??
|
|
855
842
|
// Last, so a family that already resolves keeps exactly the faces it had.
|
|
856
|
-
(await resolveDeclaredFamilyAlias(originalCaseFamily, declaredFamilies(), optional, options, fontText))
|
|
843
|
+
(await resolveDeclaredFamilyAlias(originalCaseFamily, declaredFamilies(), optional, options, fontText)) ??
|
|
844
|
+
(await resolveWeightedClassFamily(originalCaseFamily, optional, options, fontText));
|
|
857
845
|
options.diag.endFamily(Boolean(familyRules));
|
|
858
846
|
if (familyRules) {
|
|
859
847
|
rules.push(...familyRules);
|
|
@@ -1534,6 +1522,7 @@ export async function injectDeterministicFontFaces(html, options = {}) {
|
|
|
1534
1522
|
failClosedFontFetch,
|
|
1535
1523
|
fetchImpl,
|
|
1536
1524
|
allowSystemFontCapture,
|
|
1525
|
+
systemFontsByInnerName: options.systemFontsByInnerName === true,
|
|
1537
1526
|
abortSignal: options.abortSignal,
|
|
1538
1527
|
retryPolicy,
|
|
1539
1528
|
retryDeadlineMs: Date.now() + retryPolicy.maxElapsedMs,
|
|
@@ -1541,6 +1530,8 @@ export async function injectDeterministicFontFaces(html, options = {}) {
|
|
|
1541
1530
|
diag,
|
|
1542
1531
|
};
|
|
1543
1532
|
const existingFaces = extractExistingFontFaces(html);
|
|
1533
|
+
for (const family of options.projectFontFamilies ?? [])
|
|
1534
|
+
existingFaces.add(normalizeFamilyName(family));
|
|
1544
1535
|
const requestedFamilies = extractRequestedFontFamilies(html);
|
|
1545
1536
|
const pendingFamilies = new Map();
|
|
1546
1537
|
for (const [normalizedFamily, requested] of requestedFamilies) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Points the locator's per-user font folder, on every platform, at an empty folder and returns it. */
|
|
2
|
+
export declare function fixtureFontDir(): string;
|
|
3
|
+
interface FaceSpec {
|
|
4
|
+
families: string[];
|
|
5
|
+
weight?: number;
|
|
6
|
+
italic?: boolean;
|
|
7
|
+
flavor?: "ttf" | "otf";
|
|
8
|
+
}
|
|
9
|
+
export declare function makeSfnt(spec: FaceSpec): Buffer;
|
|
10
|
+
export declare function makeTtc(specs: FaceSpec[]): {
|
|
11
|
+
file: Buffer;
|
|
12
|
+
offsets: number[];
|
|
13
|
+
};
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Builds small font files in memory so tests need no binary fixtures.
|
|
2
|
+
import { mkdirSync, mkdtempSync } from "node:fs";
|
|
3
|
+
import { afterAll } from "vitest";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
/** Points the locator's per-user font folder, on every platform, at an empty folder and returns it. */
|
|
7
|
+
export function fixtureFontDir() {
|
|
8
|
+
const home = mkdtempSync(join(tmpdir(), "hf-home-"));
|
|
9
|
+
const set = {
|
|
10
|
+
HOME: home,
|
|
11
|
+
USERPROFILE: home,
|
|
12
|
+
WINDIR: join(home, "Win"),
|
|
13
|
+
LOCALAPPDATA: join(home, "Local"),
|
|
14
|
+
};
|
|
15
|
+
const before = Object.fromEntries(Object.keys(set).map((k) => [k, process.env[k]]));
|
|
16
|
+
Object.assign(process.env, set);
|
|
17
|
+
afterAll(() => {
|
|
18
|
+
for (const [k, v] of Object.entries(before)) {
|
|
19
|
+
if (v === undefined)
|
|
20
|
+
delete process.env[k];
|
|
21
|
+
else
|
|
22
|
+
process.env[k] = v;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
const dir = process.platform === "win32"
|
|
26
|
+
? join(home, "Local", "Microsoft", "Windows", "Fonts")
|
|
27
|
+
: process.platform === "darwin"
|
|
28
|
+
? join(home, "Library", "Fonts")
|
|
29
|
+
: join(home, ".fonts");
|
|
30
|
+
mkdirSync(dir, { recursive: true });
|
|
31
|
+
return dir;
|
|
32
|
+
}
|
|
33
|
+
function nameTable(families) {
|
|
34
|
+
const strings = families.map((f) => Buffer.from(f, "utf16le").swap16());
|
|
35
|
+
const header = Buffer.alloc(6 + families.length * 12);
|
|
36
|
+
header.writeUInt16BE(0, 0);
|
|
37
|
+
header.writeUInt16BE(families.length, 2);
|
|
38
|
+
header.writeUInt16BE(header.length, 4);
|
|
39
|
+
let at = 0;
|
|
40
|
+
strings.forEach((s, i) => {
|
|
41
|
+
const r = 6 + i * 12;
|
|
42
|
+
header.writeUInt16BE(3, r);
|
|
43
|
+
header.writeUInt16BE(1, r + 2);
|
|
44
|
+
header.writeUInt16BE(0x409, r + 4);
|
|
45
|
+
header.writeUInt16BE(i === 0 ? 1 : 16, r + 6);
|
|
46
|
+
header.writeUInt16BE(s.length, r + 8);
|
|
47
|
+
header.writeUInt16BE(at, r + 10);
|
|
48
|
+
at += s.length;
|
|
49
|
+
});
|
|
50
|
+
return Buffer.concat([header, ...strings]);
|
|
51
|
+
}
|
|
52
|
+
function os2Table(weight, italic) {
|
|
53
|
+
const t = Buffer.alloc(78);
|
|
54
|
+
t.writeUInt16BE(weight, 4);
|
|
55
|
+
t.writeUInt16BE(italic ? 1 : 0, 62);
|
|
56
|
+
return t;
|
|
57
|
+
}
|
|
58
|
+
/** One face: its tables laid out from `base`, so a collection can place it anywhere in the file. */
|
|
59
|
+
function face(spec, base) {
|
|
60
|
+
const tables = [
|
|
61
|
+
["OS/2", os2Table(spec.weight ?? 400, spec.italic ?? false)],
|
|
62
|
+
["glyf", Buffer.from("glyph-data-" + spec.families[0])],
|
|
63
|
+
["name", nameTable(spec.families)],
|
|
64
|
+
];
|
|
65
|
+
const head = Buffer.alloc(12 + tables.length * 16);
|
|
66
|
+
head.write(spec.flavor === "otf" ? "OTTO" : "\0\x01\0\0", 0, "latin1");
|
|
67
|
+
head.writeUInt16BE(tables.length, 4);
|
|
68
|
+
let at = base + head.length;
|
|
69
|
+
const body = [];
|
|
70
|
+
tables.forEach(([tag, data], i) => {
|
|
71
|
+
const r = 12 + i * 16;
|
|
72
|
+
head.write(tag, r, "latin1");
|
|
73
|
+
head.writeUInt32BE(0, r + 4);
|
|
74
|
+
head.writeUInt32BE(at, r + 8);
|
|
75
|
+
head.writeUInt32BE(data.length, r + 12);
|
|
76
|
+
const padded = Buffer.alloc((data.length + 3) & ~3);
|
|
77
|
+
data.copy(padded);
|
|
78
|
+
body.push(padded);
|
|
79
|
+
at += padded.length;
|
|
80
|
+
});
|
|
81
|
+
return { head, body: Buffer.concat(body) };
|
|
82
|
+
}
|
|
83
|
+
export function makeSfnt(spec) {
|
|
84
|
+
const { head, body } = face(spec, 0);
|
|
85
|
+
return Buffer.concat([head, body]);
|
|
86
|
+
}
|
|
87
|
+
export function makeTtc(specs) {
|
|
88
|
+
let at = 12 + specs.length * 4;
|
|
89
|
+
const offsets = [];
|
|
90
|
+
const parts = [];
|
|
91
|
+
for (const spec of specs) {
|
|
92
|
+
const { head, body } = face(spec, at);
|
|
93
|
+
offsets.push(at);
|
|
94
|
+
parts.push(head, body);
|
|
95
|
+
at += head.length + body.length;
|
|
96
|
+
}
|
|
97
|
+
const header = Buffer.alloc(12 + specs.length * 4);
|
|
98
|
+
header.write("ttcf", 0, "latin1");
|
|
99
|
+
header.writeUInt32BE(0x00010000, 4);
|
|
100
|
+
header.writeUInt32BE(specs.length, 8);
|
|
101
|
+
offsets.forEach((o, i) => header.writeUInt32BE(o, 12 + i * 4));
|
|
102
|
+
return { file: Buffer.concat([header, ...parts]), offsets };
|
|
103
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** One face of a font file: where it starts and the family names written inside it. */
|
|
2
|
+
export interface SfntFace {
|
|
3
|
+
offset: number;
|
|
4
|
+
families: string[];
|
|
5
|
+
weight: string;
|
|
6
|
+
style: "normal" | "italic";
|
|
7
|
+
}
|
|
8
|
+
/** Every face of a font file, with the family names inside it; a collection (.ttc) yields one per member. */
|
|
9
|
+
export declare function readSfntFaces(path: string): SfntFace[];
|
|
10
|
+
/** One face of a font file as a standalone font, which a browser can load; a collection cannot be loaded whole. */
|
|
11
|
+
export declare function extractSfntFace(path: string, faceOffset: number): {
|
|
12
|
+
bytes: Buffer;
|
|
13
|
+
format: "ttf" | "otf";
|
|
14
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { closeSync, openSync, readFileSync, readSync } from "node:fs";
|
|
2
|
+
const NAME_FAMILY_IDS = new Set([1, 16, 21]);
|
|
3
|
+
const MAX_FACES = 64;
|
|
4
|
+
function readAt(fd, position, length) {
|
|
5
|
+
const buf = Buffer.alloc(length);
|
|
6
|
+
const read = readSync(fd, buf, 0, length, position);
|
|
7
|
+
return read === length ? buf : buf.subarray(0, read);
|
|
8
|
+
}
|
|
9
|
+
function tableDirectory(fd, faceOffset) {
|
|
10
|
+
const header = readAt(fd, faceOffset, 12);
|
|
11
|
+
const count = header.length === 12 ? header.readUInt16BE(4) : 0;
|
|
12
|
+
const dir = readAt(fd, faceOffset + 12, count * 16);
|
|
13
|
+
const tables = new Map();
|
|
14
|
+
for (let i = 0; i + 16 <= dir.length; i += 16) {
|
|
15
|
+
tables.set(dir.toString("latin1", i, i + 4), {
|
|
16
|
+
offset: dir.readUInt32BE(i + 8),
|
|
17
|
+
length: dir.readUInt32BE(i + 12),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return tables;
|
|
21
|
+
}
|
|
22
|
+
function familiesIn(name) {
|
|
23
|
+
if (name.length < 6)
|
|
24
|
+
return [];
|
|
25
|
+
const count = name.readUInt16BE(2);
|
|
26
|
+
const strings = name.readUInt16BE(4);
|
|
27
|
+
const found = new Set();
|
|
28
|
+
for (let i = 0; i < count && 6 + i * 12 + 12 <= name.length; i++) {
|
|
29
|
+
const at = 6 + i * 12;
|
|
30
|
+
const platform = name.readUInt16BE(at);
|
|
31
|
+
const nameId = name.readUInt16BE(at + 6);
|
|
32
|
+
if (!NAME_FAMILY_IDS.has(nameId))
|
|
33
|
+
continue;
|
|
34
|
+
const length = name.readUInt16BE(at + 8);
|
|
35
|
+
const raw = name.subarray(strings + name.readUInt16BE(at + 10), strings + name.readUInt16BE(at + 10) + length);
|
|
36
|
+
// Windows (3) and Unicode (0) names are UTF-16BE; Macintosh (1) names are single-byte.
|
|
37
|
+
const text = platform === 1 ? raw.toString("latin1") : Buffer.from(raw).swap16().toString("utf16le");
|
|
38
|
+
if (text.trim())
|
|
39
|
+
found.add(text.trim());
|
|
40
|
+
}
|
|
41
|
+
return [...found];
|
|
42
|
+
}
|
|
43
|
+
function readFace(fd, offset) {
|
|
44
|
+
const tables = tableDirectory(fd, offset);
|
|
45
|
+
const nameTable = tables.get("name");
|
|
46
|
+
if (!nameTable)
|
|
47
|
+
return null;
|
|
48
|
+
const families = familiesIn(readAt(fd, nameTable.offset, Math.min(nameTable.length, 1 << 20)));
|
|
49
|
+
const os2Table = tables.get("OS/2");
|
|
50
|
+
const os2 = os2Table
|
|
51
|
+
? readAt(fd, os2Table.offset, Math.min(os2Table.length, 64))
|
|
52
|
+
: Buffer.alloc(0);
|
|
53
|
+
const weightClass = os2.length >= 6 ? os2.readUInt16BE(4) : 400;
|
|
54
|
+
const italic = os2.length >= 64 && (os2.readUInt16BE(62) & 1) === 1;
|
|
55
|
+
return {
|
|
56
|
+
offset,
|
|
57
|
+
families,
|
|
58
|
+
weight: String(weightClass >= 100 && weightClass <= 900 ? weightClass : 400),
|
|
59
|
+
style: italic ? "italic" : "normal",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Every face of a font file, with the family names inside it; a collection (.ttc) yields one per member. */
|
|
63
|
+
export function readSfntFaces(path) {
|
|
64
|
+
let fd;
|
|
65
|
+
try {
|
|
66
|
+
fd = openSync(path, "r");
|
|
67
|
+
const head = readAt(fd, 0, 12);
|
|
68
|
+
if (head.length < 12)
|
|
69
|
+
return [];
|
|
70
|
+
const collection = head.toString("latin1", 0, 4) === "ttcf";
|
|
71
|
+
const table = collection ? readAt(fd, 12, Math.min(head.readUInt32BE(8), MAX_FACES) * 4) : head;
|
|
72
|
+
const offsets = collection
|
|
73
|
+
? Array.from({ length: Math.floor(table.length / 4) }, (_, i) => table.readUInt32BE(i * 4))
|
|
74
|
+
: [0];
|
|
75
|
+
return offsets.map((offset) => readFace(fd, offset)).filter((f) => f !== null);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
if (fd !== undefined)
|
|
82
|
+
closeSync(fd);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** One face of a font file as a standalone font, which a browser can load; a collection cannot be loaded whole. */
|
|
86
|
+
export function extractSfntFace(path, faceOffset) {
|
|
87
|
+
const file = readFileSync(path);
|
|
88
|
+
const count = file.readUInt16BE(faceOffset + 4);
|
|
89
|
+
const records = Array.from({ length: count }, (_, i) => {
|
|
90
|
+
const at = faceOffset + 12 + i * 16;
|
|
91
|
+
return {
|
|
92
|
+
tag: file.subarray(at, at + 4),
|
|
93
|
+
checksum: file.readUInt32BE(at + 4),
|
|
94
|
+
data: file.subarray(file.readUInt32BE(at + 8), file.readUInt32BE(at + 8) + file.readUInt32BE(at + 12)),
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
const dirEnd = 12 + count * 16;
|
|
98
|
+
const parts = [Buffer.alloc(dirEnd)];
|
|
99
|
+
let next = dirEnd;
|
|
100
|
+
const out = parts[0];
|
|
101
|
+
file.copy(out, 0, faceOffset, faceOffset + 12);
|
|
102
|
+
records.forEach((record, i) => {
|
|
103
|
+
const at = 12 + i * 16;
|
|
104
|
+
record.tag.copy(out, at);
|
|
105
|
+
out.writeUInt32BE(record.checksum, at + 4);
|
|
106
|
+
out.writeUInt32BE(next, at + 8);
|
|
107
|
+
out.writeUInt32BE(record.data.length, at + 12);
|
|
108
|
+
const padded = Buffer.alloc((record.data.length + 3) & ~3);
|
|
109
|
+
record.data.copy(padded);
|
|
110
|
+
parts.push(padded);
|
|
111
|
+
next += padded.length;
|
|
112
|
+
});
|
|
113
|
+
const bytes = Buffer.concat(parts);
|
|
114
|
+
return { bytes, format: bytes.toString("latin1", 0, 4) === "OTTO" ? "otf" : "ttf" };
|
|
115
|
+
}
|
|
@@ -17,11 +17,14 @@ export interface FontFileEntry {
|
|
|
17
17
|
* path, filename, and derived family name. Shared by the font listing route
|
|
18
18
|
* and the per-family locator to avoid duplicating the directory scan loop.
|
|
19
19
|
*/
|
|
20
|
-
export declare function collectFontFileEntries(dir: string, depth?: number): FontFileEntry[];
|
|
20
|
+
export declare function collectFontFileEntries(dir: string, depth?: number, maxDepth?: number): FontFileEntry[];
|
|
21
21
|
export declare function locateSystemFont(family: string): LocatedFont | null;
|
|
22
22
|
export interface LocatedFontVariant extends LocatedFont {
|
|
23
23
|
weight: string;
|
|
24
24
|
style: "normal" | "italic";
|
|
25
|
+
face?: number;
|
|
25
26
|
}
|
|
26
|
-
export declare function locateSystemFontVariants(family: string
|
|
27
|
+
export declare function locateSystemFontVariants(family: string, options?: {
|
|
28
|
+
byInnerName?: boolean;
|
|
29
|
+
}): LocatedFontVariant[];
|
|
27
30
|
export declare function clearSystemFontCache(): void;
|
|
@@ -2,6 +2,7 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, lstatSync, readdirSync, realpathSync } from "node:fs";
|
|
3
3
|
import { homedir, platform } from "node:os";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
|
+
import { readSfntFaces } from "./sfntNames.js";
|
|
5
6
|
export const SYSTEM_FONT_SIZE_LIMIT = 5 * 1024 * 1024;
|
|
6
7
|
const FC_MATCH_TIMEOUT_MS = 3000;
|
|
7
8
|
export const FONT_EXT_RE = /\.(otf|ttf|ttc|woff2?)$/i;
|
|
@@ -124,6 +125,7 @@ export function fontDirectories() {
|
|
|
124
125
|
"/Library/Fonts",
|
|
125
126
|
"/System/Library/Fonts",
|
|
126
127
|
"/System/Library/Fonts/Supplemental",
|
|
128
|
+
"/System/Library/AssetsV2",
|
|
127
129
|
];
|
|
128
130
|
}
|
|
129
131
|
if (platform() === "win32") {
|
|
@@ -144,15 +146,15 @@ export function fontDirectories() {
|
|
|
144
146
|
* path, filename, and derived family name. Shared by the font listing route
|
|
145
147
|
* and the per-family locator to avoid duplicating the directory scan loop.
|
|
146
148
|
*/
|
|
147
|
-
export function collectFontFileEntries(dir, depth = 0) {
|
|
148
|
-
if (!existsSync(dir) || depth >
|
|
149
|
+
export function collectFontFileEntries(dir, depth = 0, maxDepth = 2) {
|
|
150
|
+
if (!existsSync(dir) || depth > maxDepth)
|
|
149
151
|
return [];
|
|
150
152
|
const entries = [];
|
|
151
153
|
try {
|
|
152
154
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
153
155
|
const fullPath = join(dir, entry.name);
|
|
154
156
|
if (entry.isDirectory()) {
|
|
155
|
-
entries.push(...collectFontFileEntries(fullPath, depth + 1));
|
|
157
|
+
entries.push(...collectFontFileEntries(fullPath, depth + 1, maxDepth));
|
|
156
158
|
continue;
|
|
157
159
|
}
|
|
158
160
|
if (!FONT_EXT_RE.test(entry.name))
|
|
@@ -262,7 +264,7 @@ function inferWeightAndStyle(fileName) {
|
|
|
262
264
|
}
|
|
263
265
|
return { weight: "400", style };
|
|
264
266
|
}
|
|
265
|
-
export function locateSystemFontVariants(family) {
|
|
267
|
+
export function locateSystemFontVariants(family, options = {}) {
|
|
266
268
|
const normalized = normalizeName(family);
|
|
267
269
|
if (!normalized)
|
|
268
270
|
return [];
|
|
@@ -275,8 +277,47 @@ export function locateSystemFontVariants(family) {
|
|
|
275
277
|
const { weight, style } = inferWeightAndStyle(c.path);
|
|
276
278
|
variants.push({ path: c.path, format: c.format, weight, style });
|
|
277
279
|
}
|
|
280
|
+
if (variants.length === 0 && options.byInnerName) {
|
|
281
|
+
return regularAndBold(dedupeVariants(variantsByInnerName(normalized)));
|
|
282
|
+
}
|
|
278
283
|
return dedupeVariants(variants);
|
|
279
284
|
}
|
|
285
|
+
const INNER_NAME_MAX_DEPTH = 4;
|
|
286
|
+
let innerNameIndex = null;
|
|
287
|
+
function innerNameVariants(entry) {
|
|
288
|
+
const ext = entry.fileName.match(FONT_EXT_RE)?.[1]?.toLowerCase() ?? "";
|
|
289
|
+
if (ext !== "ttf" && ext !== "otf" && ext !== "ttc")
|
|
290
|
+
return [];
|
|
291
|
+
const collection = ext === "ttc";
|
|
292
|
+
return readSfntFaces(entry.path).flatMap((face) => face.families.map((family) => [
|
|
293
|
+
normalizeName(family),
|
|
294
|
+
{
|
|
295
|
+
path: entry.path,
|
|
296
|
+
format: collection ? "ttc" : extensionToFormat(ext),
|
|
297
|
+
weight: face.weight,
|
|
298
|
+
style: face.style,
|
|
299
|
+
...(collection && { face: face.offset }),
|
|
300
|
+
},
|
|
301
|
+
]));
|
|
302
|
+
}
|
|
303
|
+
function buildInnerNameIndex() {
|
|
304
|
+
const index = new Map();
|
|
305
|
+
const entries = fontDirectories().flatMap((dir) => collectFontFileEntries(dir, 0, INNER_NAME_MAX_DEPTH));
|
|
306
|
+
for (const [family, variant] of entries.flatMap(innerNameVariants)) {
|
|
307
|
+
index.set(family, [...(index.get(family) ?? []), variant]);
|
|
308
|
+
}
|
|
309
|
+
return index;
|
|
310
|
+
}
|
|
311
|
+
function regularAndBold(variants) {
|
|
312
|
+
const nearest = (target) => variants.reduce((best, v) => !best || Math.abs(Number(v.weight) - target) < Math.abs(Number(best.weight) - target)
|
|
313
|
+
? v
|
|
314
|
+
: best, undefined);
|
|
315
|
+
return [...new Set([nearest(400), nearest(700)])].filter((v) => !!v);
|
|
316
|
+
}
|
|
317
|
+
function variantsByInnerName(normalized) {
|
|
318
|
+
innerNameIndex ??= buildInnerNameIndex();
|
|
319
|
+
return innerNameIndex.get(normalized) ?? [];
|
|
320
|
+
}
|
|
280
321
|
function dedupeVariants(variants) {
|
|
281
322
|
const seen = new Map();
|
|
282
323
|
for (const v of variants) {
|
|
@@ -288,5 +329,6 @@ function dedupeVariants(variants) {
|
|
|
288
329
|
}
|
|
289
330
|
export function clearSystemFontCache() {
|
|
290
331
|
cache.clear();
|
|
332
|
+
innerNameIndex = null;
|
|
291
333
|
allowedDirsCache = null;
|
|
292
334
|
}
|