@json-to-office/shared 0.8.0 → 0.9.0
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/chunk-CP2I5NPP.js +40 -0
- package/dist/chunk-CP2I5NPP.js.map +1 -0
- package/dist/fonts/node.d.ts +86 -0
- package/dist/fonts/node.js +239 -0
- package/dist/fonts/node.js.map +1 -0
- package/dist/index.d.ts +438 -1
- package/dist/index.js +1493 -7
- package/dist/index.js.map +1 -1
- package/dist/types-CL0Hbw6x.d.ts +215 -0
- package/package.json +10 -3
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// src/fonts/sources/format.ts
|
|
2
|
+
function detectFontFormat(buf) {
|
|
3
|
+
if (buf.length < 4) return "unknown";
|
|
4
|
+
const b0 = buf[0], b1 = buf[1], b2 = buf[2], b3 = buf[3];
|
|
5
|
+
if (b0 === 0 && b1 === 1 && b2 === 0 && b3 === 0 || b0 === 116 && b1 === 114 && b2 === 117 && b3 === 101 || b0 === 116 && b1 === 121 && b2 === 112 && b3 === 49) {
|
|
6
|
+
return "ttf";
|
|
7
|
+
}
|
|
8
|
+
if (b0 === 79 && b1 === 84 && b2 === 84 && b3 === 79) return "otf";
|
|
9
|
+
if (b0 === 119 && b1 === 79 && b2 === 70 && b3 === 70) return "woff";
|
|
10
|
+
if (b0 === 119 && b1 === 79 && b2 === 70 && b3 === 50) return "woff2";
|
|
11
|
+
if (buf.length >= 36 && buf[34] === 76 && buf[35] === 80) return "eot";
|
|
12
|
+
if (b0 === 128 && b1 === 1) return "pfb";
|
|
13
|
+
if (buf.length >= 14 && buf.slice(0, 14).toString("ascii") === "%!PS-AdobeFont") {
|
|
14
|
+
return "pfb";
|
|
15
|
+
}
|
|
16
|
+
return "unknown";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/fonts/sources/url-allowlist.ts
|
|
20
|
+
var FONT_URL_ALLOWLIST = [
|
|
21
|
+
"fonts.gstatic.com",
|
|
22
|
+
"fonts.googleapis.com",
|
|
23
|
+
"cdn.jsdelivr.net"
|
|
24
|
+
];
|
|
25
|
+
function isAllowedFontUrl(url) {
|
|
26
|
+
let parsed;
|
|
27
|
+
try {
|
|
28
|
+
parsed = new URL(url);
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (parsed.protocol !== "https:") return false;
|
|
33
|
+
return FONT_URL_ALLOWLIST.includes(parsed.hostname.toLowerCase());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
detectFontFormat,
|
|
38
|
+
isAllowedFontUrl
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=chunk-CP2I5NPP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/fonts/sources/format.ts","../src/fonts/sources/url-allowlist.ts"],"sourcesContent":["/**\n * Font format detection from magic bytes.\n * Source: OpenType spec + WOFF1/WOFF2 W3C specs.\n */\n\nimport type { ResolvedFontSource } from '../types';\n\nexport function detectFontFormat(buf: Buffer): ResolvedFontSource['format'] {\n if (buf.length < 4) return 'unknown';\n\n const b0 = buf[0],\n b1 = buf[1],\n b2 = buf[2],\n b3 = buf[3];\n\n // TTF: 0x00010000 (SFNT) or 'true' (0x74727565) or 'typ1' (0x74797031)\n if (\n (b0 === 0x00 && b1 === 0x01 && b2 === 0x00 && b3 === 0x00) ||\n (b0 === 0x74 && b1 === 0x72 && b2 === 0x75 && b3 === 0x65) ||\n (b0 === 0x74 && b1 === 0x79 && b2 === 0x70 && b3 === 0x31)\n ) {\n return 'ttf';\n }\n // OTF: 'OTTO'\n if (b0 === 0x4f && b1 === 0x54 && b2 === 0x54 && b3 === 0x4f) return 'otf';\n // WOFF: 'wOFF'\n if (b0 === 0x77 && b1 === 0x4f && b2 === 0x46 && b3 === 0x46) return 'woff';\n // WOFF2: 'wOF2'\n if (b0 === 0x77 && b1 === 0x4f && b2 === 0x46 && b3 === 0x32) return 'woff2';\n // EOT: version bytes at offset 8-11 — rougher signature\n if (buf.length >= 36 && buf[34] === 0x4c && buf[35] === 0x50) return 'eot';\n // PostScript Type 1 (.pfb) — binary container marker byte 0x80 followed by\n // segment type 0x01 (ASCII). Also match the text-form ASCII header\n // \"%!PS-AdobeFont\". Note: .pfm (metric files) have no reliable magic and\n // stay in 'unknown' — same treatment (rejection at the loader).\n if (b0 === 0x80 && b1 === 0x01) return 'pfb';\n if (\n buf.length >= 14 &&\n buf.slice(0, 14).toString('ascii') === '%!PS-AdobeFont'\n ) {\n return 'pfb';\n }\n\n return 'unknown';\n}\n\n/**\n * Formats we detect but cannot legally embed in an OOXML document:\n * WOFF/WOFF2 are web-only containers; PostScript (.pfb) is explicitly\n * disallowed by Microsoft's embedding guidance.\n */\nexport const UNEMBEDDABLE_FORMATS = new Set<ResolvedFontSource['format']>([\n 'woff',\n 'woff2',\n 'pfb',\n]);\n","/**\n * Hostname allowlist for font fetchers.\n *\n * `url-fetcher` and `variable-fetcher` can be handed arbitrary URLs via\n * `FontRegistryEntry.sources`, which may originate from document JSON. Without\n * a guard, a malicious doc could point fetchers at internal hosts (SSRF), the\n * filesystem (`file://`), or the IMDS endpoint. Limit downloads to the hosts\n * our catalog + UPSTREAM_OVERRIDES actually target.\n *\n * Keep the list small and HTTPS-only. Expansions should be deliberate code\n * reviews, not config-driven — the cost of a new domain is the code change.\n */\n\nexport const FONT_URL_ALLOWLIST: readonly string[] = [\n 'fonts.gstatic.com',\n 'fonts.googleapis.com',\n 'cdn.jsdelivr.net',\n];\n\nexport function isAllowedFontUrl(url: string): boolean {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return false;\n }\n if (parsed.protocol !== 'https:') return false;\n return FONT_URL_ALLOWLIST.includes(parsed.hostname.toLowerCase());\n}\n"],"mappings":";AAOO,SAAS,iBAAiB,KAA2C;AAC1E,MAAI,IAAI,SAAS,EAAG,QAAO;AAE3B,QAAM,KAAK,IAAI,CAAC,GACd,KAAK,IAAI,CAAC,GACV,KAAK,IAAI,CAAC,GACV,KAAK,IAAI,CAAC;AAGZ,MACG,OAAO,KAAQ,OAAO,KAAQ,OAAO,KAAQ,OAAO,KACpD,OAAO,OAAQ,OAAO,OAAQ,OAAO,OAAQ,OAAO,OACpD,OAAO,OAAQ,OAAO,OAAQ,OAAO,OAAQ,OAAO,IACrD;AACA,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,MAAQ,OAAO,MAAQ,OAAO,MAAQ,OAAO,GAAM,QAAO;AAErE,MAAI,OAAO,OAAQ,OAAO,MAAQ,OAAO,MAAQ,OAAO,GAAM,QAAO;AAErE,MAAI,OAAO,OAAQ,OAAO,MAAQ,OAAO,MAAQ,OAAO,GAAM,QAAO;AAErE,MAAI,IAAI,UAAU,MAAM,IAAI,EAAE,MAAM,MAAQ,IAAI,EAAE,MAAM,GAAM,QAAO;AAKrE,MAAI,OAAO,OAAQ,OAAO,EAAM,QAAO;AACvC,MACE,IAAI,UAAU,MACd,IAAI,MAAM,GAAG,EAAE,EAAE,SAAS,OAAO,MAAM,kBACvC;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;;;AC/BO,IAAM,qBAAwC;AAAA,EACnD;AAAA,EACA;AAAA,EACA;AACF;AAEO,SAAS,iBAAiB,KAAsB;AACrD,MAAI;AACJ,MAAI;AACF,aAAS,IAAI,IAAI,GAAG;AAAA,EACtB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,OAAO,aAAa,SAAU,QAAO;AACzC,SAAO,mBAAmB,SAAS,OAAO,SAAS,YAAY,CAAC;AAClE;","names":[]}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { R as ResolvedFontSource } from '../types-CL0Hbw6x.js';
|
|
2
|
+
import '@sinclair/typebox';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Load a .ttf/.otf file from disk.
|
|
6
|
+
* Node-only — called from the render pipeline.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
interface FileSourceInput {
|
|
10
|
+
path: string;
|
|
11
|
+
weight?: number;
|
|
12
|
+
italic?: boolean;
|
|
13
|
+
baseDir?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Read a font file and wrap as a ResolvedFontSource. */
|
|
16
|
+
declare function loadFileFontSource(input: FileSourceInput): Promise<ResolvedFontSource>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* On-disk cache for fetched Google Fonts TTFs.
|
|
20
|
+
* Optional — only active when a cacheDir is provided. Node-only.
|
|
21
|
+
*/
|
|
22
|
+
declare class FontDiskCache {
|
|
23
|
+
private readonly dir;
|
|
24
|
+
private ensurePromise;
|
|
25
|
+
constructor(dir: string);
|
|
26
|
+
private ensureDir;
|
|
27
|
+
private pathFor;
|
|
28
|
+
get(key: string): Promise<Buffer | undefined>;
|
|
29
|
+
set(key: string, value: Buffer): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Variable-font instancer. Fetches a variable TTF once (disk-cached), then
|
|
34
|
+
* pins its `wght` axis (plus any additional axes) to produce a clean static
|
|
35
|
+
* TTF per requested weight. Uses harfbuzz via `subset-font` — pure JS + WASM,
|
|
36
|
+
* no native toolchain.
|
|
37
|
+
*
|
|
38
|
+
* Why this exists. Google Fonts serves pre-instanced static TTFs for many
|
|
39
|
+
* families, but the instancing step is lossy: Inter Thin (100) and
|
|
40
|
+
* ExtraLight (200) both ship with `OS/2.usWeightClass=250` and near-
|
|
41
|
+
* identical glyph outlines (xAvgCharWidth differs by 1.8%, glyf table
|
|
42
|
+
* differs by 83 bytes out of 135 KB). Pinning the upstream variable TTF's
|
|
43
|
+
* `wght` axis at exactly 100 vs 200 produces properly distinct instances.
|
|
44
|
+
*
|
|
45
|
+
* Cache strategy:
|
|
46
|
+
* 1. Raw variable TTF cached at key `varsrc|<url>` — one download per URL
|
|
47
|
+
* per process (+ optional disk layer).
|
|
48
|
+
* 2. Instanced static TTF cached at `variable|<url>|<weight>|<italic>` —
|
|
49
|
+
* avoids re-running harfbuzz for weights we've already produced.
|
|
50
|
+
*
|
|
51
|
+
* Full-glyph retention. subset-font's `text` parameter drives which
|
|
52
|
+
* codepoints' glyphs survive. We pass every BMP codepoint so the output
|
|
53
|
+
* is effectively a full-glyph static (not a subset) for any Latin /
|
|
54
|
+
* Cyrillic / Greek / Vietnamese-covering family — which includes every
|
|
55
|
+
* entry in our POPULAR_GOOGLE_FONTS catalog. Supplementary-plane glyphs
|
|
56
|
+
* (emoji) would be dropped, but those aren't in the variable families we
|
|
57
|
+
* target. `preserveNameIds` keeps the human-readable name records our
|
|
58
|
+
* downstream normalization expects.
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
interface VariableFetchOptions {
|
|
62
|
+
url: string;
|
|
63
|
+
weight: number;
|
|
64
|
+
italic: boolean;
|
|
65
|
+
/** Extra axis pins merged on top of the derived `wght` pin (e.g. `ital`,
|
|
66
|
+
* `opsz`, `slnt`). Rare — the `weight`/`italic` pair is usually enough. */
|
|
67
|
+
axes?: Record<string, number>;
|
|
68
|
+
/** Family label used in error messages and diagnostics. */
|
|
69
|
+
familyLabel?: string;
|
|
70
|
+
fetchTimeoutMs?: number;
|
|
71
|
+
fetcher?: typeof fetch;
|
|
72
|
+
memoryCache?: {
|
|
73
|
+
get(key: string): Buffer | undefined;
|
|
74
|
+
set(key: string, value: Buffer): void;
|
|
75
|
+
};
|
|
76
|
+
diskCache?: {
|
|
77
|
+
get(key: string): Promise<Buffer | undefined>;
|
|
78
|
+
set(key: string, value: Buffer): Promise<void>;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
declare function fetchVariableFontSource(opts: VariableFetchOptions): Promise<{
|
|
82
|
+
source?: ResolvedFontSource;
|
|
83
|
+
warnings?: string[];
|
|
84
|
+
}>;
|
|
85
|
+
|
|
86
|
+
export { FontDiskCache, type VariableFetchOptions, fetchVariableFontSource, loadFileFontSource };
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import {
|
|
2
|
+
detectFontFormat,
|
|
3
|
+
isAllowedFontUrl
|
|
4
|
+
} from "../chunk-CP2I5NPP.js";
|
|
5
|
+
|
|
6
|
+
// src/fonts/sources/file-loader.ts
|
|
7
|
+
import { readFile } from "fs/promises";
|
|
8
|
+
import { isAbsolute, resolve as resolvePath } from "path";
|
|
9
|
+
async function loadFileFontSource(input) {
|
|
10
|
+
const fullPath = isAbsolute(input.path) ? input.path : resolvePath(input.baseDir ?? process.cwd(), input.path);
|
|
11
|
+
const data = await readFile(fullPath);
|
|
12
|
+
const format = detectFontFormat(data);
|
|
13
|
+
if (format === "unknown") {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`Font file at "${fullPath}" is not a recognized font file (expected TTF/OTF/WOFF/WOFF2)`
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
data,
|
|
20
|
+
weight: input.weight ?? 400,
|
|
21
|
+
italic: input.italic ?? false,
|
|
22
|
+
format
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/fonts/cache/disk-cache.ts
|
|
27
|
+
import { createHash } from "crypto";
|
|
28
|
+
import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
|
|
29
|
+
import { join } from "path";
|
|
30
|
+
var FontDiskCache = class {
|
|
31
|
+
dir;
|
|
32
|
+
// In-flight promise dedupes the first-write mkdir across concurrent set()
|
|
33
|
+
// calls. Without it, two simultaneous cold-cache writes could both see
|
|
34
|
+
// `ensured=false`, both issue mkdir, and both flip the flag afterwards —
|
|
35
|
+
// harmless today (recursive mkdir is idempotent) but the pattern is
|
|
36
|
+
// right and leaves room to add per-directory locks if we ever need to.
|
|
37
|
+
ensurePromise = null;
|
|
38
|
+
constructor(dir) {
|
|
39
|
+
this.dir = dir;
|
|
40
|
+
}
|
|
41
|
+
ensureDir() {
|
|
42
|
+
if (!this.ensurePromise) {
|
|
43
|
+
this.ensurePromise = mkdir(this.dir, { recursive: true }).then(
|
|
44
|
+
() => void 0
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return this.ensurePromise;
|
|
48
|
+
}
|
|
49
|
+
pathFor(key) {
|
|
50
|
+
const hash = createHash("sha256").update(key).digest("hex").slice(0, 24);
|
|
51
|
+
return join(this.dir, `${hash}.bin`);
|
|
52
|
+
}
|
|
53
|
+
async get(key) {
|
|
54
|
+
try {
|
|
55
|
+
return await readFile2(this.pathFor(key));
|
|
56
|
+
} catch {
|
|
57
|
+
return void 0;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async set(key, value) {
|
|
61
|
+
await this.ensureDir();
|
|
62
|
+
await writeFile(this.pathFor(key), value);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// src/fonts/sources/variable-fetcher.ts
|
|
67
|
+
var subsetFontPromise = null;
|
|
68
|
+
function loadSubsetFont() {
|
|
69
|
+
if (!subsetFontPromise) {
|
|
70
|
+
subsetFontPromise = import("subset-font").then((m) => m.default);
|
|
71
|
+
}
|
|
72
|
+
return subsetFontPromise;
|
|
73
|
+
}
|
|
74
|
+
function rawCacheKey(url) {
|
|
75
|
+
return `varsrc|${url}`;
|
|
76
|
+
}
|
|
77
|
+
function instanceCacheKey(url, weight, italic, axes) {
|
|
78
|
+
const axisPart = axes ? "|" + Object.entries(axes).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}=${v}`).join(",") : "";
|
|
79
|
+
return `variable|${url}|${weight}|${italic ? "i" : "r"}${axisPart}`;
|
|
80
|
+
}
|
|
81
|
+
var cachedBmpCharset = null;
|
|
82
|
+
function bmpCharset() {
|
|
83
|
+
if (cachedBmpCharset) return cachedBmpCharset;
|
|
84
|
+
let s = "";
|
|
85
|
+
for (let cp = 32; cp <= 65535; cp++) {
|
|
86
|
+
if (cp >= 55296 && cp <= 57343) continue;
|
|
87
|
+
s += String.fromCodePoint(cp);
|
|
88
|
+
}
|
|
89
|
+
cachedBmpCharset = s;
|
|
90
|
+
return s;
|
|
91
|
+
}
|
|
92
|
+
async function fetchVariableSource(opts) {
|
|
93
|
+
if (!isAllowedFontUrl(opts.url)) {
|
|
94
|
+
return { error: "host not in allowlist or non-HTTPS" };
|
|
95
|
+
}
|
|
96
|
+
const key = rawCacheKey(opts.url);
|
|
97
|
+
const mem = opts.memoryCache?.get(key);
|
|
98
|
+
if (mem) return { buf: mem };
|
|
99
|
+
const disk = await opts.diskCache?.get(key);
|
|
100
|
+
if (disk) {
|
|
101
|
+
opts.memoryCache?.set(key, disk);
|
|
102
|
+
return { buf: disk };
|
|
103
|
+
}
|
|
104
|
+
const ctrl = new AbortController();
|
|
105
|
+
const timer = setTimeout(() => ctrl.abort(), opts.fetchTimeoutMs ?? 1e4);
|
|
106
|
+
try {
|
|
107
|
+
const f = opts.fetcher ?? fetch;
|
|
108
|
+
let res = await f(opts.url, { signal: ctrl.signal, redirect: "manual" });
|
|
109
|
+
let hops = 0;
|
|
110
|
+
while (res.status >= 300 && res.status < 400 && res.status !== 304) {
|
|
111
|
+
const next = res.headers.get("location");
|
|
112
|
+
if (!next) return { error: `${res.status} with no Location` };
|
|
113
|
+
const resolved = new URL(next, opts.url).toString();
|
|
114
|
+
if (!isAllowedFontUrl(resolved)) {
|
|
115
|
+
return { error: `redirect to disallowed host: ${resolved}` };
|
|
116
|
+
}
|
|
117
|
+
if (++hops > 3) return { error: "too many redirects" };
|
|
118
|
+
res = await f(resolved, { signal: ctrl.signal, redirect: "manual" });
|
|
119
|
+
}
|
|
120
|
+
if (!res.ok) return { error: `HTTP ${res.status} ${res.statusText}` };
|
|
121
|
+
const ab = await res.arrayBuffer();
|
|
122
|
+
const buf = Buffer.from(ab);
|
|
123
|
+
if (buf.length < 1024)
|
|
124
|
+
return { error: `response too small (${buf.length}B)` };
|
|
125
|
+
const format = detectFontFormat(buf);
|
|
126
|
+
if (format !== "ttf" && format !== "otf") {
|
|
127
|
+
return { error: `unexpected font format: ${format}` };
|
|
128
|
+
}
|
|
129
|
+
opts.memoryCache?.set(key, buf);
|
|
130
|
+
await opts.diskCache?.set(key, buf);
|
|
131
|
+
return { buf };
|
|
132
|
+
} catch (err) {
|
|
133
|
+
return { error: err.message };
|
|
134
|
+
} finally {
|
|
135
|
+
clearTimeout(timer);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function fetchVariableFontSource(opts) {
|
|
139
|
+
const key = instanceCacheKey(opts.url, opts.weight, opts.italic, opts.axes);
|
|
140
|
+
const mem = opts.memoryCache?.get(key);
|
|
141
|
+
if (mem) {
|
|
142
|
+
return {
|
|
143
|
+
source: {
|
|
144
|
+
data: mem,
|
|
145
|
+
weight: opts.weight,
|
|
146
|
+
italic: opts.italic,
|
|
147
|
+
format: detectFontFormat(mem)
|
|
148
|
+
},
|
|
149
|
+
warnings: []
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const disk = await opts.diskCache?.get(key);
|
|
153
|
+
if (disk) {
|
|
154
|
+
opts.memoryCache?.set(key, disk);
|
|
155
|
+
return {
|
|
156
|
+
source: {
|
|
157
|
+
data: disk,
|
|
158
|
+
weight: opts.weight,
|
|
159
|
+
italic: opts.italic,
|
|
160
|
+
format: detectFontFormat(disk)
|
|
161
|
+
},
|
|
162
|
+
warnings: []
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const fetched = await fetchVariableSource(opts);
|
|
166
|
+
if ("error" in fetched) {
|
|
167
|
+
return {
|
|
168
|
+
warnings: [
|
|
169
|
+
`Variable font fetch "${opts.url}" for "${opts.familyLabel ?? opts.url}" weight ${opts.weight}: ${fetched.error}; falling back to host defaults.`
|
|
170
|
+
]
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const raw = fetched.buf;
|
|
174
|
+
const variationAxes = {
|
|
175
|
+
wght: opts.weight,
|
|
176
|
+
...opts.axes ?? {}
|
|
177
|
+
};
|
|
178
|
+
let instanced;
|
|
179
|
+
try {
|
|
180
|
+
const subsetFont = await loadSubsetFont();
|
|
181
|
+
instanced = await subsetFont(raw, bmpCharset(), {
|
|
182
|
+
targetFormat: "sfnt",
|
|
183
|
+
variationAxes,
|
|
184
|
+
// Keep every common name record. harfbuzz drops the ones not in
|
|
185
|
+
// this list; our downstream rewrites need 1/2/4/6/16/17 intact.
|
|
186
|
+
preserveNameIds: [
|
|
187
|
+
0,
|
|
188
|
+
1,
|
|
189
|
+
2,
|
|
190
|
+
3,
|
|
191
|
+
4,
|
|
192
|
+
5,
|
|
193
|
+
6,
|
|
194
|
+
7,
|
|
195
|
+
8,
|
|
196
|
+
9,
|
|
197
|
+
10,
|
|
198
|
+
11,
|
|
199
|
+
12,
|
|
200
|
+
13,
|
|
201
|
+
14,
|
|
202
|
+
15,
|
|
203
|
+
16,
|
|
204
|
+
17,
|
|
205
|
+
18,
|
|
206
|
+
19,
|
|
207
|
+
20,
|
|
208
|
+
21,
|
|
209
|
+
22,
|
|
210
|
+
23,
|
|
211
|
+
24,
|
|
212
|
+
25
|
|
213
|
+
]
|
|
214
|
+
});
|
|
215
|
+
} catch (err) {
|
|
216
|
+
return {
|
|
217
|
+
warnings: [
|
|
218
|
+
`Variable font instancing for "${opts.familyLabel ?? opts.url}" weight ${opts.weight}: ${err.message}`
|
|
219
|
+
]
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
opts.memoryCache?.set(key, instanced);
|
|
223
|
+
await opts.diskCache?.set(key, instanced);
|
|
224
|
+
return {
|
|
225
|
+
source: {
|
|
226
|
+
data: instanced,
|
|
227
|
+
weight: opts.weight,
|
|
228
|
+
italic: opts.italic,
|
|
229
|
+
format: detectFontFormat(instanced)
|
|
230
|
+
},
|
|
231
|
+
warnings: []
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
export {
|
|
235
|
+
FontDiskCache,
|
|
236
|
+
fetchVariableFontSource,
|
|
237
|
+
loadFileFontSource
|
|
238
|
+
};
|
|
239
|
+
//# sourceMappingURL=node.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/fonts/sources/file-loader.ts","../../src/fonts/cache/disk-cache.ts","../../src/fonts/sources/variable-fetcher.ts"],"sourcesContent":["/**\n * Load a .ttf/.otf file from disk.\n * Node-only — called from the render pipeline.\n */\n\nimport { readFile } from 'fs/promises';\nimport { isAbsolute, resolve as resolvePath } from 'path';\nimport type { ResolvedFontSource } from '../types';\nimport { detectFontFormat } from './format';\n\nexport interface FileSourceInput {\n path: string;\n weight?: number;\n italic?: boolean;\n baseDir?: string;\n}\n\n/** Read a font file and wrap as a ResolvedFontSource. */\nexport async function loadFileFontSource(\n input: FileSourceInput\n): Promise<ResolvedFontSource> {\n const fullPath = isAbsolute(input.path)\n ? input.path\n : resolvePath(input.baseDir ?? process.cwd(), input.path);\n const data = await readFile(fullPath);\n const format = detectFontFormat(data);\n if (format === 'unknown') {\n throw new Error(\n `Font file at \"${fullPath}\" is not a recognized font file (expected TTF/OTF/WOFF/WOFF2)`\n );\n }\n // No format rejection here: bytes flow to the LibreOffice preview\n // stager, which handles WOFF/WOFF2 natively via fontconfig on\n // Linux/macOS. Office output never embeds these bytes — substitute/\n // custom modes rely on recipient-side fonts.\n return {\n data,\n weight: input.weight ?? 400,\n italic: input.italic ?? false,\n format,\n };\n}\n","/**\n * On-disk cache for fetched Google Fonts TTFs.\n * Optional — only active when a cacheDir is provided. Node-only.\n */\n\nimport { createHash } from 'crypto';\nimport { mkdir, readFile, writeFile } from 'fs/promises';\nimport { join } from 'path';\n\nexport class FontDiskCache {\n private readonly dir: string;\n // In-flight promise dedupes the first-write mkdir across concurrent set()\n // calls. Without it, two simultaneous cold-cache writes could both see\n // `ensured=false`, both issue mkdir, and both flip the flag afterwards —\n // harmless today (recursive mkdir is idempotent) but the pattern is\n // right and leaves room to add per-directory locks if we ever need to.\n private ensurePromise: Promise<void> | null = null;\n\n constructor(dir: string) {\n this.dir = dir;\n }\n\n private ensureDir(): Promise<void> {\n if (!this.ensurePromise) {\n this.ensurePromise = mkdir(this.dir, { recursive: true }).then(\n () => undefined\n );\n }\n return this.ensurePromise;\n }\n\n private pathFor(key: string): string {\n const hash = createHash('sha256').update(key).digest('hex').slice(0, 24);\n return join(this.dir, `${hash}.bin`);\n }\n\n async get(key: string): Promise<Buffer | undefined> {\n try {\n return await readFile(this.pathFor(key));\n } catch {\n return undefined;\n }\n }\n\n async set(key: string, value: Buffer): Promise<void> {\n await this.ensureDir();\n await writeFile(this.pathFor(key), value);\n }\n}\n","/**\n * Variable-font instancer. Fetches a variable TTF once (disk-cached), then\n * pins its `wght` axis (plus any additional axes) to produce a clean static\n * TTF per requested weight. Uses harfbuzz via `subset-font` — pure JS + WASM,\n * no native toolchain.\n *\n * Why this exists. Google Fonts serves pre-instanced static TTFs for many\n * families, but the instancing step is lossy: Inter Thin (100) and\n * ExtraLight (200) both ship with `OS/2.usWeightClass=250` and near-\n * identical glyph outlines (xAvgCharWidth differs by 1.8%, glyf table\n * differs by 83 bytes out of 135 KB). Pinning the upstream variable TTF's\n * `wght` axis at exactly 100 vs 200 produces properly distinct instances.\n *\n * Cache strategy:\n * 1. Raw variable TTF cached at key `varsrc|<url>` — one download per URL\n * per process (+ optional disk layer).\n * 2. Instanced static TTF cached at `variable|<url>|<weight>|<italic>` —\n * avoids re-running harfbuzz for weights we've already produced.\n *\n * Full-glyph retention. subset-font's `text` parameter drives which\n * codepoints' glyphs survive. We pass every BMP codepoint so the output\n * is effectively a full-glyph static (not a subset) for any Latin /\n * Cyrillic / Greek / Vietnamese-covering family — which includes every\n * entry in our POPULAR_GOOGLE_FONTS catalog. Supplementary-plane glyphs\n * (emoji) would be dropped, but those aren't in the variable families we\n * target. `preserveNameIds` keeps the human-readable name records our\n * downstream normalization expects.\n */\n\nimport type { ResolvedFontSource } from '../types';\nimport { detectFontFormat } from './format';\nimport { isAllowedFontUrl } from './url-allowlist';\n\n// `subset-font` carries a harfbuzz WASM payload and is Node-only. Lazy-load\n// so a browser bundler that chases the generic `sources/` tree doesn't pull\n// it in. Cached across calls so the WASM heap is created once per process.\nlet subsetFontPromise: Promise<typeof import('subset-font').default> | null =\n null;\nfunction loadSubsetFont(): Promise<typeof import('subset-font').default> {\n if (!subsetFontPromise) {\n subsetFontPromise = import('subset-font').then((m) => m.default);\n }\n return subsetFontPromise;\n}\n\nexport interface VariableFetchOptions {\n url: string;\n weight: number;\n italic: boolean;\n /** Extra axis pins merged on top of the derived `wght` pin (e.g. `ital`,\n * `opsz`, `slnt`). Rare — the `weight`/`italic` pair is usually enough. */\n axes?: Record<string, number>;\n /** Family label used in error messages and diagnostics. */\n familyLabel?: string;\n fetchTimeoutMs?: number;\n fetcher?: typeof fetch;\n memoryCache?: {\n get(key: string): Buffer | undefined;\n set(key: string, value: Buffer): void;\n };\n diskCache?: {\n get(key: string): Promise<Buffer | undefined>;\n set(key: string, value: Buffer): Promise<void>;\n };\n}\n\nfunction rawCacheKey(url: string): string {\n return `varsrc|${url}`;\n}\n\nfunction instanceCacheKey(\n url: string,\n weight: number,\n italic: boolean,\n axes?: Record<string, number>\n): string {\n // Axes go into the key deterministically so different axis pins don't\n // collide. Sorted so `{a:1,b:2}` and `{b:2,a:1}` hash the same.\n const axisPart = axes\n ? '|' +\n Object.entries(axes)\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([k, v]) => `${k}=${v}`)\n .join(',')\n : '';\n return `variable|${url}|${weight}|${italic ? 'i' : 'r'}${axisPart}`;\n}\n\n/**\n * String covering every assigned BMP codepoint (0x20-0xFFFF minus surrogate\n * range). Built lazily on first use — ~127 KiB of UTF-16 memory (0xFFFF\n * codepoints × 2 bytes per UTF-16 code unit, minus the surrogate range)\n * held for the lifetime of the process, which is negligible next to the\n * WASM heap harfbuzz already carries.\n */\nlet cachedBmpCharset: string | null = null;\nfunction bmpCharset(): string {\n if (cachedBmpCharset) return cachedBmpCharset;\n let s = '';\n for (let cp = 0x20; cp <= 0xffff; cp++) {\n // Surrogate range is structurally invalid as standalone codepoints —\n // harfbuzz rejects them. Skip.\n if (cp >= 0xd800 && cp <= 0xdfff) continue;\n s += String.fromCodePoint(cp);\n }\n cachedBmpCharset = s;\n return s;\n}\n\ntype FetchResult = { buf: Buffer } | { error: string };\n\nasync function fetchVariableSource(\n opts: VariableFetchOptions\n): Promise<FetchResult> {\n if (!isAllowedFontUrl(opts.url)) {\n return { error: 'host not in allowlist or non-HTTPS' };\n }\n const key = rawCacheKey(opts.url);\n const mem = opts.memoryCache?.get(key);\n if (mem) return { buf: mem };\n const disk = await opts.diskCache?.get(key);\n if (disk) {\n opts.memoryCache?.set(key, disk);\n return { buf: disk };\n }\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), opts.fetchTimeoutMs ?? 10000);\n try {\n const f = opts.fetcher ?? fetch;\n // redirect: 'manual' so the allowlist can't be bypassed via Location.\n let res = await f(opts.url, { signal: ctrl.signal, redirect: 'manual' });\n let hops = 0;\n while (res.status >= 300 && res.status < 400 && res.status !== 304) {\n const next = res.headers.get('location');\n if (!next) return { error: `${res.status} with no Location` };\n const resolved = new URL(next, opts.url).toString();\n if (!isAllowedFontUrl(resolved)) {\n return { error: `redirect to disallowed host: ${resolved}` };\n }\n if (++hops > 3) return { error: 'too many redirects' };\n res = await f(resolved, { signal: ctrl.signal, redirect: 'manual' });\n }\n if (!res.ok) return { error: `HTTP ${res.status} ${res.statusText}` };\n const ab = await res.arrayBuffer();\n const buf = Buffer.from(ab);\n // Sanity-check: reject sub-1KB or non-TTF responses up front. The\n // instancer would fail loudly on garbage, but a clear \"wrong URL\"\n // signal here shortens the debug cycle.\n if (buf.length < 1024)\n return { error: `response too small (${buf.length}B)` };\n const format = detectFontFormat(buf);\n if (format !== 'ttf' && format !== 'otf') {\n return { error: `unexpected font format: ${format}` };\n }\n opts.memoryCache?.set(key, buf);\n await opts.diskCache?.set(key, buf);\n return { buf };\n } catch (err) {\n return { error: (err as Error).message };\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport async function fetchVariableFontSource(\n opts: VariableFetchOptions\n): Promise<{ source?: ResolvedFontSource; warnings?: string[] }> {\n const key = instanceCacheKey(opts.url, opts.weight, opts.italic, opts.axes);\n const mem = opts.memoryCache?.get(key);\n if (mem) {\n return {\n source: {\n data: mem,\n weight: opts.weight,\n italic: opts.italic,\n format: detectFontFormat(mem),\n },\n warnings: [],\n };\n }\n const disk = await opts.diskCache?.get(key);\n if (disk) {\n opts.memoryCache?.set(key, disk);\n return {\n source: {\n data: disk,\n weight: opts.weight,\n italic: opts.italic,\n format: detectFontFormat(disk),\n },\n warnings: [],\n };\n }\n\n const fetched = await fetchVariableSource(opts);\n if ('error' in fetched) {\n return {\n warnings: [\n `Variable font fetch \"${opts.url}\" for \"${opts.familyLabel ?? opts.url}\" weight ${opts.weight}: ${fetched.error}; falling back to host defaults.`,\n ],\n };\n }\n const raw = fetched.buf;\n\n // Harfbuzz refuses to emit WOFF2 for subset-font's default SFNT target,\n // but we need plain SFNT anyway — Office embeds TTFs, not compressed\n // formats. Pin the weight (and any extra axes) and preserve the name\n // records that our downstream `normalizeNameTable` depends on.\n //\n // Note: italic is encoded by URL (separate italic master), not by axis pin.\n // The `ital` axis exists on some fonts but not others (Inter ships a\n // separate InterVariable-Italic.ttf instead). Callers that want to force\n // an axis pin can pass `axes: { ital: 1 }` explicitly.\n const variationAxes: Record<string, number> = {\n wght: opts.weight,\n ...(opts.axes ?? {}),\n };\n\n let instanced: Buffer;\n try {\n const subsetFont = await loadSubsetFont();\n instanced = await subsetFont(raw, bmpCharset(), {\n targetFormat: 'sfnt',\n variationAxes,\n // Keep every common name record. harfbuzz drops the ones not in\n // this list; our downstream rewrites need 1/2/4/6/16/17 intact.\n preserveNameIds: [\n 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,\n 20, 21, 22, 23, 24, 25,\n ],\n });\n } catch (err) {\n return {\n warnings: [\n `Variable font instancing for \"${opts.familyLabel ?? opts.url}\" weight ${opts.weight}: ${(err as Error).message}`,\n ],\n };\n }\n\n opts.memoryCache?.set(key, instanced);\n await opts.diskCache?.set(key, instanced);\n return {\n source: {\n data: instanced,\n weight: opts.weight,\n italic: opts.italic,\n format: detectFontFormat(instanced),\n },\n warnings: [],\n };\n}\n"],"mappings":"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","names":["readFile"]}
|