@json-to-office/shared 1.5.0 → 1.11.2

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.
@@ -158,14 +158,80 @@ function rewriteNameTable(input, decide) {
158
158
  }
159
159
  return out;
160
160
  }
161
- function rewriteFontFamilyName(input, newFamily) {
161
+ function findTable(input, tag) {
162
+ if (input.length < 12) return null;
163
+ const numTables = input.readUInt16BE(4);
164
+ for (let i = 0; i < numTables; i += 1) {
165
+ const eo = 12 + i * 16;
166
+ if (eo + 16 > input.length) return null;
167
+ if (input.toString("ascii", eo, eo + 4) === tag) {
168
+ return {
169
+ offset: input.readUInt32BE(eo + 8),
170
+ length: input.readUInt32BE(eo + 12)
171
+ };
172
+ }
173
+ }
174
+ return null;
175
+ }
176
+ function readNameRecords(input, wanted) {
177
+ const nt = findTable(input, "name");
178
+ if (!nt) return [];
179
+ const tableOff = nt.offset;
180
+ if (tableOff + 6 > input.length) return [];
181
+ const tableEnd = Math.min(tableOff + nt.length, input.length);
182
+ const count = input.readUInt16BE(tableOff + 2);
183
+ const storage = tableOff + input.readUInt16BE(tableOff + 4);
184
+ const recordsEnd = Math.min(storage, tableEnd);
185
+ const out = [];
186
+ for (let i = 0; i < count; i += 1) {
187
+ const ro = tableOff + 6 + i * 12;
188
+ if (ro + 12 > recordsEnd) break;
189
+ const platformID = input.readUInt16BE(ro);
190
+ const nameID = input.readUInt16BE(ro + 6);
191
+ if (wanted && !wanted.has(nameID)) continue;
192
+ const length = input.readUInt16BE(ro + 8);
193
+ const offset = input.readUInt16BE(ro + 10);
194
+ const start = storage + offset;
195
+ if (start + length > tableEnd) continue;
196
+ const raw = input.slice(start, start + length);
197
+ let value;
198
+ if (platformID === 1) {
199
+ value = raw.toString("ascii");
200
+ } else {
201
+ const swapped = Buffer.from(raw);
202
+ if (swapped.length % 2 === 0) swapped.swap16();
203
+ value = swapped.toString("utf16le");
204
+ }
205
+ out.push({ platformID, nameID, value });
206
+ }
207
+ return out;
208
+ }
209
+ function readFontFamilyNames(input) {
210
+ const out = [];
211
+ for (const r of readNameRecords(input, FAMILY_LOOKUP_IDS)) {
212
+ const value = r.value.trim();
213
+ if (value.length > 0 && !out.includes(value)) out.push(value);
214
+ }
215
+ return out;
216
+ }
217
+ var FAMILY_LOOKUP_IDS = /* @__PURE__ */ new Set([1, 16]);
218
+ function legacySubfamilyName(bold, italic) {
219
+ if (bold) return italic ? "Bold Italic" : "Bold";
220
+ return italic ? "Italic" : "Regular";
221
+ }
222
+ function rewriteFontFamilyName(input, newFamily, subfamily) {
223
+ const style = subfamily && subfamily !== "Regular" ? subfamily : "";
224
+ const fullName = style ? `${newFamily} ${style}` : newFamily;
162
225
  const psForbidden = /[[\](){}<>/%]/g;
163
- const isAscii = /^[\x00-\x7f]*$/.test(newFamily);
164
- const psName = newFamily.replace(/\s+/g, "").replace(psForbidden, "").replace(/[^\x21-\x7e]/g, "");
226
+ const isAscii = /^[\x00-\x7f]*$/.test(fullName);
227
+ const psSafe = (s) => s.replace(/\s+/g, "").replace(psForbidden, "").replace(/[^\x21-\x7e]/g, "");
228
+ const psName = style ? `${psSafe(newFamily)}-${psSafe(style)}` : psSafe(newFamily);
165
229
  return rewriteNameTable(input, (r) => {
166
230
  if (!FAMILY_NAME_IDS.has(r.nameID)) return { action: "keep" };
167
231
  if (r.platformID === 1 && !isAscii) return { action: "drop" };
168
- return { action: "set", value: r.nameID === 6 ? psName : newFamily };
232
+ if (r.nameID === 6) return { action: "set", value: psName };
233
+ if (r.nameID === 4) return { action: "set", value: fullName };
234
+ return { action: "set", value: newFamily };
169
235
  });
170
236
  }
171
237
  var STANDARD_SUBFAMILY = {
@@ -184,7 +250,7 @@ function standardSubfamilyNames(weight, italic) {
184
250
  if (!base) return null;
185
251
  return {
186
252
  typographic: italic ? `${base} Italic` : base,
187
- legacy: weight >= 600 ? italic ? "Bold Italic" : "Bold" : italic ? "Italic" : "Regular"
253
+ legacy: legacySubfamilyName(weight >= 600, italic)
188
254
  };
189
255
  }
190
256
  function rewriteFontSubfamilyNames(input, weight, italic) {
@@ -233,6 +299,8 @@ function detectFontFormat(buf) {
233
299
  }
234
300
 
235
301
  export {
302
+ readNameRecords,
303
+ readFontFamilyNames,
236
304
  rewriteFontFamilyName,
237
305
  standardSubfamilyNames,
238
306
  rewriteFontSubfamilyNames,
@@ -240,4 +308,4 @@ export {
240
308
  isAllowedFontUrl,
241
309
  detectFontFormat
242
310
  };
243
- //# sourceMappingURL=chunk-MCUIFSUN.js.map
311
+ //# sourceMappingURL=chunk-OM2IOZMT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/fonts/sources/ttf-name.ts","../src/fonts/sources/url-allowlist.ts","../src/fonts/sources/format.ts"],"sourcesContent":["/**\n * Name-table reading and rewriting for TTF/OTF sfnt fonts. Two public\n * transforms share the rebuild machinery, plus one reader:\n *\n * - `rewriteFontFamilyName` — rewrite `nameID` 1 / 4 / 6 / 16 to a\n * synthetic family name. Used by `FontRegistry` (so resolved bytes\n * declare the family they were resolved as) and by the preview-side\n * font stagers (so running-text references like `\"Inter Light\"` resolve\n * to the correct face). Core Text / fontconfig / GDI all index by the\n * font's internal `name` table rather than the filename.\n *\n * - `rewriteFontSubfamilyNames` — rewrite `nameID` 2 / 17 to the standard\n * subfamily strings for a (weight, italic) pair. Used by the variable-\n * font instancer: harfbuzz preserves the source font's name records\n * verbatim, so an instanced Bold would otherwise keep the variable\n * font's default-instance \"Regular\" subfamily and trip\n * `validateFontMetadata`.\n *\n * - `readNameRecords` / `readFontFamilyNames` — the reading half, shared\n * with `validateFontMetadata` so the checker and the writer cannot\n * drift on how a name record is located or decoded.\n *\n * Each transform rebuilds the whole font: new `name` table bytes, new\n * table directory with shifted offsets, recomputed per-table checksums,\n * and the magic `head.checkSumAdjustment` recomputed against the whole\n * output buffer. Nothing else is touched.\n *\n * OTF (CFF-flavoured) and TTF (glyf-flavoured) share the sfnt outer\n * structure, so the same code handles both.\n */\n\nconst FAMILY_NAME_IDS = new Set<number>([\n 1, // Font Family\n 4, // Full Font Name\n 6, // PostScript Name\n 16, // Typographic/Preferred Family\n]);\n\nconst MAGIC_HEAD_CHECKSUM = 0xb1b0afba;\n\ninterface NameRecord {\n platformID: number;\n encodingID: number;\n languageID: number;\n nameID: number;\n bytes: Buffer;\n}\n\ninterface TableEntry {\n tag: string;\n checksum: number;\n /** Assigned when rebuilding. */\n offset: number;\n data: Buffer;\n originalOffset: number;\n}\n\n/**\n * Compute the 32-bit big-endian uint sum of `buf`, treating it as a\n * stream of uint32s zero-padded to a 4-byte boundary. Used for per-table\n * checksums and the whole-font `head.checkSumAdjustment`.\n */\nfunction sfntChecksum(buf: Buffer): number {\n let sum = 0;\n const end = buf.length;\n const aligned = end - (end % 4);\n for (let i = 0; i < aligned; i += 4) {\n sum = (sum + buf.readUInt32BE(i)) >>> 0;\n }\n if (aligned < end) {\n let chunk = 0;\n const remaining = end - aligned;\n if (remaining >= 1) chunk |= buf[aligned] << 24;\n if (remaining >= 2) chunk |= buf[aligned + 1] << 16;\n if (remaining >= 3) chunk |= buf[aligned + 2] << 8;\n sum = (sum + chunk) >>> 0;\n }\n return sum >>> 0;\n}\n\nfunction encodeString(record: NameRecord, value: string): Buffer {\n // Platform 3 (Microsoft) and 0 (Unicode) use UTF-16 BE. Platform 1\n // (Macintosh) uses a legacy Roman encoding we approximate with ASCII —\n // non-ASCII family names are rare in this code path.\n if (record.platformID === 1) {\n return Buffer.from(value, 'ascii');\n }\n const out = Buffer.alloc(value.length * 2);\n for (let i = 0; i < value.length; i += 1) {\n out.writeUInt16BE(value.charCodeAt(i), i * 2);\n }\n return out;\n}\n\nfunction buildNameTable(records: NameRecord[]): Buffer {\n const count = records.length;\n const headerSize = 6 + count * 12;\n let heapSize = 0;\n for (const r of records) heapSize += r.bytes.length;\n const raw = Buffer.alloc(headerSize + heapSize);\n raw.writeUInt16BE(0, 0); // format 0\n raw.writeUInt16BE(count, 2);\n raw.writeUInt16BE(headerSize, 4); // stringOffset\n let heapCursor = 0;\n for (let i = 0; i < count; i += 1) {\n const r = records[i];\n const ro = 6 + i * 12;\n raw.writeUInt16BE(r.platformID, ro);\n raw.writeUInt16BE(r.encodingID, ro + 2);\n raw.writeUInt16BE(r.languageID, ro + 4);\n raw.writeUInt16BE(r.nameID, ro + 6);\n raw.writeUInt16BE(r.bytes.length, ro + 8);\n raw.writeUInt16BE(heapCursor, ro + 10);\n r.bytes.copy(raw, headerSize + heapCursor);\n heapCursor += r.bytes.length;\n }\n return raw;\n}\n\n/** Per-record decision for `rewriteNameTable`. */\ntype NameRewrite =\n | { action: 'keep' }\n | { action: 'drop' }\n | { action: 'set'; value: string };\n\n/**\n * Return a copy of `input` whose name records have been mapped through\n * `decide`. Returns the original buffer unchanged if the font has no\n * `name` table or the sfnt header is invalid.\n */\nfunction rewriteNameTable(\n input: Buffer,\n decide: (record: NameRecord) => NameRewrite\n): Buffer {\n if (input.length < 12) return input;\n const version = input.readUInt32BE(0);\n // Accept sfnt (0x00010000), OTTO (OpenType CFF), true, typ1.\n const isSfnt =\n version === 0x00010000 ||\n version === 0x4f54544f /* OTTO */ ||\n version === 0x74727565 /* true */ ||\n version === 0x74797031; /* typ1 */\n if (!isSfnt) return input;\n\n const numTables = input.readUInt16BE(4);\n if (numTables === 0 || input.length < 12 + numTables * 16) return input;\n\n // Read every table's directory entry, slurp its data.\n const tables: TableEntry[] = [];\n for (let i = 0; i < numTables; i += 1) {\n const eo = 12 + i * 16;\n const tag = input.toString('ascii', eo, eo + 4);\n const checksum = input.readUInt32BE(eo + 4);\n const offset = input.readUInt32BE(eo + 8);\n const length = input.readUInt32BE(eo + 12);\n if (offset + length > input.length) return input;\n tables.push({\n tag,\n checksum,\n offset: 0,\n originalOffset: offset,\n data: input.slice(offset, offset + length),\n });\n }\n\n const nameIdx = tables.findIndex((t) => t.tag === 'name');\n if (nameIdx === -1) return input;\n\n // Parse existing name records so we preserve all non-family entries.\n const nameBuf = tables[nameIdx].data;\n if (nameBuf.length < 6) return input;\n // `buildNameTable` emits format 0, which has no language-tag section. A\n // format-1 table keeps its tags after the name records, and any record with\n // languageID >= 0x8000 is an index into them — rewriting it as format 0\n // would strip the tags and leave those records pointing at nothing. Leaving\n // the font unstamped is the lesser loss, so hand it back untouched.\n // Preserving the tag section (and re-homing its string offsets) is the\n // follow-up if a real font ever needs the rewrite.\n if (nameBuf.readUInt16BE(0) !== 0) return input;\n const recordCount = nameBuf.readUInt16BE(2);\n const stringOffset = nameBuf.readUInt16BE(4);\n if (nameBuf.length < 6 + recordCount * 12) return input;\n\n const records: NameRecord[] = [];\n for (let i = 0; i < recordCount; i += 1) {\n const ro = 6 + i * 12;\n const platformID = nameBuf.readUInt16BE(ro);\n const encodingID = nameBuf.readUInt16BE(ro + 2);\n const languageID = nameBuf.readUInt16BE(ro + 4);\n const nameID = nameBuf.readUInt16BE(ro + 6);\n const length = nameBuf.readUInt16BE(ro + 8);\n const offset = nameBuf.readUInt16BE(ro + 10);\n const bytes = nameBuf.slice(\n stringOffset + offset,\n stringOffset + offset + length\n );\n records.push({ platformID, encodingID, languageID, nameID, bytes });\n }\n\n const survivors: NameRecord[] = [];\n for (const r of records) {\n const verdict = decide(r);\n if (verdict.action === 'drop') continue;\n if (verdict.action === 'set') {\n r.bytes = encodeString(r, verdict.value);\n }\n survivors.push(r);\n }\n records.length = 0;\n records.push(...survivors);\n\n const rebuiltName = buildNameTable(records);\n tables[nameIdx] = {\n ...tables[nameIdx],\n data: rebuiltName,\n checksum: sfntChecksum(rebuiltName),\n };\n\n // Preserve original physical order so tables whose offsets follow each\n // other stay contiguous (some consumers skim by offset rather than\n // directory). Offsets get reassigned either way — this is purely\n // aesthetic. Sort is stable.\n tables.sort((a, b) => a.originalOffset - b.originalOffset);\n\n // Assign new offsets, 4-byte aligned.\n let cursor = 12 + tables.length * 16;\n for (const t of tables) {\n cursor = (cursor + 3) & ~3;\n t.offset = cursor;\n cursor += t.data.length;\n }\n const totalSize = (cursor + 3) & ~3;\n\n const out = Buffer.alloc(totalSize);\n // Header — copy sfnt version, entrySelector, etc. verbatim; we preserve\n // numTables since we're not adding/removing entries.\n input.copy(out, 0, 0, 12);\n out.writeUInt16BE(tables.length, 4);\n\n // Directory entries go in alphabetical tag order per the sfnt spec.\n const dirTables = [...tables].sort((a, b) =>\n a.tag < b.tag ? -1 : a.tag > b.tag ? 1 : 0\n );\n for (let i = 0; i < dirTables.length; i += 1) {\n const t = dirTables[i];\n const eo = 12 + i * 16;\n out.write(t.tag, eo, 4, 'ascii');\n out.writeUInt32BE(t.checksum, eo + 4);\n out.writeUInt32BE(t.offset, eo + 8);\n out.writeUInt32BE(t.data.length, eo + 12);\n }\n\n // Write each table's bytes at its new offset. `out` is zero-filled, so\n // the 0–3 byte alignment padding after each table is already correct.\n for (const t of tables) {\n t.data.copy(out, t.offset);\n }\n\n // Recompute head.checkSumAdjustment. The algorithm: zero the field,\n // sum the whole font, then set the field to MAGIC - sum.\n const headTable = tables.find((t) => t.tag === 'head');\n if (headTable && headTable.data.length >= 12) {\n out.writeUInt32BE(0, headTable.offset + 8);\n const fontSum = sfntChecksum(out);\n const adjustment = (MAGIC_HEAD_CHECKSUM - fontSum) >>> 0;\n out.writeUInt32BE(adjustment, headTable.offset + 8);\n }\n\n return out;\n}\n\n/** One decoded `name` record: where it came from and what it says. */\nexport interface DecodedNameRecord {\n platformID: number;\n nameID: number;\n value: string;\n}\n\n/** Byte range of `tag`'s table within `input`, or null. */\nfunction findTable(\n input: Buffer,\n tag: string\n): { offset: number; length: number } | null {\n if (input.length < 12) return null;\n const numTables = input.readUInt16BE(4);\n for (let i = 0; i < numTables; i += 1) {\n const eo = 12 + i * 16;\n if (eo + 16 > input.length) return null;\n if (input.toString('ascii', eo, eo + 4) === tag) {\n return {\n offset: input.readUInt32BE(eo + 8),\n length: input.readUInt32BE(eo + 12),\n };\n }\n }\n return null;\n}\n\n/**\n * Read the `name` records a font carries, optionally narrowed to `wanted`\n * nameIDs. Tolerant of malformed tables, because these bytes come off the\n * network: everything is bounded by the `name` table's own extent, not by\n * the buffer. `Buffer.slice` clamps an out-of-range window silently rather\n * than throwing, so without that bound a table claiming more records than it\n * holds decodes the bytes of whatever table follows it, and a record with a\n * bogus string offset returns a truncated or foreign name.\n *\n * A header that would run past the table ends the scan; a single record\n * whose string does is skipped, since the other records are still readable\n * and each one stands alone. Either way the name is not reported, which is\n * the safe direction — a caller asking \"do these bytes answer to family X?\"\n * gets no for an unreadable record instead of a coincidental yes.\n *\n * Shared with `validateFontMetadata` so the checker and the rewriters above\n * cannot disagree about what a font declares.\n */\nexport function readNameRecords(\n input: Buffer,\n wanted?: Set<number>\n): DecodedNameRecord[] {\n const nt = findTable(input, 'name');\n if (!nt) return [];\n const tableOff = nt.offset;\n if (tableOff + 6 > input.length) return [];\n // The directory may claim a length past the end of a truncated download.\n const tableEnd = Math.min(tableOff + nt.length, input.length);\n const count = input.readUInt16BE(tableOff + 2);\n const storage = tableOff + input.readUInt16BE(tableOff + 4);\n // Records occupy exactly the span between the 6-byte header and the string\n // storage. Bounding them by the table alone is not enough: an inflated\n // `count` would keep reading 12-byte \"records\" out of the string heap,\n // which is inside the table and decodes to plausible-looking garbage.\n const recordsEnd = Math.min(storage, tableEnd);\n const out: DecodedNameRecord[] = [];\n for (let i = 0; i < count; i += 1) {\n const ro = tableOff + 6 + i * 12;\n if (ro + 12 > recordsEnd) break;\n const platformID = input.readUInt16BE(ro);\n const nameID = input.readUInt16BE(ro + 6);\n if (wanted && !wanted.has(nameID)) continue;\n const length = input.readUInt16BE(ro + 8);\n const offset = input.readUInt16BE(ro + 10);\n const start = storage + offset;\n if (start + length > tableEnd) continue;\n const raw = input.slice(start, start + length);\n let value: string;\n if (platformID === 1) {\n value = raw.toString('ascii');\n } else {\n // UTF-16BE. Buffer has no utf16be decoder — swap to LE and decode.\n const swapped = Buffer.from(raw);\n if (swapped.length % 2 === 0) swapped.swap16();\n value = swapped.toString('utf16le');\n }\n out.push({ platformID, nameID, value });\n }\n return out;\n}\n\n/**\n * Every distinct family name a font declares — `nameID` 1 (family) and 16\n * (typographic/preferred family), deduped, in record order.\n *\n * Both count: a weight shipped as its own family names itself \"Life Sans\n * Medium\" in nameID 1 while nameID 16 still says \"Life Sans\", and either\n * is a legitimate way for a consumer to find it. Callers asking \"do these\n * bytes answer to family X?\" must therefore accept a match on either.\n */\nexport function readFontFamilyNames(input: Buffer): string[] {\n const out: string[] = [];\n for (const r of readNameRecords(input, FAMILY_LOOKUP_IDS)) {\n const value = r.value.trim();\n if (value.length > 0 && !out.includes(value)) out.push(value);\n }\n return out;\n}\n\nconst FAMILY_LOOKUP_IDS = new Set<number>([1, 16]);\n\n/**\n * The RIBBI style label a face carries alongside its family name — the\n * four-style vocabulary `nameID` 2 is restricted to. Used to build the\n * full (`nameID` 4) and PostScript (`nameID` 6) names, which must stay\n * distinct across the faces of one family.\n */\nexport function legacySubfamilyName(\n bold: boolean,\n italic: boolean\n): 'Regular' | 'Italic' | 'Bold' | 'Bold Italic' {\n if (bold) return italic ? 'Bold Italic' : 'Bold';\n return italic ? 'Italic' : 'Regular';\n}\n\n/**\n * Return a copy of `input` whose name table has `nameID` 1/4/6/16 rewritten\n * to `newFamily`. Returns the original buffer unchanged if the font has no\n * `name` table or the sfnt header is invalid.\n *\n * `subfamily` is the RIBBI style this face occupies *within* `newFamily`.\n * The family IDs (1/16) always become `newFamily` alone, but the full name\n * (4) and PostScript name (6) take the style as a suffix, so the four faces\n * of one family stay individually addressable — \"Inter\" / \"Inter Italic\" /\n * \"Inter Bold\" / \"Inter Bold Italic\", PostScript \"Inter\" / \"Inter-Italic\" /\n * … Two faces sharing a PostScript name is malformed, and Core Text may\n * refuse the second registration outright.\n *\n * Omitted (or \"Regular\") reproduces the historical behaviour — every\n * targeted ID becomes `newFamily` — which is correct for a face staged\n * under a weight-synthesized family of its own (\"Inter Medium\"), where the\n * face IS that family's Regular member.\n */\nexport function rewriteFontFamilyName(\n input: Buffer,\n newFamily: string,\n subfamily?: string\n): Buffer {\n // \"Regular\" is the absence of a style suffix, not a suffix reading\n // \"Regular\" — a family's default face is named after the family alone.\n const style = subfamily && subfamily !== 'Regular' ? subfamily : '';\n const fullName = style ? `${newFamily} ${style}` : newFamily;\n // PostScript names (nameID 6) are restricted to printable ASCII 33-126\n // minus `[](){}<>/%` per the OpenType spec — fold spaces out and strip\n // any forbidden chars so Word doesn't silently reject the font. The\n // style rides as a hyphenated suffix, the convention every shipped\n // family uses (\"Inter-BoldItalic\").\n const psForbidden = /[[\\](){}<>/%]/g;\n // eslint-disable-next-line no-control-regex\n const isAscii = /^[\\x00-\\x7f]*$/.test(fullName);\n const psSafe = (s: string): string =>\n s\n .replace(/\\s+/g, '')\n .replace(psForbidden, '')\n // eslint-disable-next-line no-control-regex\n .replace(/[^\\x21-\\x7e]/g, '');\n const psName = style\n ? `${psSafe(newFamily)}-${psSafe(style)}`\n : psSafe(newFamily);\n return rewriteNameTable(input, (r) => {\n if (!FAMILY_NAME_IDS.has(r.nameID)) return { action: 'keep' };\n // Platform 1 (Macintosh Roman) only round-trips ASCII. For non-ASCII\n // family names (e.g. CJK), `Buffer.from(value, 'ascii')` silently\n // drops the high bytes and produces garbled Roman-encoded strings\n // that Core Text may still index. Drop those records instead —\n // platforms 0 (Unicode) and 3 (Microsoft) carry the UTF-16 form and\n // are what modern consumers prefer anyway.\n if (r.platformID === 1 && !isAscii) return { action: 'drop' };\n if (r.nameID === 6) return { action: 'set', value: psName };\n if (r.nameID === 4) return { action: 'set', value: fullName };\n return { action: 'set', value: newFamily };\n });\n}\n\nconst STANDARD_SUBFAMILY: Record<number, string> = {\n 100: 'Thin',\n 200: 'ExtraLight',\n 300: 'Light',\n 400: 'Regular',\n 500: 'Medium',\n 600: 'SemiBold',\n 700: 'Bold',\n 800: 'ExtraBold',\n 900: 'Black',\n};\n\n/**\n * Standard OpenType subfamily strings for a (weight, italic) pair, or null\n * for a non-standard weight. `typographic` is the nameID 17 form (full\n * weight vocabulary); `legacy` is the nameID 2 form, restricted to the\n * four-style RIBBI model (Regular/Italic/Bold/Bold Italic) that GDI-era\n * consumers expect. Single source of truth shared with\n * `validateFontMetadata` so writer and checker cannot diverge.\n */\nexport function standardSubfamilyNames(\n weight: number,\n italic: boolean\n): { typographic: string; legacy: string } | null {\n const base = STANDARD_SUBFAMILY[weight];\n if (!base) return null;\n return {\n typographic: italic ? `${base} Italic` : base,\n legacy: legacySubfamilyName(weight >= 600, italic),\n };\n}\n\n/**\n * Return a copy of `input` whose existing `nameID` 2/17 records carry the\n * standard subfamily strings for (weight, italic). Missing records are not\n * added. Returns the original buffer unchanged for non-standard weights,\n * or if the font has no `name` table or the sfnt header is invalid.\n */\nexport function rewriteFontSubfamilyNames(\n input: Buffer,\n weight: number,\n italic: boolean\n): Buffer {\n const std = standardSubfamilyNames(weight, italic);\n if (!std) return input;\n return rewriteNameTable(input, (r) => {\n if (r.nameID === 2) return { action: 'set', value: std.legacy };\n if (r.nameID === 17) return { action: 'set', value: std.typographic };\n return { action: 'keep' };\n });\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","/**\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"],"mappings":"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","names":[]}
@@ -2,7 +2,7 @@ import {
2
2
  detectFontFormat,
3
3
  isAllowedFontUrl,
4
4
  rewriteFontSubfamilyNames
5
- } from "../chunk-MCUIFSUN.js";
5
+ } from "../chunk-OM2IOZMT.js";
6
6
 
7
7
  // src/fonts/sources/file-loader.ts
8
8
  import { readFile } from "fs/promises";
package/dist/index.d.ts CHANGED
@@ -201,38 +201,25 @@ interface SynthesizedFamily {
201
201
  */
202
202
  declare function synthesizeFamilyName(family: string, weight: number | undefined, italic: boolean): SynthesizedFamily;
203
203
 
204
- /**
205
- * Name-table rewriting for TTF/OTF sfnt fonts. Two public transforms share
206
- * the rebuild machinery:
207
- *
208
- * - `rewriteFontFamilyName` — rewrite `nameID` 1 / 4 / 6 / 16 to a
209
- * synthetic family name. Used by the preview-side font stagers so that
210
- * running-text references like `"Inter Light"` resolve to the correct
211
- * face when the stager registers it with Core Text / fontconfig / GDI
212
- * (all of which index by the font's internal `name` table rather than
213
- * the filename).
214
- *
215
- * - `rewriteFontSubfamilyNames` — rewrite `nameID` 2 / 17 to the standard
216
- * subfamily strings for a (weight, italic) pair. Used by the variable-
217
- * font instancer: harfbuzz preserves the source font's name records
218
- * verbatim, so an instanced Bold would otherwise keep the variable
219
- * font's default-instance "Regular" subfamily and trip
220
- * `validateFontMetadata`.
221
- *
222
- * Each transform rebuilds the whole font: new `name` table bytes, new
223
- * table directory with shifted offsets, recomputed per-table checksums,
224
- * and the magic `head.checkSumAdjustment` recomputed against the whole
225
- * output buffer. Nothing else is touched.
226
- *
227
- * OTF (CFF-flavoured) and TTF (glyf-flavoured) share the sfnt outer
228
- * structure, so the same code handles both.
229
- */
230
204
  /**
231
205
  * Return a copy of `input` whose name table has `nameID` 1/4/6/16 rewritten
232
206
  * to `newFamily`. Returns the original buffer unchanged if the font has no
233
207
  * `name` table or the sfnt header is invalid.
208
+ *
209
+ * `subfamily` is the RIBBI style this face occupies *within* `newFamily`.
210
+ * The family IDs (1/16) always become `newFamily` alone, but the full name
211
+ * (4) and PostScript name (6) take the style as a suffix, so the four faces
212
+ * of one family stay individually addressable — "Inter" / "Inter Italic" /
213
+ * "Inter Bold" / "Inter Bold Italic", PostScript "Inter" / "Inter-Italic" /
214
+ * … Two faces sharing a PostScript name is malformed, and Core Text may
215
+ * refuse the second registration outright.
216
+ *
217
+ * Omitted (or "Regular") reproduces the historical behaviour — every
218
+ * targeted ID becomes `newFamily` — which is correct for a face staged
219
+ * under a weight-synthesized family of its own ("Inter Medium"), where the
220
+ * face IS that family's Regular member.
234
221
  */
235
- declare function rewriteFontFamilyName(input: Buffer, newFamily: string): Buffer;
222
+ declare function rewriteFontFamilyName(input: Buffer, newFamily: string, subfamily?: string): Buffer;
236
223
 
237
224
  /**
238
225
  * Hostname allowlist for font fetchers.
package/dist/index.js CHANGED
@@ -2,9 +2,11 @@ import {
2
2
  FONT_URL_ALLOWLIST,
3
3
  detectFontFormat,
4
4
  isAllowedFontUrl,
5
+ readFontFamilyNames,
6
+ readNameRecords,
5
7
  rewriteFontFamilyName,
6
8
  standardSubfamilyNames
7
- } from "./chunk-MCUIFSUN.js";
9
+ } from "./chunk-OM2IOZMT.js";
8
10
  import {
9
11
  ComponentValidationError,
10
12
  DuplicateComponentError,
@@ -570,36 +572,6 @@ function readUsWeightClass(ttf) {
570
572
  if (os2.off + 6 > ttf.length) return null;
571
573
  return ttf.readUInt16BE(os2.off + 4);
572
574
  }
573
- function readNames(ttf, wanted) {
574
- const nt = readTable(ttf, "name");
575
- if (!nt) return [];
576
- const tableOff = nt.off;
577
- if (tableOff + 6 > ttf.length) return [];
578
- const count = ttf.readUInt16BE(tableOff + 2);
579
- const storageRel = ttf.readUInt16BE(tableOff + 4);
580
- const storage = tableOff + storageRel;
581
- const out = [];
582
- for (let j = 0; j < count; j++) {
583
- const r = tableOff + 6 + j * 12;
584
- if (r + 12 > ttf.length) break;
585
- const platformID = ttf.readUInt16BE(r);
586
- const nameID = ttf.readUInt16BE(r + 6);
587
- if (!wanted.has(nameID)) continue;
588
- const length = ttf.readUInt16BE(r + 8);
589
- const offset = ttf.readUInt16BE(r + 10);
590
- const raw = ttf.slice(storage + offset, storage + offset + length);
591
- let value;
592
- if (platformID === 1) {
593
- value = raw.toString("ascii");
594
- } else {
595
- const swapped = Buffer.from(raw);
596
- if (swapped.length % 2 === 0) swapped.swap16();
597
- value = swapped.toString("utf16le");
598
- }
599
- out.push({ platformID, nameID, value });
600
- }
601
- return out;
602
- }
603
575
  function validateFontMetadata(ttf, weight, italic, familyLabel) {
604
576
  const diags = [];
605
577
  const usWeight = readUsWeightClass(ttf);
@@ -609,11 +581,20 @@ function validateFontMetadata(ttf, weight, italic, familyLabel) {
609
581
  message: `Font "${familyLabel}" weight ${weight}: OS/2.usWeightClass reports ${usWeight}. Likely a defective redistribution \u2014 consider adding an upstream override.`
610
582
  });
611
583
  }
584
+ const declaredFamilies = readFontFamilyNames(ttf);
585
+ if (declaredFamilies.length > 0 && !declaredFamilies.includes(familyLabel.trim())) {
586
+ diags.push({
587
+ code: "FAMILY_MISMATCH",
588
+ message: `Font "${familyLabel}" weight ${weight}${italic ? " italic" : ""}: name table declares ${declaredFamilies.map((f) => `"${f}"`).join(
589
+ " / "
590
+ )}, not "${familyLabel}". Referencing runs will not resolve this face.`
591
+ });
592
+ }
612
593
  const std = standardSubfamilyNames(weight, italic);
613
594
  if (!std) return diags;
614
595
  const expected17 = std.typographic;
615
596
  const expected2 = std.legacy;
616
- const names = readNames(ttf, /* @__PURE__ */ new Set([2, 17]));
597
+ const names = readNameRecords(ttf, /* @__PURE__ */ new Set([2, 17]));
617
598
  for (const n of names) {
618
599
  if (n.nameID === 17 && n.value !== expected17) {
619
600
  diags.push({
@@ -631,6 +612,75 @@ function validateFontMetadata(ttf, weight, italic, familyLabel) {
631
612
  return diags;
632
613
  }
633
614
 
615
+ // src/fonts/sources/ttf-structure.ts
616
+ var SFNT_VERSIONS = /* @__PURE__ */ new Set([
617
+ 65536,
618
+ // TrueType outlines
619
+ 1330926671,
620
+ // 'OTTO' — CFF outlines
621
+ 1953658213,
622
+ // 'true'
623
+ 1954115633
624
+ // 'typ1'
625
+ ]);
626
+ var HEADER_SIZE2 = 12;
627
+ var TABLE_RECORD_SIZE2 = 16;
628
+ function validateFontStructure(ttf, weight, italic, familyLabel) {
629
+ const face = `Font "${familyLabel}" weight ${weight}${italic ? " italic" : ""}`;
630
+ const unreadable = (reason) => ({
631
+ code: "FONT_UNREADABLE",
632
+ message: `${face}: ${reason} The face will not resolve; text referencing it renders in a fallback.`
633
+ });
634
+ if (ttf.length < HEADER_SIZE2) {
635
+ return unreadable(
636
+ `${ttf.length} bytes is shorter than an sfnt header \u2014 the download is truncated or empty.`
637
+ );
638
+ }
639
+ const version = ttf.readUInt32BE(0);
640
+ if (!SFNT_VERSIONS.has(version)) {
641
+ return unreadable(
642
+ `sfnt version 0x${version.toString(16).padStart(8, "0")} is neither TrueType nor OpenType.`
643
+ );
644
+ }
645
+ const numTables = ttf.readUInt16BE(4);
646
+ if (numTables === 0) return unreadable("its table directory is empty.");
647
+ const directoryEnd = HEADER_SIZE2 + numTables * TABLE_RECORD_SIZE2;
648
+ if (directoryEnd > ttf.length) {
649
+ return unreadable(
650
+ `its directory claims ${numTables} tables (${directoryEnd} bytes) but the file is ${ttf.length} bytes \u2014 truncated.`
651
+ );
652
+ }
653
+ const tags = /* @__PURE__ */ new Set();
654
+ for (let i = 0; i < numTables; i += 1) {
655
+ const ro = HEADER_SIZE2 + i * TABLE_RECORD_SIZE2;
656
+ const tag = ttf.toString("ascii", ro, ro + 4);
657
+ const offset = ttf.readUInt32BE(ro + 8);
658
+ const length = ttf.readUInt32BE(ro + 12);
659
+ if (offset + length > ttf.length) {
660
+ return unreadable(
661
+ `its "${tag}" table runs to byte ${offset + length} but the file is ${ttf.length} bytes \u2014 truncated.`
662
+ );
663
+ }
664
+ tags.add(tag);
665
+ }
666
+ for (const required of ["name", "head"]) {
667
+ if (!tags.has(required)) {
668
+ return unreadable(`it has no "${required}" table.`);
669
+ }
670
+ }
671
+ const hasTrueTypeOutlines = tags.has("glyf") && tags.has("loca");
672
+ const hasCffOutlines = tags.has("CFF ") || tags.has("CFF2");
673
+ if (!hasTrueTypeOutlines && !hasCffOutlines) {
674
+ return unreadable(
675
+ 'it carries no glyph outlines (neither "glyf" + "loca" nor "CFF ").'
676
+ );
677
+ }
678
+ if (readNameRecords(ttf).length === 0) {
679
+ return unreadable('its "name" table carries no readable records.');
680
+ }
681
+ return null;
682
+ }
683
+
634
684
  // src/fonts/cache/memory-cache.ts
635
685
  var FontMemoryCache = class {
636
686
  store = /* @__PURE__ */ new Map();
@@ -665,6 +715,19 @@ var FontMemoryCache = class {
665
715
  };
666
716
 
667
717
  // src/fonts/registry.ts
718
+ function stampResolvedFamily(source, family) {
719
+ if (source.format !== "ttf" && source.format !== "otf") return source;
720
+ const declared = readFontFamilyNames(source.data);
721
+ if (declared.length === 0 || declared.includes(family)) return source;
722
+ const subfamily = standardSubfamilyNames(
723
+ source.weight,
724
+ source.italic
725
+ )?.legacy;
726
+ return {
727
+ ...source,
728
+ data: rewriteFontFamilyName(source.data, family, subfamily)
729
+ };
730
+ }
668
731
  var FontRegistry = class {
669
732
  index;
670
733
  cache;
@@ -721,8 +784,21 @@ var FontRegistry = class {
721
784
  for (const source of entry.sources) {
722
785
  try {
723
786
  const materialized = await this.materializeSource(source, warnings);
724
- for (const s of materialized) {
725
- if (s.format === "ttf" || s.format === "otf") {
787
+ for (const raw of materialized) {
788
+ const sfnt = raw.format === "ttf" || raw.format === "otf";
789
+ const broken = sfnt ? validateFontStructure(
790
+ raw.data,
791
+ raw.weight,
792
+ raw.italic,
793
+ entry.family
794
+ ) : null;
795
+ if (broken) {
796
+ warnings.push(`[${broken.code}] ${broken.message}`);
797
+ sources.push(raw);
798
+ continue;
799
+ }
800
+ const s = stampResolvedFamily(raw, entry.family);
801
+ if (sfnt) {
726
802
  for (const d of validateFontMetadata(
727
803
  s.data,
728
804
  s.weight,
@@ -1284,7 +1360,7 @@ function deepMerge(target, source) {
1284
1360
  if (isObject(target) && isObject(source)) {
1285
1361
  Object.keys(source).forEach((key) => {
1286
1362
  if (isObject(source[key])) {
1287
- if (!(key in target)) {
1363
+ if (!(key in target) || !isObject(target[key])) {
1288
1364
  output[key] = source[key];
1289
1365
  } else {
1290
1366
  output[key] = deepMerge(target[key], source[key]);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types/services.ts","../src/fonts/collect.ts","../src/fonts/document-registry.ts","../src/fonts/validator.ts","../src/fonts/synthesize.ts","../src/fonts/sources/data-loader.ts","../src/fonts/sources/google-fetcher.ts","../src/fonts/sources/url-fetcher.ts","../src/fonts/sources/ttf-validate.ts","../src/fonts/cache/memory-cache.ts","../src/fonts/registry.ts","../src/fonts/catalog/popular-google.ts","../src/fonts/catalog/upstream-overrides.ts","../src/fonts/substitute.ts","../src/theme/chart-palette.ts","../src/utils/deepMerge.ts"],"sourcesContent":["/**\n * Service configuration types for external integrations (e.g. Highcharts export server)\n */\n\n// ============================================================================\n// Visual rasterization policy — single source of truth for DPI bounds shared\n// by the visual schema, the in-process rasterizer, the flatten transform, and\n// both HTTP /rasterize surfaces. Keep these in sync in one place.\n// ============================================================================\n\n/** Default raster resolution when a `visual` does not specify one. */\nexport const DEFAULT_VISUAL_DPI = 200;\n/** Minimum accepted raster resolution. */\nexport const MIN_VISUAL_DPI = 36;\n/** Maximum accepted raster resolution (bounds bitmap size / DoS surface). */\nexport const MAX_VISUAL_DPI = 600;\n\n/** Clamp an arbitrary dpi to [MIN_VISUAL_DPI, MAX_VISUAL_DPI]; non-finite → default. */\nexport function clampVisualDpi(dpi: unknown): number {\n if (typeof dpi !== 'number' || !Number.isFinite(dpi))\n return DEFAULT_VISUAL_DPI;\n return Math.min(MAX_VISUAL_DPI, Math.max(MIN_VISUAL_DPI, Math.round(dpi)));\n}\n\nexport type HighchartsHeaders = Record<string, string>;\n\nexport type HighchartsHeadersResolver = (\n body: unknown\n) => HighchartsHeaders | Promise<HighchartsHeaders>;\n\nexport interface HighchartsServiceConfig {\n serverUrl?: string;\n headers?: HighchartsHeaders | HighchartsHeadersResolver;\n}\n\n// ============================================================================\n// PPTX rasterization service (used by the docx `visual` component)\n// ============================================================================\n\nexport type PptxServiceHeaders = Record<string, string>;\n\nexport type PptxServiceHeadersResolver = (\n body: unknown\n) => PptxServiceHeaders | Promise<PptxServiceHeaders>;\n\n/** Max font faces accepted in one rasterize request. */\nexport const MAX_RASTERIZE_FONTS = 32;\n/** Max total DECODED font bytes accepted in one rasterize request. */\nexport const MAX_RASTERIZE_FONT_BYTES = 8 * 1024 * 1024;\n\n/**\n * One font face shipped alongside a rasterize request so the rasterizer's\n * out-of-process LibreOffice can render the slide with the document's real\n * fonts instead of a system fallback.\n *\n * `data` is base64 of the raw font file — NO `data:` URI prefix — so the\n * request stays plain serializable JSON.\n *\n * `family` is the CATALOG family (`ResolvedFont.family`, e.g. \"Inter\"), not\n * the synthesized sub-family the presentation references. The receiving\n * stager applies `synthesizeFamilyName` + `rewriteFontFamilyName` itself,\n * exactly as it does for the PDF-preview path; pre-synthesizing here would\n * double-apply the suffix (\"Inter Light Light\").\n */\nexport interface RasterizeFontFace {\n family: string;\n weight: number;\n italic: boolean;\n /** Base64-encoded font file bytes (no `data:` prefix). */\n data: string;\n format?: 'ttf' | 'otf' | 'woff' | 'woff2';\n}\n\n/**\n * Request handed to a pptx rasterizer: a single-slide pptx presentation\n * component definition plus the target resolution.\n */\nexport interface PptxRasterizeRequest {\n /** A pptx presentation component definition ({ name: 'pptx', ... }) with one slide */\n presentation: unknown;\n /** Target raster resolution in dots-per-inch */\n dpi: number;\n /**\n * Directory that relative asset paths inside the presentation resolve\n * against — the originating document's own directory. Absent → the\n * rasterizer's cwd, the legacy behavior (#142).\n */\n baseDir?: string;\n /**\n * Font faces to stage for the rasterizer's LibreOffice launch. Absent →\n * system fonts only, which is what every non-font-aware caller (and every\n * pre-Area-6 client) sends.\n */\n fonts?: RasterizeFontFace[];\n}\n\n/**\n * Result returned by a pptx rasterizer.\n */\nexport interface PptxRasterizeResult {\n /** Rendered PNG as a base64 data URI (data:image/png;base64,...) */\n base64DataUri: string;\n /** Natural pixel width of the rendered image */\n width: number;\n /** Natural pixel height of the rendered image */\n height: number;\n}\n\n/**\n * In-process rasterizer callback. Implementations build the .pptx from the\n * presentation JSON and rasterize it to a PNG (e.g. via LibreOffice + poppler).\n */\nexport type PptxRasterizer = (\n request: PptxRasterizeRequest\n) => Promise<PptxRasterizeResult>;\n\n// ============================================================================\n// Batch rasterization (#153) — one request rasterizes many independent slides.\n//\n// Each slide is a complete single-slide presentation (the exact shape a\n// single {@link PptxRasterizeRequest} carries), NOT a slide fragment of one\n// merged deck. This keeps slides independent — each may use its own canvas\n// size, theme, and dpi, so callers never need to group visuals — and lets\n// implementations key per-slide caches identically to the single-slide path.\n// ============================================================================\n\n/**\n * Maximum slides accepted in one batch request. Shared by the HTTP surface\n * (request validation) and clients (chunk size) so the two cannot drift.\n * Bounds per-request work and response size the same way rate limits bound\n * request counts.\n */\nexport const MAX_RASTERIZE_BATCH_SLIDES = 32;\n\n/** One slide in a batch: a single-slide presentation plus its resolution. */\nexport interface PptxRasterizeBatchSlide {\n /** A pptx presentation component definition ({ name: 'pptx', ... }) with one slide */\n presentation: unknown;\n /** Target raster resolution in dots-per-inch (absent → service default) */\n dpi?: number;\n}\n\n/** Request handed to a batch pptx rasterizer. */\nexport interface PptxRasterizeBatchRequest {\n /** Slides to rasterize; results come back index-aligned with this array. */\n slides: PptxRasterizeBatchSlide[];\n /** Base directory for relative asset paths, shared by every slide (#142). */\n baseDir?: string;\n /**\n * Font faces staged for the batch's LibreOffice launch, shared by every\n * slide exactly like `baseDir`. Deliberately REQUEST-level and never\n * per-slide: {@link PptxRasterizeBatchSlide} stays `{presentation, dpi}` so\n * batch-internal dedupe and the per-slide disk-cache key stay uniform with\n * the single-slide path.\n */\n fonts?: RasterizeFontFace[];\n}\n\n/**\n * Pipeline stage a slide failed in. `build` failures are caused by the\n * slide's own JSON (safe to surface verbatim to callers); `convert` and\n * `rasterize` failures are environment/tooling errors whose raw messages may\n * carry host paths — HTTP surfaces sanitize those.\n */\nexport type PptxRasterizeFailureStage = 'build' | 'convert' | 'rasterize';\n\n/**\n * Per-slide outcome. A batch response is 200-with-item-errors rather than\n * all-or-nothing: one bad visual must not discard its siblings' pixels.\n */\nexport type PptxRasterizeBatchSlideResult =\n | ({ ok: true } & PptxRasterizeResult)\n | { ok: false; error: string; stage?: PptxRasterizeFailureStage };\n\n/** Result returned by a batch pptx rasterizer. */\nexport interface PptxRasterizeBatchResult {\n /** Index-aligned with the request's `slides` (same length, same order). */\n results: PptxRasterizeBatchSlideResult[];\n}\n\n/**\n * In-process batch rasterizer callback. Batch-level failures (missing\n * binaries, bad request) throw; per-slide failures land in `results`.\n */\nexport type PptxBatchRasterizer = (\n request: PptxRasterizeBatchRequest\n) => Promise<PptxRasterizeBatchResult>;\n\n/**\n * Configuration for the pptx rasterization service backing `visual` components.\n *\n * Mirrors {@link HighchartsServiceConfig}: the published packages depend on this\n * interface, never on a binary. A host injects either an in-process `render`\n * callback or an HTTP `serverUrl`.\n */\nexport interface PptxServiceConfig {\n /**\n * In-process rasterizer. Takes precedence over `serverUrl` when provided.\n * Ideal for tests (no binaries) and single-process hosts.\n */\n render?: PptxRasterizer;\n /**\n * In-process batch rasterizer. When provided, the docx renderer coalesces a\n * document's visuals into batch calls (#153) instead of one `render` call\n * per visual. Like `render`, takes precedence over `serverUrl`.\n */\n renderBatch?: PptxBatchRasterizer;\n /**\n * HTTP rasterization service URL. The service receives\n * `{ presentation, dpi }` and returns a {@link PptxRasterizeResult}.\n */\n serverUrl?: string;\n /** Optional headers (or async resolver) for the HTTP service. */\n headers?: PptxServiceHeaders | PptxServiceHeadersResolver;\n /** Default DPI applied when a `visual` does not specify one. */\n dpi?: number;\n}\n\nexport interface ServicesConfig {\n highcharts?: HighchartsServiceConfig;\n pptx?: PptxServiceConfig;\n}\n","/**\n * Walk a document tree and collect every font family name referenced.\n *\n * Matches both DOCX conventions (`family` nested under `font`, theme.fonts.*)\n * and PPTX conventions (`fontFace`, chart `titleFontFace` etc., theme.fonts.*).\n * Key-name matching is intentionally permissive so future component schemas\n * that reuse the same conventions pick up automatically.\n */\n\nexport const FONT_NAME_KEYS = new Set([\n 'family',\n 'fontFace',\n 'titleFontFace',\n 'legendFontFace',\n 'dataLabelFontFace',\n 'catAxisLabelFontFace',\n 'valAxisLabelFontFace',\n]);\n\nexport const THEME_FONT_KEYS = new Set(['heading', 'body', 'mono', 'light']);\n\n/**\n * Subtrees that declare fonts rather than reference them. `fontRegistry`\n * entries carry `family` (and `sources[].family` for kind:'safe'/'google'),\n * which the depth-agnostic `family` match would otherwise scoop up as\n * references — a registry would self-satisfy its own validation, and a\n * `kind:'google'` source family would appear as a phantom reference.\n * substitute.ts's rewriter skips the same key; the two MUST stay in sync.\n */\nexport const FONT_DECLARATION_KEYS = new Set(['fontRegistry']);\n\nfunction collect(node: unknown, out: Set<string>, parentKey?: string): void {\n if (node == null) return;\n\n if (typeof node === 'string') {\n // String values under a font-name key or under theme.fonts.{heading,body,...}\n if (\n parentKey &&\n (FONT_NAME_KEYS.has(parentKey) || THEME_FONT_KEYS.has(parentKey))\n ) {\n const trimmed = node.trim();\n if (trimmed.length > 0) out.add(trimmed);\n }\n return;\n }\n\n if (Array.isArray(node)) {\n // Forward parentKey so font-name arrays (e.g. theme.fonts.heading: [...])\n // are walked with the same context as the non-array case. Mirrors\n // substitute.ts's rewrite walker — the two must stay in sync or\n // substitution silently misses array-shaped references.\n for (const item of node) collect(item, out, parentKey);\n return;\n }\n\n if (typeof node === 'object') {\n // Special-cased: theme.fonts is an object whose values (or values.family) are font names.\n const maybeFonts = (node as Record<string, unknown>).fonts;\n if (parentKey === 'theme' && maybeFonts && typeof maybeFonts === 'object') {\n for (const [k, v] of Object.entries(\n maybeFonts as Record<string, unknown>\n )) {\n if (typeof v === 'string') {\n const trimmed = v.trim();\n if (trimmed.length > 0) out.add(trimmed);\n } else if (v && typeof v === 'object') {\n const fam = (v as Record<string, unknown>).family;\n if (typeof fam === 'string' && fam.trim().length > 0) {\n out.add(fam.trim());\n }\n }\n void k;\n }\n }\n\n for (const [k, v] of Object.entries(node as Record<string, unknown>)) {\n // Declarations, not references — see FONT_DECLARATION_KEYS.\n if (FONT_DECLARATION_KEYS.has(k)) continue;\n collect(v, out, k);\n }\n }\n}\n\n/** Scan an arbitrary doc tree (DOCX or PPTX) for every font family referenced. */\nexport function collectFontNames(doc: unknown): Set<string> {\n const out = new Set<string>();\n collect(doc, out);\n return out;\n}\n\n/** Scan a DOCX document tree for every font family name referenced. */\nexport const collectFontNamesFromDocx = collectFontNames;\n\n/** Scan a PPTX presentation tree for every font family name referenced. */\nexport const collectFontNamesFromPptx = collectFontNames;\n","/**\n * Read the document-scoped and theme-scoped font registries and merge them\n * with runtime `fonts.extraEntries`.\n *\n * Precedence (last wins, matching registry.ts's documented resolution rules\n * and FontRuntimeOpts.extraEntries's \"merged over the document's\n * fontRegistry\"):\n *\n * theme.fontRegistry < document.props.fontRegistry < fonts.extraEntries\n *\n * Merging happens here rather than inside FontRegistry because\n * `validateFontReferences` needs the same merged list, and two merge sites\n * would eventually disagree — which would show up as a font that validates\n * but never renders, or vice versa.\n */\n\nimport type { FontRegistryEntry } from '../schemas/font-catalog';\n\nfunction isEntry(v: unknown): v is FontRegistryEntry {\n if (!v || typeof v !== 'object') return false;\n const e = v as Record<string, unknown>;\n return (\n typeof e.id === 'string' &&\n typeof e.family === 'string' &&\n Array.isArray(e.sources)\n );\n}\n\nfunction readAt(node: unknown, key: string): FontRegistryEntry[] {\n if (!node || typeof node !== 'object') return [];\n const raw = (node as Record<string, unknown>)[key];\n return Array.isArray(raw) ? raw.filter(isEntry) : [];\n}\n\n/** `document.props.fontRegistry`, defensively (props may be absent/null). */\nexport function documentFontRegistry(document: unknown): FontRegistryEntry[] {\n if (!document || typeof document !== 'object') return [];\n return readAt((document as Record<string, unknown>).props, 'fontRegistry');\n}\n\n/** `theme.fontRegistry`, defensively. */\nexport function themeFontRegistry(theme: unknown): FontRegistryEntry[] {\n return readAt(theme, 'fontRegistry');\n}\n\n/**\n * Merge entry groups in precedence order — later groups win on a collision of\n * `family` OR `id`, case-insensitively, which are the same two keys\n * `FontRegistry.addEntry` indexes on. Returns a flat list safe to hand to both\n * `validateFontReferences` and `new FontRegistry({ opts: { extraEntries } })`.\n */\nexport function mergeFontRegistries(\n ...groups: (FontRegistryEntry[] | undefined)[]\n): FontRegistryEntry[] {\n // Order IS the contract: `FontRegistry` replays this array through\n // `addEntry`, indexing on family and id with last-write-wins, so an entry\n // only outranks another by sitting later. Concatenating the groups in\n // precedence order is therefore the whole mechanism.\n //\n // Deliberately NOT de-duped through a Map keyed by family/id: `Map.set` on\n // an existing key keeps the original insertion *position*, so a\n // higher-precedence entry colliding with an earlier one would be emitted\n // early and then lose the replay to the entry it was supposed to beat.\n const out: FontRegistryEntry[] = [];\n for (const group of groups) {\n for (const entry of group ?? []) {\n const family = entry.family.toLowerCase();\n const id = entry.id.toLowerCase();\n // Drop only a true replacement — same family AND same id. An entry that\n // shares just one of the two still owns the other key in FontRegistry's\n // index, so removing it here would make that name unresolvable.\n for (let i = out.length - 1; i >= 0; i--) {\n if (\n out[i].family.toLowerCase() === family &&\n out[i].id.toLowerCase() === id\n ) {\n out.splice(i, 1);\n }\n }\n out.push(entry);\n }\n }\n return out;\n}\n","/**\n * Validate that every font name referenced in a document is either\n * in SAFE_FONTS or present in the document's fontRegistry / runtime overrides.\n *\n * Used at generate-start; emits warnings for unresolved names so pipelines\n * can surface them via their existing warning channels.\n */\n\nimport { SAFE_FONTS, isSafeFont } from '../schemas/font-catalog';\nimport type { FontRegistryEntry } from '../schemas/font-catalog';\n\n/**\n * Warning codes for font resolution + rendering.\n *\n * - `FONT_UNRESOLVED` — family not in SAFE_FONTS and not registered.\n * - `FONT_MODE_SUBSTITUTED` — non-safe families rewritten to safe equivalents.\n * - `FONT_MODE_CUSTOM` — export mode \"custom\" — refs kept as-is.\n */\nexport type FontIssueCode =\n | 'FONT_UNRESOLVED'\n | 'FONT_MODE_SUBSTITUTED'\n | 'FONT_MODE_CUSTOM';\n\nexport interface FontResolutionIssue {\n code: FontIssueCode;\n family: string;\n message: string;\n}\n\nexport interface FontValidationResult {\n /** Names that resolved via SAFE_FONTS or the registry. */\n resolved: string[];\n /** Names with no resolution path. */\n unresolved: string[];\n /** One warning per unresolved name. */\n warnings: FontResolutionIssue[];\n}\n\nexport interface FontValidationInput {\n /** Font names referenced in the document (from collectFontNamesFromDocx / FromPptx). */\n referencedNames: Iterable<string>;\n /** Runtime-registered entries (e.g. from FontRuntimeOpts.extraEntries). */\n registeredEntries?: FontRegistryEntry[];\n}\n\nfunction buildRegistryIndex(\n registeredEntries?: FontRegistryEntry[]\n): Set<string> {\n const idx = new Set<string>();\n for (const e of registeredEntries ?? []) {\n idx.add(e.family.toLowerCase());\n idx.add(e.id.toLowerCase());\n }\n return idx;\n}\n\n/**\n * Validate referenced font names against SAFE_FONTS + runtime-registered entries.\n * Does not perform network fetches — this runs purely off schema + opts content.\n */\nexport function validateFontReferences(\n input: FontValidationInput\n): FontValidationResult {\n const registryIdx = buildRegistryIndex(input.registeredEntries);\n const resolved: string[] = [];\n const unresolved: string[] = [];\n const warnings: FontResolutionIssue[] = [];\n\n for (const name of input.referencedNames) {\n if (isSafeFont(name) || registryIdx.has(name.toLowerCase())) {\n resolved.push(name);\n continue;\n }\n unresolved.push(name);\n warnings.push({\n code: 'FONT_UNRESOLVED',\n family: name,\n message:\n `Font \"${name}\" is not a SAFE_FONTS entry and is not registered via fonts.extraEntries. ` +\n `It will render with a host fallback on machines lacking the font. ` +\n `Safe fonts: ${SAFE_FONTS.join(', ')}.`,\n });\n }\n\n return { resolved, unresolved, warnings };\n}\n","/**\n * Map a (family, weight, italic) tuple to the pair of\n * `(familyName, { bold, italic })` the renderer should actually use.\n *\n * OOXML runs can only carry a bold/italic toggle, not a numeric weight.\n * For weights outside the RIBBI quad (400/700 × roman/italic), Word\n * resolves intermediate weights via **separate sub-family faces** whose\n * internal family name is the canonical Google-Fonts-style subfamily,\n * e.g. `Inter Light`, `Inter ExtraBold Italic`. Rewriting the run's\n * `family` to that synthetic name lets Word pick the right face when the\n * recipient has the full family installed, and lets the LibreOffice\n * preview resolve the matching staged TTF by its internal name.\n *\n * No embedding involved — this is purely a name transform applied at\n * render time. Safe fonts and unrecognised weights fall back to the\n * bold-only heuristic (`weight >= 600 → bold`).\n */\n\n/** Human-readable labels for the canonical font-weight numbers. */\nexport const WEIGHT_LABELS: Record<number, string> = {\n 100: 'Thin',\n 200: 'ExtraLight',\n 300: 'Light',\n 400: 'Regular',\n 500: 'Medium',\n 600: 'SemiBold',\n 700: 'Bold',\n 800: 'ExtraBold',\n 900: 'Black',\n};\n\nexport interface SynthesizedFamily {\n /** The family name to emit in `rFonts`/`fontFace`. */\n family: string;\n /** Whether to also set the run's bold toggle. */\n bold: boolean;\n /** Whether to also set the run's italic toggle. */\n italic: boolean;\n /**\n * `true` when the input `weight` was not one of the canonical\n * 100/200/.../900 labels. The canonical family name is returned with\n * a `weight >= 600 → bold` fallback, but the run will not match a\n * dedicated sub-family face — callers should surface this so authors\n * know the weight was effectively rounded to Regular or Bold.\n */\n nonCanonicalWeight: boolean;\n}\n\n/**\n * Translate `(family, weight, italic)` into the rendering-time family name\n * plus the bold/italic toggles to emit on the run.\n *\n * - RIBBI (weights 400 + 700, roman + italic) stays on the canonical family\n * name and uses native bold/italic toggles.\n * - Other canonical weights become `\"<Family> <Weight>\"` (e.g.\n * `\"Inter Light\"`) with bold/italic toggles cleared; any italic flag is\n * folded into the name (`\"Inter Light Italic\"`).\n * - Non-canonical weights (floating or out-of-range) fall back to\n * `bold = weight >= 600` and leave the family name untouched.\n */\nexport function synthesizeFamilyName(\n family: string,\n weight: number | undefined,\n italic: boolean\n): SynthesizedFamily {\n if (weight == null) {\n return { family, bold: false, italic, nonCanonicalWeight: false };\n }\n // RIBBI — no rewrite needed, let native bold/italic do the work.\n if (weight === 400) {\n return { family, bold: false, italic, nonCanonicalWeight: false };\n }\n if (weight === 700) {\n return { family, bold: true, italic, nonCanonicalWeight: false };\n }\n const label = WEIGHT_LABELS[weight];\n if (!label) {\n // Non-canonical weight — best-effort bold fallback. Flag the result so\n // callers can warn: the run will render as Regular or Bold, not the\n // intermediate weight the author asked for.\n return {\n family,\n bold: weight >= 600,\n italic,\n nonCanonicalWeight: true,\n };\n }\n const suffix = italic ? ` ${label} Italic` : ` ${label}`;\n return {\n family: `${family}${suffix}`,\n bold: false,\n italic: false,\n nonCanonicalWeight: false,\n };\n}\n","/**\n * Decode a base64 / data-URL font payload to a Buffer.\n */\n\nimport type { ResolvedFontSource } from '../types';\nimport { detectFontFormat } from './format';\n\nexport interface DataSourceInput {\n data: string;\n weight?: number;\n italic?: boolean;\n}\n\n/**\n * Hard upper bound on the decoded font buffer. Real TTF/OTF faces are well\n * under 5 MB; even variable-axis fonts with CJK coverage rarely exceed 4 MB.\n * Rejecting oversized payloads before decoding prevents a malicious\n * `kind: 'data'` registry entry from allocating arbitrary server memory\n * when the generator runs behind an HTTP endpoint.\n */\nconst MAX_DATA_FONT_BYTES = 5 * 1024 * 1024;\n\n/**\n * Accepts either a bare base64 string or a data: URL.\n * Throws on invalid input; renderer catches and emits a warning.\n */\nexport function loadDataFontSource(input: DataSourceInput): ResolvedFontSource {\n const raw = input.data.trim();\n let b64: string;\n if (raw.startsWith('data:')) {\n const comma = raw.indexOf(',');\n if (comma < 0) throw new Error('Invalid data URL: no payload separator');\n // Only base64-encoded payloads are supported.\n const header = raw.slice(5, comma);\n if (!header.includes(';base64')) {\n throw new Error('Data URL must be base64-encoded');\n }\n b64 = raw.slice(comma + 1);\n } else {\n b64 = raw;\n }\n // Upper-bound the decoded size via base64 length before allocating. A\n // base64 string decodes to ~3/4 its character count, so a conservative\n // check on the encoded size avoids decoding a 50 MB payload just to\n // reject it afterward.\n const approxDecodedBytes = Math.floor((b64.length * 3) / 4);\n if (approxDecodedBytes > MAX_DATA_FONT_BYTES) {\n throw new Error(\n `Font data payload exceeds ${MAX_DATA_FONT_BYTES} byte limit`\n );\n }\n const data = Buffer.from(b64, 'base64');\n if (data.length === 0) throw new Error('Decoded font buffer is empty');\n if (data.length > MAX_DATA_FONT_BYTES) {\n throw new Error(\n `Font data payload exceeds ${MAX_DATA_FONT_BYTES} byte limit`\n );\n }\n // Base64 decoding silently discards invalid characters, so garbage input\n // yields a non-empty buffer that isn't a real font. Magic-byte check\n // rejects the garbage before it reaches the LibreOffice preview stager.\n const format = detectFontFormat(data);\n if (format === 'unknown') {\n throw new Error(\n 'Decoded font buffer is not a recognized font (expected TTF/OTF/WOFF/WOFF2)'\n );\n }\n // WOFF/WOFF2 flow through: Office output never embeds bytes, and the\n // LibreOffice preview stager handles them via fontconfig.\n return {\n data,\n weight: input.weight ?? 400,\n italic: input.italic ?? false,\n format,\n };\n}\n","/**\n * Google Fonts fetcher.\n *\n * Hits the CSS API v2 with an older User-Agent that returns TTF (default UA\n * gets WOFF2, which Office cannot embed as-is). Parses the `src: url(...)` line\n * and downloads the binary.\n *\n * Uses memory + optional disk cache keyed by `${family}|${weight}|${italic}`.\n */\n\nimport type { ResolvedFontSource } from '../types';\nimport { FontMemoryCache } from '../cache/memory-cache';\nimport { detectFontFormat } from './format';\n\ninterface FontDiskCacheLike {\n get(key: string): Promise<Buffer | undefined>;\n set(key: string, value: Buffer): Promise<void>;\n}\n\nexport interface GoogleFetchOptions {\n family: string;\n weights: number[];\n italics?: boolean;\n memoryCache?: FontMemoryCache;\n diskCache?: FontDiskCacheLike;\n fetchTimeoutMs?: number;\n /** Override for tests. */\n fetcher?: typeof fetch;\n}\n\nexport interface GoogleFetchResult {\n sources: ResolvedFontSource[];\n warnings: string[];\n}\n\nconst TTF_UA = 'Mozilla/4.0';\n\nasync function fetchWithTimeout(\n url: string,\n opts: {\n headers?: Record<string, string>;\n timeoutMs?: number;\n fetcher?: typeof fetch;\n }\n): Promise<Response> {\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 5000);\n try {\n const f = opts.fetcher ?? fetch;\n return await f(url, { headers: opts.headers, signal: ctrl.signal });\n } finally {\n clearTimeout(timer);\n }\n}\n\nfunction buildCssUrl(\n family: string,\n weights: number[],\n italics: boolean\n): string {\n // URL-encode then restore spaces as `+` (Google's CSS2 API convention).\n const famPart = encodeURIComponent(family).replace(/%20/g, '+');\n const sortedWeights = [...weights].sort((a, b) => a - b);\n if (italics) {\n const axis = sortedWeights.flatMap((w) => [`0,${w}`, `1,${w}`]).join(';');\n return `https://fonts.googleapis.com/css2?family=${famPart}:ital,wght@${axis}&display=swap`;\n }\n const wghtPart = sortedWeights.join(';');\n return `https://fonts.googleapis.com/css2?family=${famPart}:wght@${wghtPart}&display=swap`;\n}\n\n/**\n * Parse Google Fonts CSS response into { weight, italic, ttfUrl } tuples.\n * Each @font-face block contains the src + font-weight + font-style we need.\n */\nfunction parseCssFaces(\n css: string\n): { weight: number; italic: boolean; ttfUrl: string }[] {\n const out: { weight: number; italic: boolean; ttfUrl: string }[] = [];\n const faceRe = /@font-face\\s*\\{([^}]*)\\}/g;\n let m: RegExpExecArray | null;\n while ((m = faceRe.exec(css)) !== null) {\n const block = m[1];\n // Pin the CDN: only accept URLs whose hostname is fonts.gstatic.com so a\n // hijacked/mitm'd CSS response can't redirect downloads to arbitrary hosts.\n const urlM = block.match(\n /src:\\s*url\\((https:\\/\\/fonts\\.gstatic\\.com\\/[^)]+\\.ttf)\\)/\n );\n if (!urlM) continue;\n const weightM = block.match(/font-weight:\\s*(\\d+)/);\n const italicM = block.match(/font-style:\\s*italic/);\n out.push({\n weight: weightM ? parseInt(weightM[1], 10) : 400,\n italic: Boolean(italicM),\n ttfUrl: urlM[1],\n });\n }\n return out;\n}\n\nfunction cacheKey(family: string, weight: number, italic: boolean): string {\n return `google|${family}|${weight}|${italic ? 'i' : 'r'}`;\n}\n\nexport async function fetchGoogleFontSources(\n opts: GoogleFetchOptions\n): Promise<GoogleFetchResult> {\n const weights = opts.weights?.length ? opts.weights : [400, 700];\n const italics = opts.italics ?? false;\n const warnings: string[] = [];\n const sources: ResolvedFontSource[] = [];\n\n // Try every (weight, italic) combo — first against caches, then via fetch.\n const wanted: { weight: number; italic: boolean }[] = [];\n for (const w of weights) {\n wanted.push({ weight: w, italic: false });\n if (italics) wanted.push({ weight: w, italic: true });\n }\n\n // Resolve any cache hits first.\n const pending: { weight: number; italic: boolean }[] = [];\n for (const w of wanted) {\n const key = cacheKey(opts.family, w.weight, w.italic);\n const mem = opts.memoryCache?.get(key);\n if (mem) {\n sources.push({\n data: mem,\n weight: w.weight,\n italic: w.italic,\n format: detectFontFormat(mem),\n });\n continue;\n }\n const disk = await opts.diskCache?.get(key);\n if (disk) {\n opts.memoryCache?.set(key, disk);\n sources.push({\n data: disk,\n weight: w.weight,\n italic: w.italic,\n format: detectFontFormat(disk),\n });\n continue;\n }\n pending.push(w);\n }\n\n if (pending.length === 0) {\n return { sources, warnings };\n }\n\n // Single CSS request covers all pending variants.\n const needItalics = pending.some((p) => p.italic);\n const cssUrl = buildCssUrl(\n opts.family,\n Array.from(new Set(pending.map((p) => p.weight))),\n needItalics\n );\n let faces: { weight: number; italic: boolean; ttfUrl: string }[];\n try {\n const cssRes = await fetchWithTimeout(cssUrl, {\n headers: { 'User-Agent': TTF_UA },\n timeoutMs: opts.fetchTimeoutMs,\n fetcher: opts.fetcher,\n });\n if (!cssRes.ok) {\n warnings.push(\n `Google Fonts CSS fetch for \"${opts.family}\" returned ${cssRes.status}`\n );\n return { sources, warnings };\n }\n const css = await cssRes.text();\n faces = parseCssFaces(css);\n } catch (err) {\n warnings.push(\n `Google Fonts CSS fetch for \"${opts.family}\" failed: ${\n (err as Error).message\n }`\n );\n return { sources, warnings };\n }\n\n for (const need of pending) {\n const match = faces.find(\n (f) => f.weight === need.weight && f.italic === need.italic\n );\n if (!match) {\n warnings.push(\n `Google Fonts \"${opts.family}\" missing weight ${need.weight}${\n need.italic ? ' italic' : ''\n }`\n );\n continue;\n }\n try {\n const res = await fetchWithTimeout(match.ttfUrl, {\n timeoutMs: opts.fetchTimeoutMs,\n fetcher: opts.fetcher,\n });\n if (!res.ok) {\n warnings.push(\n `Google Fonts TTF fetch for \"${opts.family}\" ${need.weight} returned ${res.status}`\n );\n continue;\n }\n const ab = await res.arrayBuffer();\n // Metadata validation (weight class, name-table defects, fsType) runs\n // centrally in FontRegistry.materializeEntry so file/data/url/google\n // sources are all checked under one code path.\n const buf = Buffer.from(ab);\n const key = cacheKey(opts.family, need.weight, need.italic);\n opts.memoryCache?.set(key, buf);\n await opts.diskCache?.set(key, buf);\n sources.push({\n data: buf,\n weight: need.weight,\n italic: need.italic,\n format: detectFontFormat(buf),\n });\n } catch (err) {\n warnings.push(\n `Google Fonts TTF fetch for \"${opts.family}\" ${need.weight} failed: ${\n (err as Error).message\n }`\n );\n }\n }\n\n return { sources, warnings };\n}\n","/**\n * Direct-URL font fetcher. Downloads a single TTF/OTF from an HTTPS URL and\n * returns it as a `ResolvedFontSource`. Cache-keyed the same way as the\n * Google Fonts fetcher so fetches are deduplicated across generations.\n *\n * Used as an escape hatch for families whose Google Fonts redistribution has\n * known defects (e.g. Inter's static Thin/ExtraLight shipping with a broken\n * `OS/2.usWeightClass`). The `UPSTREAM_OVERRIDES` catalog points affected\n * families at clean upstream sources like rsms/inter via jsDelivr.\n */\n\nimport type { ResolvedFontSource } from '../types';\nimport { detectFontFormat } from './format';\nimport { isAllowedFontUrl } from './url-allowlist';\n\nexport interface UrlFetchOptions {\n url: string;\n weight: number;\n italic: boolean;\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 cacheKey(url: string, weight: number, italic: boolean): string {\n return `url|${url}|${weight}|${italic ? 'i' : 'r'}`;\n}\n\nexport async function fetchUrlFontSource(\n opts: UrlFetchOptions\n): Promise<{ source?: ResolvedFontSource; warnings?: string[] }> {\n if (!isAllowedFontUrl(opts.url)) {\n return {\n warnings: [\n `URL font fetch rejected (host not in allowlist or non-HTTPS): ${opts.url}`,\n ],\n };\n }\n const key = cacheKey(opts.url, opts.weight, opts.italic);\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 ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), opts.fetchTimeoutMs ?? 10000);\n try {\n const f = opts.fetcher ?? fetch;\n // redirect: 'manual' prevents the allowlist from being bypassed via\n // a 3xx Location pointing at an off-list host. We re-validate the\n // Location header against the allowlist before following.\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) {\n return {\n warnings: [\n `URL font fetch \"${opts.url}\" ${res.status} with no Location header`,\n ],\n };\n }\n const resolved = new URL(next, opts.url).toString();\n if (!isAllowedFontUrl(resolved)) {\n return {\n warnings: [\n `URL font fetch \"${opts.url}\" redirected to disallowed host: ${resolved}`,\n ],\n };\n }\n if (++hops > 3) {\n return {\n warnings: [`URL font fetch \"${opts.url}\" too many redirects`],\n };\n }\n res = await f(resolved, { signal: ctrl.signal, redirect: 'manual' });\n }\n if (!res.ok) {\n return {\n warnings: [`URL font fetch \"${opts.url}\" returned ${res.status}`],\n };\n }\n const ab = await res.arrayBuffer();\n const raw = Buffer.from(ab);\n // Reject obvious non-font responses — e.g. jsDelivr 404 HTML, 200 OK\n // redirect pages, or aliased directory listings. Without this check the\n // bytes would sail through to the embed step and corrupt the output.\n const format = detectFontFormat(raw);\n if (format === 'unknown' || raw.length < 512) {\n return {\n warnings: [\n `URL font fetch \"${opts.url}\" returned ${raw.length} bytes of ${format} — not a TTF/OTF. Skipping.`,\n ],\n };\n }\n // WOFF/WOFF2 flow through: Office output never embeds bytes; the\n // LibreOffice preview stager handles them via fontconfig.\n // Metadata validation runs centrally in FontRegistry.materializeEntry.\n const buf = raw;\n opts.memoryCache?.set(key, buf);\n await opts.diskCache?.set(key, buf);\n return {\n source: {\n data: buf,\n weight: opts.weight,\n italic: opts.italic,\n format,\n },\n warnings: [],\n };\n } catch (err) {\n return {\n warnings: [\n `URL font fetch \"${opts.url}\" failed: ${(err as Error).message}`,\n ],\n };\n } finally {\n clearTimeout(timer);\n }\n}\n","/**\n * Post-fetch metadata validation for TTF/OTF bytes. Catches the three known\n * classes of defects that surface in Google Fonts' redistribution pipeline:\n *\n * 1. Wrong `OS/2.usWeightClass` (Chivo Light, Mada Regular, Petrona)\n * 2. Duplicate usWeightClass across weights (Exo Thin/ExtraLight)\n * 3. Non-unique `name` subfamily records (Inter, Manrope, Recursive)\n *\n * We don't throw on mismatch — the pipeline has already tried to fix the\n * bytes where it can (`rewriteFontSubfamilyNames` after variable-font\n * instancing; `rewriteFontFamilyName` at preview staging). The validator\n * returns human-readable diagnostics so the caller can emit warnings tagged\n * `FONT_METADATA_DEFECT`, pointing users at the upstream override escape\n * hatch before they ship a broken document.\n */\n\nimport { standardSubfamilyNames } from './ttf-name';\n\nconst HEADER_SIZE = 12;\nconst TABLE_RECORD_SIZE = 16;\n\ninterface NameProbe {\n platformID: number;\n nameID: number;\n value: string;\n}\n\nfunction readTable(\n ttf: Buffer,\n tag: string\n): { off: number; len: number } | null {\n if (ttf.length < HEADER_SIZE) return null;\n const version = ttf.readUInt32BE(0);\n if (version !== 0x00010000 && version !== 0x4f54544f) return null;\n const numTables = ttf.readUInt16BE(4);\n for (let i = 0; i < numTables; i++) {\n const r = HEADER_SIZE + i * TABLE_RECORD_SIZE;\n if (r + TABLE_RECORD_SIZE > ttf.length) return null;\n if (ttf.toString('ascii', r, r + 4) === tag) {\n return { off: ttf.readUInt32BE(r + 8), len: ttf.readUInt32BE(r + 12) };\n }\n }\n return null;\n}\n\nfunction readUsWeightClass(ttf: Buffer): number | null {\n const os2 = readTable(ttf, 'OS/2');\n if (!os2) return null;\n if (os2.off + 6 > ttf.length) return null;\n return ttf.readUInt16BE(os2.off + 4);\n}\n\nfunction readNames(ttf: Buffer, wanted: Set<number>): NameProbe[] {\n const nt = readTable(ttf, 'name');\n if (!nt) return [];\n const tableOff = nt.off;\n // Name-table header is 6 bytes (format, count, stringOffset) before the\n // first name record. Reject obviously-truncated tables up front so we\n // don't read count/storageRel past the buffer's end on malformed fonts.\n if (tableOff + 6 > ttf.length) return [];\n const count = ttf.readUInt16BE(tableOff + 2);\n const storageRel = ttf.readUInt16BE(tableOff + 4);\n const storage = tableOff + storageRel;\n const out: NameProbe[] = [];\n for (let j = 0; j < count; j++) {\n const r = tableOff + 6 + j * 12;\n // Each name record is 12 bytes. Stop as soon as the claimed count\n // would walk past the buffer — a malformed font claiming count=999999\n // would otherwise read garbage on each iteration.\n if (r + 12 > ttf.length) break;\n const platformID = ttf.readUInt16BE(r);\n const nameID = ttf.readUInt16BE(r + 6);\n if (!wanted.has(nameID)) continue;\n const length = ttf.readUInt16BE(r + 8);\n const offset = ttf.readUInt16BE(r + 10);\n const raw = ttf.slice(storage + offset, storage + offset + length);\n let value: string;\n if (platformID === 1) {\n value = raw.toString('ascii');\n } else {\n // Decode UTF-16BE. Buffer has no direct utf16be support; swap bytes.\n const swapped = Buffer.from(raw);\n if (swapped.length % 2 === 0) swapped.swap16();\n value = swapped.toString('utf16le');\n }\n out.push({ platformID, nameID, value });\n }\n return out;\n}\n\nexport interface FontMetadataDiagnostic {\n code:\n | 'WEIGHT_CLASS_MISMATCH'\n | 'SUBFAMILY_MISMATCH'\n | 'LEGACY_SUBFAMILY_MISMATCH';\n message: string;\n}\n\n/**\n * Inspect a font's metadata against the weight + italic we asked it to\n * represent. Returns one diagnostic per detected defect.\n */\nexport function validateFontMetadata(\n ttf: Buffer,\n weight: number,\n italic: boolean,\n familyLabel: string\n): FontMetadataDiagnostic[] {\n const diags: FontMetadataDiagnostic[] = [];\n const usWeight = readUsWeightClass(ttf);\n if (usWeight != null && usWeight !== weight) {\n diags.push({\n code: 'WEIGHT_CLASS_MISMATCH',\n message: `Font \"${familyLabel}\" weight ${weight}: OS/2.usWeightClass reports ${usWeight}. Likely a defective redistribution — consider adding an upstream override.`,\n });\n }\n\n // OS/2.fsType (embedding-permission bits) deliberately NOT checked. Office\n // output never embeds font bytes anymore — substitute mode rewrites to\n // SAFE_FONTS, custom mode ships references as-is, and the LibreOffice\n // preview stager only registers bytes transiently with the converter's\n // child process. Permission warnings would be pure noise for every\n // Google Fonts resolution.\n\n const std = standardSubfamilyNames(weight, italic);\n if (!std) return diags;\n const expected17 = std.typographic;\n const expected2 = std.legacy;\n\n const names = readNames(ttf, new Set([2, 17]));\n for (const n of names) {\n if (n.nameID === 17 && n.value !== expected17) {\n diags.push({\n code: 'SUBFAMILY_MISMATCH',\n message: `Font \"${familyLabel}\" weight ${weight}${italic ? ' italic' : ''}: name record (platform ${n.platformID}) nameID 17 = \"${n.value}\", expected \"${expected17}\".`,\n });\n }\n if (n.nameID === 2 && n.value !== expected2) {\n diags.push({\n code: 'LEGACY_SUBFAMILY_MISMATCH',\n message: `Font \"${familyLabel}\" weight ${weight}${italic ? ' italic' : ''}: name record (platform ${n.platformID}) nameID 2 = \"${n.value}\", expected \"${expected2}\".`,\n });\n }\n }\n return diags;\n}\n","/**\n * In-process LRU cache for resolved font buffers.\n * Scoped to a single process — do not share across requests on a server.\n */\n\nexport interface MemoryCacheOptions {\n /** Approximate soft cap in bytes. LRU-evict when exceeded. */\n maxBytes?: number;\n}\n\nexport class FontMemoryCache {\n private readonly store = new Map<string, Buffer>();\n private bytes = 0;\n private readonly maxBytes: number;\n\n constructor(opts: MemoryCacheOptions = {}) {\n this.maxBytes = opts.maxBytes ?? 20 * 1024 * 1024; // 20 MB default\n }\n\n get(key: string): Buffer | undefined {\n const v = this.store.get(key);\n if (!v) return undefined;\n // Refresh LRU position.\n this.store.delete(key);\n this.store.set(key, v);\n return v;\n }\n\n set(key: string, value: Buffer): void {\n const existing = this.store.get(key);\n if (existing) this.bytes -= existing.byteLength;\n this.store.set(key, value);\n this.bytes += value.byteLength;\n while (this.bytes > this.maxBytes && this.store.size > 0) {\n const oldest = this.store.keys().next().value as string | undefined;\n if (!oldest) break;\n const removed = this.store.get(oldest);\n this.store.delete(oldest);\n if (removed) this.bytes -= removed.byteLength;\n }\n }\n\n size(): number {\n return this.store.size;\n }\n}\n","/**\n * FontRegistry — merges catalog + document registry + runtime entries\n * and materializes referenced fonts into ResolvedFont records.\n *\n * Resolution rules, per referenced name:\n * 1. Registry match (by family or id, case-insensitive). Runtime entries win\n * on collision with document entries. Materialize each source.\n * 2. SAFE_FONTS membership → empty sources.\n * 3. Otherwise → empty sources with FONT_UNRESOLVED warning.\n */\n\nimport { isSafeFont } from '../schemas/font-catalog';\nimport type { FontRegistryEntry, FontSource } from '../schemas/font-catalog';\nimport type {\n FontRuntimeOpts,\n ResolvedFont,\n ResolvedFontSource,\n} from './types';\nimport { loadDataFontSource } from './sources/data-loader';\nimport { fetchGoogleFontSources } from './sources/google-fetcher';\nimport { fetchUrlFontSource } from './sources/url-fetcher';\nimport { validateFontMetadata } from './sources/ttf-validate';\nimport { FontMemoryCache } from './cache/memory-cache';\n\n/**\n * Minimal interface the registry needs from a disk cache. The concrete\n * implementation ships in `./cache/disk-cache` but is Node-only (uses fs/crypto).\n * Callers on Node inject an instance; browser callers pass nothing.\n */\nexport interface FontDiskCacheLike {\n get(key: string): Promise<Buffer | undefined>;\n set(key: string, value: Buffer): Promise<void>;\n}\n\n/**\n * Minimal interface for a file-loader. Same reasoning as FontDiskCacheLike:\n * concrete impl is Node-only, callers inject when on Node.\n */\nexport type FontFileLoader = (input: {\n path: string;\n weight?: number;\n italic?: boolean;\n baseDir?: string;\n}) => Promise<ResolvedFontSource>;\n\n/**\n * Minimal interface for the variable-font fetcher. `subset-font` (the\n * harfbuzz-wasm wrapper we use for axis pinning) reaches for `fs` at\n * init time, which crashes in the browser. Injection keeps that import\n * behind the Node-only subpath; browser bundles never pull it in, and\n * browser callers simply won't see `kind: 'variable'` fonts resolved\n * (the registry warns and skips instead).\n */\nexport type FontVariableLoader = (input: {\n url: string;\n weight: number;\n italic: boolean;\n axes?: Record<string, number>;\n fetchTimeoutMs?: number;\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}) => Promise<{ source?: ResolvedFontSource; warnings?: string[] }>;\n\nexport interface FontRegistryInput {\n /** Runtime options — entries come from opts.extraEntries. */\n opts?: FontRuntimeOpts;\n /** Optional disk cache (Node only). Pass an instance of FontDiskCache. */\n diskCache?: FontDiskCacheLike;\n /**\n * Optional `kind: \"file\"` loader (Node only). Inject `loadFileFontSource`\n * from `@json-to-office/shared/fonts/sources/file-loader` on Node. Browser\n * callers pass nothing; `kind: \"file\"` sources then warn and skip.\n */\n fileLoader?: FontFileLoader;\n /**\n * Optional `kind: \"variable\"` loader (Node only). Inject\n * `fetchVariableFontSource` from `@json-to-office/shared/fonts/node` on\n * Node. Browser callers pass nothing; `kind: \"variable\"` sources then\n * warn and skip. Keeping this injected avoids dragging subset-font (and\n * its `fs.promises.readFile` bootstrap) into client bundles.\n */\n variableLoader?: FontVariableLoader;\n}\n\nexport class FontRegistry {\n private readonly index: Map<string, FontRegistryEntry>;\n private readonly cache: Map<string, ResolvedFont>;\n private readonly opts: FontRuntimeOpts;\n private readonly memoryCache: FontMemoryCache;\n private readonly diskCache: FontDiskCacheLike | undefined;\n private readonly fileLoader: FontFileLoader | undefined;\n private readonly variableLoader: FontVariableLoader | undefined;\n\n constructor(input: FontRegistryInput = {}) {\n this.opts = input.opts ?? {};\n this.index = new Map();\n this.cache = new Map();\n this.memoryCache = new FontMemoryCache();\n this.diskCache = input.diskCache;\n this.fileLoader = input.fileLoader;\n this.variableLoader = input.variableLoader;\n\n for (const e of this.opts.extraEntries ?? []) this.addEntry(e);\n }\n\n private addEntry(entry: FontRegistryEntry): void {\n this.index.set(entry.family.toLowerCase(), entry);\n this.index.set(entry.id.toLowerCase(), entry);\n }\n\n /** Resolve every referenced name in one pass. Order preserved. */\n async resolveMany(names: Iterable<string>): Promise<ResolvedFont[]> {\n const out: ResolvedFont[] = [];\n for (const n of names) out.push(await this.resolve(n));\n return out;\n }\n\n async resolve(name: string): Promise<ResolvedFont> {\n const key = name.toLowerCase();\n const cached = this.cache.get(key);\n if (cached) return cached;\n\n const entry = this.index.get(key);\n let result: ResolvedFont;\n\n if (entry) {\n result = await this.materializeEntry(entry);\n } else if (isSafeFont(name)) {\n result = { family: name, sources: [], warnings: [] };\n } else {\n result = {\n family: name,\n sources: [],\n warnings: [\n `Font \"${name}\" is not registered and not in SAFE_FONTS; will rely on host fallback.`,\n ],\n };\n }\n this.cache.set(key, result);\n return result;\n }\n\n private async materializeEntry(\n entry: FontRegistryEntry\n ): Promise<ResolvedFont> {\n const sources: ResolvedFontSource[] = [];\n const warnings: string[] = [];\n\n for (const source of entry.sources) {\n try {\n const materialized = await this.materializeSource(source, warnings);\n for (const s of materialized) {\n if (s.format === 'ttf' || s.format === 'otf') {\n for (const d of validateFontMetadata(\n s.data,\n s.weight,\n s.italic,\n entry.family\n )) {\n warnings.push(`[FONT_METADATA_DEFECT:${d.code}] ${d.message}`);\n }\n }\n sources.push(s);\n }\n } catch (err) {\n warnings.push(\n `Font \"${entry.family}\" source (${source.kind}) failed: ${\n (err as Error).message\n }`\n );\n }\n }\n\n return {\n family: entry.family,\n sources,\n warnings,\n };\n }\n\n private async materializeSource(\n source: FontSource,\n warnings: string[]\n ): Promise<ResolvedFontSource[]> {\n switch (source.kind) {\n case 'safe':\n // System-installed; no embedding data.\n return [];\n case 'file': {\n if (!this.fileLoader) {\n warnings.push(\n `kind:\"file\" source for \"${source.path}\" requires a fileLoader (Node-only); skipping.`\n );\n return [];\n }\n return [\n await this.fileLoader({\n path: source.path,\n weight: source.weight,\n italic: source.italic,\n baseDir: this.opts.baseDir,\n }),\n ];\n }\n case 'data':\n return [\n loadDataFontSource({\n data: source.data,\n weight: source.weight,\n italic: source.italic,\n }),\n ];\n case 'google': {\n const gf = this.opts.googleFonts;\n if (gf?.enabled === false) {\n warnings.push(\n `Google Fonts fetch disabled — skipping \"${source.family}\".`\n );\n return [];\n }\n const { sources: fetched, warnings: fetchWarnings } =\n await fetchGoogleFontSources({\n family: source.family,\n weights: source.weights ?? [400, 700],\n italics: source.italics ?? false,\n memoryCache: this.memoryCache,\n diskCache: this.diskCache,\n fetchTimeoutMs: gf?.fetchTimeoutMs,\n });\n warnings.push(...fetchWarnings);\n return fetched;\n }\n case 'url': {\n const gf = this.opts.googleFonts;\n const { source: fetched, warnings: fetchWarnings } =\n await fetchUrlFontSource({\n url: source.url,\n weight: source.weight ?? 400,\n italic: source.italic ?? false,\n memoryCache: this.memoryCache,\n diskCache: this.diskCache,\n fetchTimeoutMs: gf?.fetchTimeoutMs,\n });\n if (fetchWarnings) warnings.push(...fetchWarnings);\n return fetched ? [fetched] : [];\n }\n case 'variable': {\n // Variable-font instancing: fetch the variable TTF once (disk-\n // cached), then harfbuzz-pin the `wght` axis to produce a clean\n // static for this weight. Requires a Node-injected loader because\n // `subset-font` pulls in `fs` at init; without it, browser\n // bundles would break. Callers on Node pass `fetchVariableFontSource`\n // from `@json-to-office/shared/fonts/node`.\n if (!this.variableLoader) {\n warnings.push(\n `kind:\"variable\" source for \"${source.url}\" requires a variableLoader (Node-only); skipping.`\n );\n return [];\n }\n const gf = this.opts.googleFonts;\n const { source: fetched, warnings: fetchWarnings } =\n await this.variableLoader({\n url: source.url,\n weight: source.weight,\n italic: source.italic ?? false,\n axes: source.axes,\n memoryCache: this.memoryCache,\n diskCache: this.diskCache,\n fetchTimeoutMs: gf?.fetchTimeoutMs,\n });\n if (fetchWarnings) warnings.push(...fetchWarnings);\n return fetched ? [fetched] : [];\n }\n default:\n // Exhaustiveness guard — new kind added to schema without handler\n throw new Error(\n `Unknown font source kind: ${(source as { kind: string }).kind}`\n );\n }\n }\n}\n","/**\n * Curated list of popular Google Fonts for picker autocomplete.\n *\n * Not exhaustive — the full Google Fonts library has ~1500 families.\n * This is ~37 names known to cover most real-world use cases.\n */\n\nexport interface PopularGoogleFont {\n family: string;\n category: 'sans' | 'serif' | 'mono' | 'display' | 'handwriting';\n /** Weights available on Google Fonts for this family. */\n weights: number[];\n /** Whether italic variants exist. */\n hasItalic: boolean;\n}\n\nexport const POPULAR_GOOGLE_FONTS: readonly PopularGoogleFont[] = [\n // Sans-serif\n {\n family: 'Inter',\n category: 'sans',\n weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: false,\n },\n {\n family: 'Roboto',\n category: 'sans',\n weights: [100, 300, 400, 500, 700, 900],\n hasItalic: true,\n },\n {\n family: 'Open Sans',\n category: 'sans',\n weights: [300, 400, 500, 600, 700, 800],\n hasItalic: true,\n },\n {\n family: 'Lato',\n category: 'sans',\n weights: [100, 300, 400, 700, 900],\n hasItalic: true,\n },\n {\n family: 'Montserrat',\n category: 'sans',\n weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'Poppins',\n category: 'sans',\n weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'Work Sans',\n category: 'sans',\n weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'Nunito',\n category: 'sans',\n weights: [200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'DM Sans',\n category: 'sans',\n weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'Rubik',\n category: 'sans',\n weights: [300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'Source Sans 3',\n category: 'sans',\n weights: [200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'Manrope',\n category: 'sans',\n weights: [200, 300, 400, 500, 600, 700, 800],\n hasItalic: false,\n },\n {\n family: 'Plus Jakarta Sans',\n category: 'sans',\n weights: [200, 300, 400, 500, 600, 700, 800],\n hasItalic: true,\n },\n {\n family: 'IBM Plex Sans',\n category: 'sans',\n weights: [100, 200, 300, 400, 500, 600, 700],\n hasItalic: true,\n },\n {\n family: 'Archivo',\n category: 'sans',\n weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'Geist',\n category: 'sans',\n weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'Space Grotesk',\n category: 'sans',\n weights: [300, 400, 500, 600, 700],\n hasItalic: false,\n },\n\n // Serif\n {\n family: 'Playfair Display',\n category: 'serif',\n weights: [400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'Merriweather',\n category: 'serif',\n weights: [300, 400, 700, 900],\n hasItalic: true,\n },\n {\n family: 'Lora',\n category: 'serif',\n weights: [400, 500, 600, 700],\n hasItalic: true,\n },\n {\n family: 'Source Serif 4',\n category: 'serif',\n weights: [200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'DM Serif Display',\n category: 'serif',\n weights: [400],\n hasItalic: true,\n },\n {\n family: 'Crimson Pro',\n category: 'serif',\n weights: [200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'Cormorant Garamond',\n category: 'serif',\n weights: [300, 400, 500, 600, 700],\n hasItalic: true,\n },\n\n // Monospace\n {\n family: 'JetBrains Mono',\n category: 'mono',\n weights: [100, 200, 300, 400, 500, 600, 700, 800],\n hasItalic: true,\n },\n {\n family: 'Fira Code',\n category: 'mono',\n weights: [300, 400, 500, 600, 700],\n hasItalic: false,\n },\n {\n family: 'IBM Plex Mono',\n category: 'mono',\n weights: [100, 200, 300, 400, 500, 600, 700],\n hasItalic: true,\n },\n {\n family: 'Source Code Pro',\n category: 'mono',\n weights: [200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'Space Mono',\n category: 'mono',\n weights: [400, 700],\n hasItalic: true,\n },\n {\n family: 'Geist Mono',\n category: 'mono',\n weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n\n // Display\n {\n family: 'Bebas Neue',\n category: 'display',\n weights: [400],\n hasItalic: false,\n },\n {\n family: 'Abril Fatface',\n category: 'display',\n weights: [400],\n hasItalic: false,\n },\n {\n family: 'Archivo Black',\n category: 'display',\n weights: [400],\n hasItalic: false,\n },\n {\n family: 'Oswald',\n category: 'display',\n weights: [200, 300, 400, 500, 600, 700],\n hasItalic: false,\n },\n\n // Handwriting\n {\n family: 'Caveat',\n category: 'handwriting',\n weights: [400, 500, 600, 700],\n hasItalic: false,\n },\n {\n family: 'Pacifico',\n category: 'handwriting',\n weights: [400],\n hasItalic: false,\n },\n];\n","/**\n * Per-family upstream overrides for popular Google Fonts whose\n * redistribution on fonts.google.com has known defects we can't fix via\n * metadata patching alone.\n *\n * When `autoGoogleFontEntries` hits a family present in this table, it\n * builds override sources instead of issuing `kind: \"google\"` CSS requests.\n * Each entry is either:\n *\n * - `{ kind: \"url\", url, weight, italic? }` — a direct HTTPS TTF/OTF.\n * Use when a clean per-weight static exists on a stable CDN.\n *\n * - `{ kind: \"variable\", url, weight, italic? }` — points at a variable\n * TTF with an `fvar` table. The registry downloads the variable font\n * once and harfbuzz-pins the `wght` axis to the specified weight,\n * producing a clean static TTF. Use when the upstream ships a variable\n * font but no per-weight statics on a CDN (rsms/inter is the\n * canonical example — variable font on jsDelivr, per-weight statics\n * only in GitHub release zips).\n *\n * Pick `variable` over `url` when both are available: the instancer\n * produces per-weight glyph outlines that diverge correctly at every\n * axis value. Google's static redistributions collapse adjacent weights\n * onto the same instance — Inter Thin and ExtraLight both source at\n * ~wght=250 in Google's pipeline, so their static TTFs have 98% identical\n * glyph outlines. Instancing the upstream variable font at exactly wght=100\n * vs wght=200 gives properly distinct geometry.\n *\n * Validate new entries with a HEAD request before adding — the fetchers\n * reject non-TTF responses, but a failed override silently falls back to\n * the Google path, defeating the purpose.\n */\n\n/** One upstream variant source. Type matches the FontSource schema so we\n * can pass the entry directly into `FontRegistry`'s materialize pipeline. */\nexport type UpstreamVariant =\n | {\n kind: 'url';\n url: string;\n weight: number;\n italic?: boolean;\n }\n | {\n kind: 'variable';\n url: string;\n weight: number;\n italic?: boolean;\n /** Extra axis pins merged on top of the derived `wght` pin. */\n axes?: Record<string, number>;\n };\n\nexport interface UpstreamOverride {\n /** Human-readable for logs/diagnostics only. */\n reason: string;\n variants: UpstreamVariant[];\n}\n\n/**\n * rsms/inter publishes the upright variable master as `InterVariable.ttf`\n * on jsDelivr, but the italic master ONLY as `InterVariable-Italic.woff2`\n * — no italic `.ttf` exists under `docs/font-files/` at any tag, so a\n * `.ttf` italic URL 404s and every italic Inter run silently falls back\n * to host defaults. The variable fetcher accepts woff2 sources (fontverter\n * converts to sfnt before instancing), so point at the woff2 directly.\n *\n * We instance each master at every advertised weight so Inter Thin (100),\n * ExtraLight (200), Light (300), Medium (500), SemiBold (600), ExtraBold\n * (800), and Black (900) come out with distinct glyph outlines instead of\n * the near-duplicates Google's static redistribution ships. Regular (400)\n * and Bold (700) from Google were already clean, but instancing them from\n * the same variable font keeps the full family visually consistent.\n *\n * Version pin: `@v4.1` — the last stable rsms/inter release at the time\n * of writing. jsDelivr caches the file aggressively; a version bump here\n * invalidates that cache for users on a subsequent generate.\n */\nconst INTER_VARIABLE_URL =\n 'https://cdn.jsdelivr.net/gh/rsms/inter@v4.1/docs/font-files/InterVariable.ttf';\nconst INTER_VARIABLE_ITALIC_URL =\n 'https://cdn.jsdelivr.net/gh/rsms/inter@v4.1/docs/font-files/InterVariable-Italic.woff2';\n\nfunction interVariants(): UpstreamVariant[] {\n const weights = [100, 200, 300, 400, 500, 600, 700, 800, 900];\n const upright = weights.map((weight) => ({\n kind: 'variable' as const,\n url: INTER_VARIABLE_URL,\n weight,\n italic: false,\n }));\n const italic = weights.map((weight) => ({\n kind: 'variable' as const,\n url: INTER_VARIABLE_ITALIC_URL,\n weight,\n italic: true,\n }));\n return [...upright, ...italic];\n}\n\nexport const UPSTREAM_OVERRIDES: Record<string, UpstreamOverride> = {\n inter: {\n reason:\n \"Google's static Inter Thin/ExtraLight both carry usWeightClass=250 and near-identical glyph outlines (xAvgCharWidth differs by 1.8%); instancing the upstream variable font per weight produces properly distinct statics.\",\n variants: interVariants(),\n },\n};\n\n/** Case-insensitive lookup. Returns undefined when the family has no override. */\nexport function getUpstreamOverride(\n family: string\n): UpstreamOverride | undefined {\n return UPSTREAM_OVERRIDES[family.toLowerCase()];\n}\n","/**\n * Font family substitution: rewrite every non-safe family reference in\n * the doc tree + theme to a SAFE_FONTS equivalent. Used by the\n * `'substitute'` export mode (`FontRuntimeOpts.mode`) so that non-safe\n * fonts (Playfair Display, Inter, …) ship as Georgia/Calibri and the\n * document renders identically on every recipient machine — no embed\n * bytes, no Word-for-Mac intermediate-weight surprises.\n *\n * The walker mirrors the shape used by `collectFontNamesFromDocx/Pptx`\n * so the two stay in sync: whatever `collect` scans, `rewrite` will\n * rewrite. Future component-schema additions that introduce new font\n * keys go in `FONT_NAME_KEYS` / `THEME_FONT_KEYS` once, both sides pick\n * them up.\n */\n\nimport { SAFE_FONTS, isSafeFont } from '../schemas/font-catalog';\nimport { POPULAR_GOOGLE_FONTS } from './catalog/popular-google';\nimport {\n FONT_NAME_KEYS,\n THEME_FONT_KEYS,\n FONT_DECLARATION_KEYS,\n} from './collect';\n\n/** One swap recorded during a rewrite. */\nexport interface FontSubstitution {\n from: string;\n to: string;\n}\n\nexport interface ApplyFontSubstitutionResult<T> {\n doc: T;\n substitutions: FontSubstitution[];\n}\n\n/**\n * Walk a doc tree + swap every non-safe family reference per `mapping`.\n * Returns a new tree (structural clone) plus the list of `(from, to)`\n * swaps made, deduped by source name.\n *\n * One deliberate exception to the clone: `fontRegistry` subtrees are carried\n * through by reference, since they declare fonts rather than reference them\n * and nothing downstream mutates them.\n *\n * Families already in SAFE_FONTS are never rewritten (even if a mapping\n * entry targets them as a key — safe fonts don't need substitution).\n * Families with no mapping entry are left untouched — callers should\n * feed the result of `buildDefaultSubstitutionMap` to ensure every\n * non-safe reference gets a fallback.\n */\nexport function applyFontSubstitution<T>(\n doc: T,\n mapping: Record<string, string>\n): ApplyFontSubstitutionResult<T> {\n const seen = new Map<string, string>();\n const rewritten = rewrite(doc, mapping, seen) as T;\n const substitutions: FontSubstitution[] = [];\n for (const [from, to] of seen) substitutions.push({ from, to });\n return { doc: rewritten, substitutions };\n}\n\nfunction rewrite(\n node: unknown,\n mapping: Record<string, string>,\n seen: Map<string, string>,\n parentKey?: string\n): unknown {\n if (node == null) return node;\n\n if (typeof node === 'string') {\n if (\n parentKey &&\n (FONT_NAME_KEYS.has(parentKey) || THEME_FONT_KEYS.has(parentKey))\n ) {\n return maybeSwap(node, mapping, seen);\n }\n return node;\n }\n\n if (Array.isArray(node)) {\n return node.map((item) => rewrite(item, mapping, seen, parentKey));\n }\n\n if (typeof node === 'object') {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(node as Record<string, unknown>)) {\n // Mirror of collect.ts: a fontRegistry subtree declares families, it\n // does not reference them. Rewriting `entry.family` would rename the\n // registration out from under every reference and key an entry on a\n // SAFE_FONTS name. Carry it through by reference (see the clone note\n // on applyFontSubstitution).\n if (FONT_DECLARATION_KEYS.has(k)) {\n out[k] = v;\n continue;\n }\n // Parallel to collect.ts: `theme.fonts` may hold plain strings\n // keyed by heading/body/mono/light. Those strings are font names\n // and need swapping just like `font.family` does.\n if (\n parentKey === 'theme' &&\n k === 'fonts' &&\n v &&\n typeof v === 'object'\n ) {\n const nextFonts: Record<string, unknown> = {};\n for (const [fk, fv] of Object.entries(v as Record<string, unknown>)) {\n if (typeof fv === 'string') {\n nextFonts[fk] = maybeSwap(fv, mapping, seen);\n } else if (fv && typeof fv === 'object') {\n const fam = (fv as Record<string, unknown>).family;\n nextFonts[fk] =\n typeof fam === 'string'\n ? { ...(fv as object), family: maybeSwap(fam, mapping, seen) }\n : rewrite(fv, mapping, seen, fk);\n } else {\n nextFonts[fk] = fv;\n }\n }\n out[k] = nextFonts;\n continue;\n }\n out[k] = rewrite(v, mapping, seen, k);\n }\n return out;\n }\n\n return node;\n}\n\nfunction maybeSwap(\n name: string,\n mapping: Record<string, string>,\n seen: Map<string, string>\n): string {\n const trimmed = name.trim();\n if (trimmed.length === 0) return name;\n if (isSafeFont(trimmed)) return name;\n // Case-insensitive mapping lookup. Users may feed mappings with\n // slightly different casing than the doc's reference (e.g. \"inter\" vs\n // \"Inter\") — honour the target verbatim, don't force case.\n const lowered = trimmed.toLowerCase();\n for (const [from, to] of Object.entries(mapping)) {\n if (from.toLowerCase() === lowered) {\n seen.set(trimmed, to);\n return to;\n }\n }\n return name;\n}\n\n// ---------------------------------------------------------------------------\n// Default-mapping builder\n// ---------------------------------------------------------------------------\n\n/**\n * Explicit overrides for the most common non-safe fonts we see. Chosen\n * for visual similarity: sans serifs map to Calibri, serifs to Georgia\n * or Cambria depending on axis proportion, monospace to Consolas.\n * Extendable as real usage surfaces more families.\n */\nconst EXPLICIT_OVERRIDES: Record<string, string> = {\n Inter: 'Calibri',\n Roboto: 'Calibri',\n 'Open Sans': 'Calibri',\n Lato: 'Calibri',\n 'Source Sans 3': 'Calibri',\n 'Source Sans Pro': 'Calibri',\n 'IBM Plex Sans': 'Calibri',\n Archivo: 'Calibri',\n Geist: 'Calibri',\n 'Space Grotesk': 'Calibri',\n 'Work Sans': 'Calibri',\n Manrope: 'Calibri',\n Nunito: 'Calibri',\n 'Nunito Sans': 'Calibri',\n Poppins: 'Calibri',\n Montserrat: 'Calibri',\n 'Playfair Display': 'Georgia',\n Merriweather: 'Georgia',\n 'Source Serif 4': 'Cambria',\n 'Source Serif Pro': 'Cambria',\n 'IBM Plex Serif': 'Cambria',\n 'Crimson Pro': 'Cambria',\n Lora: 'Georgia',\n 'PT Serif': 'Georgia',\n 'Cormorant Garamond': 'Cambria',\n 'JetBrains Mono': 'Consolas',\n 'Fira Code': 'Consolas',\n 'IBM Plex Mono': 'Consolas',\n 'Source Code Pro': 'Consolas',\n 'Roboto Mono': 'Consolas',\n 'Geist Mono': 'Consolas',\n};\n\nconst CATEGORY_FALLBACK: Record<string, string> = {\n sans: 'Calibri',\n serif: 'Georgia',\n mono: 'Consolas',\n display: 'Georgia',\n handwriting: 'Segoe UI',\n};\n\n/**\n * Pick the safe-font fallback for a single non-safe family. Precedence:\n * 1. Explicit override in `EXPLICIT_OVERRIDES`.\n * 2. Category lookup in `POPULAR_GOOGLE_FONTS`.\n * 3. Final default (`Calibri`).\n *\n * Exposed for the playground dialog so it can pre-populate the per-family\n * picker with the same defaults the CLI would apply.\n */\nexport function defaultSubstituteFor(family: string): string {\n const trimmed = family.trim();\n // Explicit (case-insensitive).\n for (const [from, to] of Object.entries(EXPLICIT_OVERRIDES)) {\n if (from.toLowerCase() === trimmed.toLowerCase()) return to;\n }\n // Category.\n const catalog = POPULAR_GOOGLE_FONTS.find(\n (f) => f.family.toLowerCase() === trimmed.toLowerCase()\n );\n if (catalog) {\n const cat = CATEGORY_FALLBACK[catalog.category];\n if (cat) return cat;\n }\n return 'Calibri';\n}\n\n/**\n * Build a substitution map for every non-safe family in `referencedNames`.\n * Safe fonts are omitted from the result since they don't need swapping.\n * Caller can override individual entries before passing to\n * `applyFontSubstitution`.\n */\nexport function buildDefaultSubstitutionMap(\n referencedNames: Iterable<string>\n): Record<string, string> {\n const out: Record<string, string> = {};\n for (const raw of referencedNames) {\n const name = raw.trim();\n if (name.length === 0) continue;\n if (isSafeFont(name)) continue;\n if (out[name]) continue;\n out[name] = defaultSubstituteFor(name);\n }\n return out;\n}\n\n/** Re-export SAFE_FONTS for dialog/CLI consumers that need the allowlist. */\nexport { SAFE_FONTS };\n\n// ---------------------------------------------------------------------------\n// Export-mode pre-pass\n// ---------------------------------------------------------------------------\n\nimport type { FontRuntimeOpts } from './types';\nimport { collectFontNames } from './collect';\n\nexport interface ApplyExportModeInput<D, T> {\n doc: D;\n theme: T;\n fonts?: FontRuntimeOpts;\n}\n\nexport interface ApplyExportModeWarning {\n code: 'FONT_MODE_CUSTOM' | 'FONT_MODE_SUBSTITUTED';\n message: string;\n}\n\nexport interface ApplyExportModeResult<D, T> {\n doc: D;\n theme: T;\n warnings: ApplyExportModeWarning[];\n}\n\n/**\n * Inspect `fonts.mode` and apply the pre-resolution rewrite for the\n * requested mode.\n *\n * - `'custom'` (default) — no rewrite. Font references stay as authored;\n * recipients need the font installed or Word falls back. The\n * LibreOffice preview stager registers resolved bytes so preview\n * fidelity matches the recipient-side experience when the font is\n * installed.\n * - `'substitute'` — rewrite every non-safe family in doc + theme to its\n * mapped safe equivalent. Fills in defaults via\n * `buildDefaultSubstitutionMap` for any non-safe reference not present\n * in `fonts.substitution`. Emits one `FONT_MODE_SUBSTITUTED` warning\n * listing every swap.\n */\nexport function applyExportMode<D, T>(\n input: ApplyExportModeInput<D, T>\n): ApplyExportModeResult<D, T> {\n const mode = input.fonts?.mode ?? 'custom';\n if (mode === 'custom') {\n // Suppress the advisory entirely when callers never passed a `fonts`\n // option: they opted out of font-mode handling, so noisy per-run\n // warnings would flood existing callers that predate the pipeline.\n if (!input.fonts) {\n return { doc: input.doc, theme: input.theme, warnings: [] };\n }\n // Only emit the advisory when the doc/theme actually references a\n // non-safe family — a safe-only doc has nothing for recipients to be\n // missing, and the warning would be noise.\n const referenced = new Set<string>([\n ...collectFontNames(input.doc),\n ...collectFontNames(input.theme),\n ]);\n const nonSafe = [...referenced].filter((name) => !isSafeFont(name.trim()));\n const warnings: ApplyExportModeWarning[] =\n nonSafe.length > 0\n ? [\n {\n code: 'FONT_MODE_CUSTOM',\n message: `Export mode \"custom\": non-safe font references (${nonSafe.join(', ')}) kept as-is. Recipients need these fonts installed locally; Word falls back to a generic substitute otherwise.`,\n },\n ]\n : [];\n return {\n doc: input.doc,\n theme: input.theme,\n warnings,\n };\n }\n // mode === 'substitute'\n const referenced = new Set<string>([\n ...collectFontNames(input.doc),\n ...collectFontNames(input.theme),\n ]);\n const defaults = buildDefaultSubstitutionMap(referenced);\n const mapping: Record<string, string> = {\n ...defaults,\n ...(input.fonts?.substitution ?? {}),\n };\n\n const docRewrite = applyFontSubstitution(input.doc, mapping);\n const themeRewrite = applyFontSubstitution(input.theme, mapping);\n const combined = new Map<string, string>();\n for (const s of docRewrite.substitutions) combined.set(s.from, s.to);\n for (const s of themeRewrite.substitutions) combined.set(s.from, s.to);\n\n const warnings: ApplyExportModeWarning[] = [];\n if (combined.size > 0) {\n const list = [...combined]\n .map(([from, to]) => `${from} → ${to}`)\n .join(', ');\n warnings.push({\n code: 'FONT_MODE_SUBSTITUTED',\n message: `Export mode \"substitute\": rewrote non-safe families to safe equivalents — ${list}. No fonts embedded; document renders identically on every machine.`,\n });\n }\n return {\n doc: docRewrite.doc,\n theme: themeRewrite.doc,\n warnings,\n };\n}\n","/**\n * Default series-color tokens for charts. Single source of truth for every\n * format: the PPTX `chart` and `highcharts` components and the DOCX\n * `highcharts` component all resolve this list, in this order, against the\n * active theme when the author sets no explicit colors. Both theme schemas\n * declare all six tokens (accent4-6 optional in each), so a theme that fills\n * every slot produces the same palette in a deck and in a document.\n *\n * Slots the theme leaves unset are skipped in both formats: the implicit\n * palette shrinks and the chart library cycles the shorter list rather than\n * repeating `primary` for every empty slot. A theme carrying only\n * primary/secondary/accent — which is what the bundled DOCX themes carry —\n * therefore paints series 4+ identically in a deck and in a document.\n *\n * Skipping compacts holes: a theme defining accent5 but not accent4 yields\n * [primary, secondary, accent, accent5], so accent5 paints series 4. The list\n * is a preference-ordered pool of candidate colors, not fixed per-series slots,\n * so keeping a color the theme did define beats dropping or duplicating one.\n *\n * A slot may also hold another token's name (`\"accent4\": \"primary\"`) — both\n * theme schemas allow it — and both formats walk that reference to hex before\n * using it, so a chained slot lands on the same color in a deck as in a\n * document. A slot whose value reaches no hex (`\"accent4\": \"nonsense\"`, or a\n * reference cycle) is dropped from the implicit palette in both formats rather\n * than emitted verbatim: PowerPoint and Highcharts both answer an unparseable\n * color with silent black. Parity here covers the token names the two schemas\n * share; each format also has private color keys (DOCX `textSecondary`, PPTX\n * `text2`) that only resolve in their own format.\n *\n * Only the implicit palette skips. An author who names a token explicitly\n * (PPTX `chartColors: ['accent4']`) still gets the `primary` fallback and a\n * warning — naming an undefined token is an authoring error and stays loud.\n * PPTX warns THEME_COLOR_FALLBACK for an unset slot and UNKNOWN_COLOR for one\n * holding an unresolvable value; DOCX throws.\n */\nexport const DEFAULT_CHART_THEME_COLORS = [\n 'primary',\n 'secondary',\n 'accent',\n 'accent4',\n 'accent5',\n 'accent6',\n];\n","/**\n * Deep Merge Utilities\n * Generic deep-merge helpers used by both docx and pptx\n * componentDefaults resolution systems.\n */\n\nfunction isObject(item: any): boolean {\n return item !== null && typeof item === 'object' && !Array.isArray(item);\n}\n\nfunction deepMerge<T>(target: any, source: any): T {\n const output = { ...target };\n\n if (isObject(target) && isObject(source)) {\n Object.keys(source).forEach((key) => {\n if (isObject(source[key])) {\n if (!(key in target)) {\n output[key] = source[key];\n } else {\n output[key] = deepMerge(target[key], source[key]);\n }\n } else {\n output[key] = source[key];\n }\n });\n }\n\n return output as T;\n}\n\n/**\n * Merge theme defaults with user-provided configuration.\n * User config takes precedence over theme defaults.\n * Uses deep merge to preserve nested objects.\n * Arrays are replaced wholesale, not merged per-element.\n */\nexport function mergeWithDefaults<T>(\n userConfig: T,\n themeDefaults: Partial<T>\n): T {\n return deepMerge<T>(themeDefaults, userConfig);\n}\n"],"mappings":"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","names":["cacheKey","referenced","warnings"]}
1
+ {"version":3,"sources":["../src/types/services.ts","../src/fonts/collect.ts","../src/fonts/document-registry.ts","../src/fonts/validator.ts","../src/fonts/synthesize.ts","../src/fonts/sources/data-loader.ts","../src/fonts/sources/google-fetcher.ts","../src/fonts/sources/url-fetcher.ts","../src/fonts/sources/ttf-validate.ts","../src/fonts/sources/ttf-structure.ts","../src/fonts/cache/memory-cache.ts","../src/fonts/registry.ts","../src/fonts/catalog/popular-google.ts","../src/fonts/catalog/upstream-overrides.ts","../src/fonts/substitute.ts","../src/theme/chart-palette.ts","../src/utils/deepMerge.ts"],"sourcesContent":["/**\n * Service configuration types for external integrations (e.g. Highcharts export server)\n */\n\n// ============================================================================\n// Visual rasterization policy — single source of truth for DPI bounds shared\n// by the visual schema, the in-process rasterizer, the flatten transform, and\n// both HTTP /rasterize surfaces. Keep these in sync in one place.\n// ============================================================================\n\n/** Default raster resolution when a `visual` does not specify one. */\nexport const DEFAULT_VISUAL_DPI = 200;\n/** Minimum accepted raster resolution. */\nexport const MIN_VISUAL_DPI = 36;\n/** Maximum accepted raster resolution (bounds bitmap size / DoS surface). */\nexport const MAX_VISUAL_DPI = 600;\n\n/** Clamp an arbitrary dpi to [MIN_VISUAL_DPI, MAX_VISUAL_DPI]; non-finite → default. */\nexport function clampVisualDpi(dpi: unknown): number {\n if (typeof dpi !== 'number' || !Number.isFinite(dpi))\n return DEFAULT_VISUAL_DPI;\n return Math.min(MAX_VISUAL_DPI, Math.max(MIN_VISUAL_DPI, Math.round(dpi)));\n}\n\nexport type HighchartsHeaders = Record<string, string>;\n\nexport type HighchartsHeadersResolver = (\n body: unknown\n) => HighchartsHeaders | Promise<HighchartsHeaders>;\n\nexport interface HighchartsServiceConfig {\n serverUrl?: string;\n headers?: HighchartsHeaders | HighchartsHeadersResolver;\n}\n\n// ============================================================================\n// PPTX rasterization service (used by the docx `visual` component)\n// ============================================================================\n\nexport type PptxServiceHeaders = Record<string, string>;\n\nexport type PptxServiceHeadersResolver = (\n body: unknown\n) => PptxServiceHeaders | Promise<PptxServiceHeaders>;\n\n/** Max font faces accepted in one rasterize request. */\nexport const MAX_RASTERIZE_FONTS = 32;\n/** Max total DECODED font bytes accepted in one rasterize request. */\nexport const MAX_RASTERIZE_FONT_BYTES = 8 * 1024 * 1024;\n\n/**\n * One font face shipped alongside a rasterize request so the rasterizer's\n * out-of-process LibreOffice can render the slide with the document's real\n * fonts instead of a system fallback.\n *\n * `data` is base64 of the raw font file — NO `data:` URI prefix — so the\n * request stays plain serializable JSON.\n *\n * `family` is the CATALOG family (`ResolvedFont.family`, e.g. \"Inter\"), not\n * the synthesized sub-family the presentation references. The receiving\n * stager applies `synthesizeFamilyName` + `rewriteFontFamilyName` itself,\n * exactly as it does for the PDF-preview path; pre-synthesizing here would\n * double-apply the suffix (\"Inter Light Light\").\n */\nexport interface RasterizeFontFace {\n family: string;\n weight: number;\n italic: boolean;\n /** Base64-encoded font file bytes (no `data:` prefix). */\n data: string;\n format?: 'ttf' | 'otf' | 'woff' | 'woff2';\n}\n\n/**\n * Request handed to a pptx rasterizer: a single-slide pptx presentation\n * component definition plus the target resolution.\n */\nexport interface PptxRasterizeRequest {\n /** A pptx presentation component definition ({ name: 'pptx', ... }) with one slide */\n presentation: unknown;\n /** Target raster resolution in dots-per-inch */\n dpi: number;\n /**\n * Directory that relative asset paths inside the presentation resolve\n * against — the originating document's own directory. Absent → the\n * rasterizer's cwd, the legacy behavior (#142).\n */\n baseDir?: string;\n /**\n * Font faces to stage for the rasterizer's LibreOffice launch. Absent →\n * system fonts only, which is what every non-font-aware caller (and every\n * pre-Area-6 client) sends.\n */\n fonts?: RasterizeFontFace[];\n}\n\n/**\n * Result returned by a pptx rasterizer.\n */\nexport interface PptxRasterizeResult {\n /** Rendered PNG as a base64 data URI (data:image/png;base64,...) */\n base64DataUri: string;\n /** Natural pixel width of the rendered image */\n width: number;\n /** Natural pixel height of the rendered image */\n height: number;\n}\n\n/**\n * In-process rasterizer callback. Implementations build the .pptx from the\n * presentation JSON and rasterize it to a PNG (e.g. via LibreOffice + poppler).\n */\nexport type PptxRasterizer = (\n request: PptxRasterizeRequest\n) => Promise<PptxRasterizeResult>;\n\n// ============================================================================\n// Batch rasterization (#153) — one request rasterizes many independent slides.\n//\n// Each slide is a complete single-slide presentation (the exact shape a\n// single {@link PptxRasterizeRequest} carries), NOT a slide fragment of one\n// merged deck. This keeps slides independent — each may use its own canvas\n// size, theme, and dpi, so callers never need to group visuals — and lets\n// implementations key per-slide caches identically to the single-slide path.\n// ============================================================================\n\n/**\n * Maximum slides accepted in one batch request. Shared by the HTTP surface\n * (request validation) and clients (chunk size) so the two cannot drift.\n * Bounds per-request work and response size the same way rate limits bound\n * request counts.\n */\nexport const MAX_RASTERIZE_BATCH_SLIDES = 32;\n\n/** One slide in a batch: a single-slide presentation plus its resolution. */\nexport interface PptxRasterizeBatchSlide {\n /** A pptx presentation component definition ({ name: 'pptx', ... }) with one slide */\n presentation: unknown;\n /** Target raster resolution in dots-per-inch (absent → service default) */\n dpi?: number;\n}\n\n/** Request handed to a batch pptx rasterizer. */\nexport interface PptxRasterizeBatchRequest {\n /** Slides to rasterize; results come back index-aligned with this array. */\n slides: PptxRasterizeBatchSlide[];\n /** Base directory for relative asset paths, shared by every slide (#142). */\n baseDir?: string;\n /**\n * Font faces staged for the batch's LibreOffice launch, shared by every\n * slide exactly like `baseDir`. Deliberately REQUEST-level and never\n * per-slide: {@link PptxRasterizeBatchSlide} stays `{presentation, dpi}` so\n * batch-internal dedupe and the per-slide disk-cache key stay uniform with\n * the single-slide path.\n */\n fonts?: RasterizeFontFace[];\n}\n\n/**\n * Pipeline stage a slide failed in. `build` failures are caused by the\n * slide's own JSON (safe to surface verbatim to callers); `convert` and\n * `rasterize` failures are environment/tooling errors whose raw messages may\n * carry host paths — HTTP surfaces sanitize those.\n */\nexport type PptxRasterizeFailureStage = 'build' | 'convert' | 'rasterize';\n\n/**\n * Per-slide outcome. A batch response is 200-with-item-errors rather than\n * all-or-nothing: one bad visual must not discard its siblings' pixels.\n */\nexport type PptxRasterizeBatchSlideResult =\n | ({ ok: true } & PptxRasterizeResult)\n | { ok: false; error: string; stage?: PptxRasterizeFailureStage };\n\n/** Result returned by a batch pptx rasterizer. */\nexport interface PptxRasterizeBatchResult {\n /** Index-aligned with the request's `slides` (same length, same order). */\n results: PptxRasterizeBatchSlideResult[];\n}\n\n/**\n * In-process batch rasterizer callback. Batch-level failures (missing\n * binaries, bad request) throw; per-slide failures land in `results`.\n */\nexport type PptxBatchRasterizer = (\n request: PptxRasterizeBatchRequest\n) => Promise<PptxRasterizeBatchResult>;\n\n/**\n * Configuration for the pptx rasterization service backing `visual` components.\n *\n * Mirrors {@link HighchartsServiceConfig}: the published packages depend on this\n * interface, never on a binary. A host injects either an in-process `render`\n * callback or an HTTP `serverUrl`.\n */\nexport interface PptxServiceConfig {\n /**\n * In-process rasterizer. Takes precedence over `serverUrl` when provided.\n * Ideal for tests (no binaries) and single-process hosts.\n */\n render?: PptxRasterizer;\n /**\n * In-process batch rasterizer. When provided, the docx renderer coalesces a\n * document's visuals into batch calls (#153) instead of one `render` call\n * per visual. Like `render`, takes precedence over `serverUrl`.\n */\n renderBatch?: PptxBatchRasterizer;\n /**\n * HTTP rasterization service URL. The service receives\n * `{ presentation, dpi }` and returns a {@link PptxRasterizeResult}.\n */\n serverUrl?: string;\n /** Optional headers (or async resolver) for the HTTP service. */\n headers?: PptxServiceHeaders | PptxServiceHeadersResolver;\n /** Default DPI applied when a `visual` does not specify one. */\n dpi?: number;\n}\n\nexport interface ServicesConfig {\n highcharts?: HighchartsServiceConfig;\n pptx?: PptxServiceConfig;\n}\n","/**\n * Walk a document tree and collect every font family name referenced.\n *\n * Matches both DOCX conventions (`family` nested under `font`, theme.fonts.*)\n * and PPTX conventions (`fontFace`, chart `titleFontFace` etc., theme.fonts.*).\n * Key-name matching is intentionally permissive so future component schemas\n * that reuse the same conventions pick up automatically.\n */\n\nexport const FONT_NAME_KEYS = new Set([\n 'family',\n 'fontFace',\n 'titleFontFace',\n 'legendFontFace',\n 'dataLabelFontFace',\n 'catAxisLabelFontFace',\n 'valAxisLabelFontFace',\n]);\n\nexport const THEME_FONT_KEYS = new Set(['heading', 'body', 'mono', 'light']);\n\n/**\n * Subtrees that declare fonts rather than reference them. `fontRegistry`\n * entries carry `family` (and `sources[].family` for kind:'safe'/'google'),\n * which the depth-agnostic `family` match would otherwise scoop up as\n * references — a registry would self-satisfy its own validation, and a\n * `kind:'google'` source family would appear as a phantom reference.\n * substitute.ts's rewriter skips the same key; the two MUST stay in sync.\n */\nexport const FONT_DECLARATION_KEYS = new Set(['fontRegistry']);\n\nfunction collect(node: unknown, out: Set<string>, parentKey?: string): void {\n if (node == null) return;\n\n if (typeof node === 'string') {\n // String values under a font-name key or under theme.fonts.{heading,body,...}\n if (\n parentKey &&\n (FONT_NAME_KEYS.has(parentKey) || THEME_FONT_KEYS.has(parentKey))\n ) {\n const trimmed = node.trim();\n if (trimmed.length > 0) out.add(trimmed);\n }\n return;\n }\n\n if (Array.isArray(node)) {\n // Forward parentKey so font-name arrays (e.g. theme.fonts.heading: [...])\n // are walked with the same context as the non-array case. Mirrors\n // substitute.ts's rewrite walker — the two must stay in sync or\n // substitution silently misses array-shaped references.\n for (const item of node) collect(item, out, parentKey);\n return;\n }\n\n if (typeof node === 'object') {\n // Special-cased: theme.fonts is an object whose values (or values.family) are font names.\n const maybeFonts = (node as Record<string, unknown>).fonts;\n if (parentKey === 'theme' && maybeFonts && typeof maybeFonts === 'object') {\n for (const [k, v] of Object.entries(\n maybeFonts as Record<string, unknown>\n )) {\n if (typeof v === 'string') {\n const trimmed = v.trim();\n if (trimmed.length > 0) out.add(trimmed);\n } else if (v && typeof v === 'object') {\n const fam = (v as Record<string, unknown>).family;\n if (typeof fam === 'string' && fam.trim().length > 0) {\n out.add(fam.trim());\n }\n }\n void k;\n }\n }\n\n for (const [k, v] of Object.entries(node as Record<string, unknown>)) {\n // Declarations, not references — see FONT_DECLARATION_KEYS.\n if (FONT_DECLARATION_KEYS.has(k)) continue;\n collect(v, out, k);\n }\n }\n}\n\n/** Scan an arbitrary doc tree (DOCX or PPTX) for every font family referenced. */\nexport function collectFontNames(doc: unknown): Set<string> {\n const out = new Set<string>();\n collect(doc, out);\n return out;\n}\n\n/** Scan a DOCX document tree for every font family name referenced. */\nexport const collectFontNamesFromDocx = collectFontNames;\n\n/** Scan a PPTX presentation tree for every font family name referenced. */\nexport const collectFontNamesFromPptx = collectFontNames;\n","/**\n * Read the document-scoped and theme-scoped font registries and merge them\n * with runtime `fonts.extraEntries`.\n *\n * Precedence (last wins, matching registry.ts's documented resolution rules\n * and FontRuntimeOpts.extraEntries's \"merged over the document's\n * fontRegistry\"):\n *\n * theme.fontRegistry < document.props.fontRegistry < fonts.extraEntries\n *\n * Merging happens here rather than inside FontRegistry because\n * `validateFontReferences` needs the same merged list, and two merge sites\n * would eventually disagree — which would show up as a font that validates\n * but never renders, or vice versa.\n */\n\nimport type { FontRegistryEntry } from '../schemas/font-catalog';\n\nfunction isEntry(v: unknown): v is FontRegistryEntry {\n if (!v || typeof v !== 'object') return false;\n const e = v as Record<string, unknown>;\n return (\n typeof e.id === 'string' &&\n typeof e.family === 'string' &&\n Array.isArray(e.sources)\n );\n}\n\nfunction readAt(node: unknown, key: string): FontRegistryEntry[] {\n if (!node || typeof node !== 'object') return [];\n const raw = (node as Record<string, unknown>)[key];\n return Array.isArray(raw) ? raw.filter(isEntry) : [];\n}\n\n/** `document.props.fontRegistry`, defensively (props may be absent/null). */\nexport function documentFontRegistry(document: unknown): FontRegistryEntry[] {\n if (!document || typeof document !== 'object') return [];\n return readAt((document as Record<string, unknown>).props, 'fontRegistry');\n}\n\n/** `theme.fontRegistry`, defensively. */\nexport function themeFontRegistry(theme: unknown): FontRegistryEntry[] {\n return readAt(theme, 'fontRegistry');\n}\n\n/**\n * Merge entry groups in precedence order — later groups win on a collision of\n * `family` OR `id`, case-insensitively, which are the same two keys\n * `FontRegistry.addEntry` indexes on. Returns a flat list safe to hand to both\n * `validateFontReferences` and `new FontRegistry({ opts: { extraEntries } })`.\n */\nexport function mergeFontRegistries(\n ...groups: (FontRegistryEntry[] | undefined)[]\n): FontRegistryEntry[] {\n // Order IS the contract: `FontRegistry` replays this array through\n // `addEntry`, indexing on family and id with last-write-wins, so an entry\n // only outranks another by sitting later. Concatenating the groups in\n // precedence order is therefore the whole mechanism.\n //\n // Deliberately NOT de-duped through a Map keyed by family/id: `Map.set` on\n // an existing key keeps the original insertion *position*, so a\n // higher-precedence entry colliding with an earlier one would be emitted\n // early and then lose the replay to the entry it was supposed to beat.\n const out: FontRegistryEntry[] = [];\n for (const group of groups) {\n for (const entry of group ?? []) {\n const family = entry.family.toLowerCase();\n const id = entry.id.toLowerCase();\n // Drop only a true replacement — same family AND same id. An entry that\n // shares just one of the two still owns the other key in FontRegistry's\n // index, so removing it here would make that name unresolvable.\n for (let i = out.length - 1; i >= 0; i--) {\n if (\n out[i].family.toLowerCase() === family &&\n out[i].id.toLowerCase() === id\n ) {\n out.splice(i, 1);\n }\n }\n out.push(entry);\n }\n }\n return out;\n}\n","/**\n * Validate that every font name referenced in a document is either\n * in SAFE_FONTS or present in the document's fontRegistry / runtime overrides.\n *\n * Used at generate-start; emits warnings for unresolved names so pipelines\n * can surface them via their existing warning channels.\n */\n\nimport { SAFE_FONTS, isSafeFont } from '../schemas/font-catalog';\nimport type { FontRegistryEntry } from '../schemas/font-catalog';\n\n/**\n * Warning codes for font resolution + rendering.\n *\n * - `FONT_UNRESOLVED` — family not in SAFE_FONTS and not registered.\n * - `FONT_MODE_SUBSTITUTED` — non-safe families rewritten to safe equivalents.\n * - `FONT_MODE_CUSTOM` — export mode \"custom\" — refs kept as-is.\n */\nexport type FontIssueCode =\n | 'FONT_UNRESOLVED'\n | 'FONT_MODE_SUBSTITUTED'\n | 'FONT_MODE_CUSTOM';\n\nexport interface FontResolutionIssue {\n code: FontIssueCode;\n family: string;\n message: string;\n}\n\nexport interface FontValidationResult {\n /** Names that resolved via SAFE_FONTS or the registry. */\n resolved: string[];\n /** Names with no resolution path. */\n unresolved: string[];\n /** One warning per unresolved name. */\n warnings: FontResolutionIssue[];\n}\n\nexport interface FontValidationInput {\n /** Font names referenced in the document (from collectFontNamesFromDocx / FromPptx). */\n referencedNames: Iterable<string>;\n /** Runtime-registered entries (e.g. from FontRuntimeOpts.extraEntries). */\n registeredEntries?: FontRegistryEntry[];\n}\n\nfunction buildRegistryIndex(\n registeredEntries?: FontRegistryEntry[]\n): Set<string> {\n const idx = new Set<string>();\n for (const e of registeredEntries ?? []) {\n idx.add(e.family.toLowerCase());\n idx.add(e.id.toLowerCase());\n }\n return idx;\n}\n\n/**\n * Validate referenced font names against SAFE_FONTS + runtime-registered entries.\n * Does not perform network fetches — this runs purely off schema + opts content.\n */\nexport function validateFontReferences(\n input: FontValidationInput\n): FontValidationResult {\n const registryIdx = buildRegistryIndex(input.registeredEntries);\n const resolved: string[] = [];\n const unresolved: string[] = [];\n const warnings: FontResolutionIssue[] = [];\n\n for (const name of input.referencedNames) {\n if (isSafeFont(name) || registryIdx.has(name.toLowerCase())) {\n resolved.push(name);\n continue;\n }\n unresolved.push(name);\n warnings.push({\n code: 'FONT_UNRESOLVED',\n family: name,\n message:\n `Font \"${name}\" is not a SAFE_FONTS entry and is not registered via fonts.extraEntries. ` +\n `It will render with a host fallback on machines lacking the font. ` +\n `Safe fonts: ${SAFE_FONTS.join(', ')}.`,\n });\n }\n\n return { resolved, unresolved, warnings };\n}\n","/**\n * Map a (family, weight, italic) tuple to the pair of\n * `(familyName, { bold, italic })` the renderer should actually use.\n *\n * OOXML runs can only carry a bold/italic toggle, not a numeric weight.\n * For weights outside the RIBBI quad (400/700 × roman/italic), Word\n * resolves intermediate weights via **separate sub-family faces** whose\n * internal family name is the canonical Google-Fonts-style subfamily,\n * e.g. `Inter Light`, `Inter ExtraBold Italic`. Rewriting the run's\n * `family` to that synthetic name lets Word pick the right face when the\n * recipient has the full family installed, and lets the LibreOffice\n * preview resolve the matching staged TTF by its internal name.\n *\n * No embedding involved — this is purely a name transform applied at\n * render time. Safe fonts and unrecognised weights fall back to the\n * bold-only heuristic (`weight >= 600 → bold`).\n */\n\n/** Human-readable labels for the canonical font-weight numbers. */\nexport const WEIGHT_LABELS: Record<number, string> = {\n 100: 'Thin',\n 200: 'ExtraLight',\n 300: 'Light',\n 400: 'Regular',\n 500: 'Medium',\n 600: 'SemiBold',\n 700: 'Bold',\n 800: 'ExtraBold',\n 900: 'Black',\n};\n\nexport interface SynthesizedFamily {\n /** The family name to emit in `rFonts`/`fontFace`. */\n family: string;\n /** Whether to also set the run's bold toggle. */\n bold: boolean;\n /** Whether to also set the run's italic toggle. */\n italic: boolean;\n /**\n * `true` when the input `weight` was not one of the canonical\n * 100/200/.../900 labels. The canonical family name is returned with\n * a `weight >= 600 → bold` fallback, but the run will not match a\n * dedicated sub-family face — callers should surface this so authors\n * know the weight was effectively rounded to Regular or Bold.\n */\n nonCanonicalWeight: boolean;\n}\n\n/**\n * Translate `(family, weight, italic)` into the rendering-time family name\n * plus the bold/italic toggles to emit on the run.\n *\n * - RIBBI (weights 400 + 700, roman + italic) stays on the canonical family\n * name and uses native bold/italic toggles.\n * - Other canonical weights become `\"<Family> <Weight>\"` (e.g.\n * `\"Inter Light\"`) with bold/italic toggles cleared; any italic flag is\n * folded into the name (`\"Inter Light Italic\"`).\n * - Non-canonical weights (floating or out-of-range) fall back to\n * `bold = weight >= 600` and leave the family name untouched.\n */\nexport function synthesizeFamilyName(\n family: string,\n weight: number | undefined,\n italic: boolean\n): SynthesizedFamily {\n if (weight == null) {\n return { family, bold: false, italic, nonCanonicalWeight: false };\n }\n // RIBBI — no rewrite needed, let native bold/italic do the work.\n if (weight === 400) {\n return { family, bold: false, italic, nonCanonicalWeight: false };\n }\n if (weight === 700) {\n return { family, bold: true, italic, nonCanonicalWeight: false };\n }\n const label = WEIGHT_LABELS[weight];\n if (!label) {\n // Non-canonical weight — best-effort bold fallback. Flag the result so\n // callers can warn: the run will render as Regular or Bold, not the\n // intermediate weight the author asked for.\n return {\n family,\n bold: weight >= 600,\n italic,\n nonCanonicalWeight: true,\n };\n }\n const suffix = italic ? ` ${label} Italic` : ` ${label}`;\n return {\n family: `${family}${suffix}`,\n bold: false,\n italic: false,\n nonCanonicalWeight: false,\n };\n}\n","/**\n * Decode a base64 / data-URL font payload to a Buffer.\n */\n\nimport type { ResolvedFontSource } from '../types';\nimport { detectFontFormat } from './format';\n\nexport interface DataSourceInput {\n data: string;\n weight?: number;\n italic?: boolean;\n}\n\n/**\n * Hard upper bound on the decoded font buffer. Real TTF/OTF faces are well\n * under 5 MB; even variable-axis fonts with CJK coverage rarely exceed 4 MB.\n * Rejecting oversized payloads before decoding prevents a malicious\n * `kind: 'data'` registry entry from allocating arbitrary server memory\n * when the generator runs behind an HTTP endpoint.\n */\nconst MAX_DATA_FONT_BYTES = 5 * 1024 * 1024;\n\n/**\n * Accepts either a bare base64 string or a data: URL.\n * Throws on invalid input; renderer catches and emits a warning.\n */\nexport function loadDataFontSource(input: DataSourceInput): ResolvedFontSource {\n const raw = input.data.trim();\n let b64: string;\n if (raw.startsWith('data:')) {\n const comma = raw.indexOf(',');\n if (comma < 0) throw new Error('Invalid data URL: no payload separator');\n // Only base64-encoded payloads are supported.\n const header = raw.slice(5, comma);\n if (!header.includes(';base64')) {\n throw new Error('Data URL must be base64-encoded');\n }\n b64 = raw.slice(comma + 1);\n } else {\n b64 = raw;\n }\n // Upper-bound the decoded size via base64 length before allocating. A\n // base64 string decodes to ~3/4 its character count, so a conservative\n // check on the encoded size avoids decoding a 50 MB payload just to\n // reject it afterward.\n const approxDecodedBytes = Math.floor((b64.length * 3) / 4);\n if (approxDecodedBytes > MAX_DATA_FONT_BYTES) {\n throw new Error(\n `Font data payload exceeds ${MAX_DATA_FONT_BYTES} byte limit`\n );\n }\n const data = Buffer.from(b64, 'base64');\n if (data.length === 0) throw new Error('Decoded font buffer is empty');\n if (data.length > MAX_DATA_FONT_BYTES) {\n throw new Error(\n `Font data payload exceeds ${MAX_DATA_FONT_BYTES} byte limit`\n );\n }\n // Base64 decoding silently discards invalid characters, so garbage input\n // yields a non-empty buffer that isn't a real font. Magic-byte check\n // rejects the garbage before it reaches the LibreOffice preview stager.\n const format = detectFontFormat(data);\n if (format === 'unknown') {\n throw new Error(\n 'Decoded font buffer is not a recognized font (expected TTF/OTF/WOFF/WOFF2)'\n );\n }\n // WOFF/WOFF2 flow through: Office output never embeds bytes, and the\n // LibreOffice preview stager handles them via fontconfig.\n return {\n data,\n weight: input.weight ?? 400,\n italic: input.italic ?? false,\n format,\n };\n}\n","/**\n * Google Fonts fetcher.\n *\n * Hits the CSS API v2 with an older User-Agent that returns TTF (default UA\n * gets WOFF2, which Office cannot embed as-is). Parses the `src: url(...)` line\n * and downloads the binary.\n *\n * Uses memory + optional disk cache keyed by `${family}|${weight}|${italic}`.\n */\n\nimport type { ResolvedFontSource } from '../types';\nimport { FontMemoryCache } from '../cache/memory-cache';\nimport { detectFontFormat } from './format';\n\ninterface FontDiskCacheLike {\n get(key: string): Promise<Buffer | undefined>;\n set(key: string, value: Buffer): Promise<void>;\n}\n\nexport interface GoogleFetchOptions {\n family: string;\n weights: number[];\n italics?: boolean;\n memoryCache?: FontMemoryCache;\n diskCache?: FontDiskCacheLike;\n fetchTimeoutMs?: number;\n /** Override for tests. */\n fetcher?: typeof fetch;\n}\n\nexport interface GoogleFetchResult {\n sources: ResolvedFontSource[];\n warnings: string[];\n}\n\nconst TTF_UA = 'Mozilla/4.0';\n\nasync function fetchWithTimeout(\n url: string,\n opts: {\n headers?: Record<string, string>;\n timeoutMs?: number;\n fetcher?: typeof fetch;\n }\n): Promise<Response> {\n const ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 5000);\n try {\n const f = opts.fetcher ?? fetch;\n return await f(url, { headers: opts.headers, signal: ctrl.signal });\n } finally {\n clearTimeout(timer);\n }\n}\n\nfunction buildCssUrl(\n family: string,\n weights: number[],\n italics: boolean\n): string {\n // URL-encode then restore spaces as `+` (Google's CSS2 API convention).\n const famPart = encodeURIComponent(family).replace(/%20/g, '+');\n const sortedWeights = [...weights].sort((a, b) => a - b);\n if (italics) {\n const axis = sortedWeights.flatMap((w) => [`0,${w}`, `1,${w}`]).join(';');\n return `https://fonts.googleapis.com/css2?family=${famPart}:ital,wght@${axis}&display=swap`;\n }\n const wghtPart = sortedWeights.join(';');\n return `https://fonts.googleapis.com/css2?family=${famPart}:wght@${wghtPart}&display=swap`;\n}\n\n/**\n * Parse Google Fonts CSS response into { weight, italic, ttfUrl } tuples.\n * Each @font-face block contains the src + font-weight + font-style we need.\n */\nfunction parseCssFaces(\n css: string\n): { weight: number; italic: boolean; ttfUrl: string }[] {\n const out: { weight: number; italic: boolean; ttfUrl: string }[] = [];\n const faceRe = /@font-face\\s*\\{([^}]*)\\}/g;\n let m: RegExpExecArray | null;\n while ((m = faceRe.exec(css)) !== null) {\n const block = m[1];\n // Pin the CDN: only accept URLs whose hostname is fonts.gstatic.com so a\n // hijacked/mitm'd CSS response can't redirect downloads to arbitrary hosts.\n const urlM = block.match(\n /src:\\s*url\\((https:\\/\\/fonts\\.gstatic\\.com\\/[^)]+\\.ttf)\\)/\n );\n if (!urlM) continue;\n const weightM = block.match(/font-weight:\\s*(\\d+)/);\n const italicM = block.match(/font-style:\\s*italic/);\n out.push({\n weight: weightM ? parseInt(weightM[1], 10) : 400,\n italic: Boolean(italicM),\n ttfUrl: urlM[1],\n });\n }\n return out;\n}\n\nfunction cacheKey(family: string, weight: number, italic: boolean): string {\n return `google|${family}|${weight}|${italic ? 'i' : 'r'}`;\n}\n\nexport async function fetchGoogleFontSources(\n opts: GoogleFetchOptions\n): Promise<GoogleFetchResult> {\n const weights = opts.weights?.length ? opts.weights : [400, 700];\n const italics = opts.italics ?? false;\n const warnings: string[] = [];\n const sources: ResolvedFontSource[] = [];\n\n // Try every (weight, italic) combo — first against caches, then via fetch.\n const wanted: { weight: number; italic: boolean }[] = [];\n for (const w of weights) {\n wanted.push({ weight: w, italic: false });\n if (italics) wanted.push({ weight: w, italic: true });\n }\n\n // Resolve any cache hits first.\n const pending: { weight: number; italic: boolean }[] = [];\n for (const w of wanted) {\n const key = cacheKey(opts.family, w.weight, w.italic);\n const mem = opts.memoryCache?.get(key);\n if (mem) {\n sources.push({\n data: mem,\n weight: w.weight,\n italic: w.italic,\n format: detectFontFormat(mem),\n });\n continue;\n }\n const disk = await opts.diskCache?.get(key);\n if (disk) {\n opts.memoryCache?.set(key, disk);\n sources.push({\n data: disk,\n weight: w.weight,\n italic: w.italic,\n format: detectFontFormat(disk),\n });\n continue;\n }\n pending.push(w);\n }\n\n if (pending.length === 0) {\n return { sources, warnings };\n }\n\n // Single CSS request covers all pending variants.\n const needItalics = pending.some((p) => p.italic);\n const cssUrl = buildCssUrl(\n opts.family,\n Array.from(new Set(pending.map((p) => p.weight))),\n needItalics\n );\n let faces: { weight: number; italic: boolean; ttfUrl: string }[];\n try {\n const cssRes = await fetchWithTimeout(cssUrl, {\n headers: { 'User-Agent': TTF_UA },\n timeoutMs: opts.fetchTimeoutMs,\n fetcher: opts.fetcher,\n });\n if (!cssRes.ok) {\n warnings.push(\n `Google Fonts CSS fetch for \"${opts.family}\" returned ${cssRes.status}`\n );\n return { sources, warnings };\n }\n const css = await cssRes.text();\n faces = parseCssFaces(css);\n } catch (err) {\n warnings.push(\n `Google Fonts CSS fetch for \"${opts.family}\" failed: ${\n (err as Error).message\n }`\n );\n return { sources, warnings };\n }\n\n for (const need of pending) {\n const match = faces.find(\n (f) => f.weight === need.weight && f.italic === need.italic\n );\n if (!match) {\n warnings.push(\n `Google Fonts \"${opts.family}\" missing weight ${need.weight}${\n need.italic ? ' italic' : ''\n }`\n );\n continue;\n }\n try {\n const res = await fetchWithTimeout(match.ttfUrl, {\n timeoutMs: opts.fetchTimeoutMs,\n fetcher: opts.fetcher,\n });\n if (!res.ok) {\n warnings.push(\n `Google Fonts TTF fetch for \"${opts.family}\" ${need.weight} returned ${res.status}`\n );\n continue;\n }\n const ab = await res.arrayBuffer();\n // Metadata validation (weight class, name-table defects, fsType) runs\n // centrally in FontRegistry.materializeEntry so file/data/url/google\n // sources are all checked under one code path.\n const buf = Buffer.from(ab);\n const key = cacheKey(opts.family, need.weight, need.italic);\n opts.memoryCache?.set(key, buf);\n await opts.diskCache?.set(key, buf);\n sources.push({\n data: buf,\n weight: need.weight,\n italic: need.italic,\n format: detectFontFormat(buf),\n });\n } catch (err) {\n warnings.push(\n `Google Fonts TTF fetch for \"${opts.family}\" ${need.weight} failed: ${\n (err as Error).message\n }`\n );\n }\n }\n\n return { sources, warnings };\n}\n","/**\n * Direct-URL font fetcher. Downloads a single TTF/OTF from an HTTPS URL and\n * returns it as a `ResolvedFontSource`. Cache-keyed the same way as the\n * Google Fonts fetcher so fetches are deduplicated across generations.\n *\n * Used as an escape hatch for families whose Google Fonts redistribution has\n * known defects (e.g. Inter's static Thin/ExtraLight shipping with a broken\n * `OS/2.usWeightClass`). The `UPSTREAM_OVERRIDES` catalog points affected\n * families at clean upstream sources like rsms/inter via jsDelivr.\n */\n\nimport type { ResolvedFontSource } from '../types';\nimport { detectFontFormat } from './format';\nimport { isAllowedFontUrl } from './url-allowlist';\n\nexport interface UrlFetchOptions {\n url: string;\n weight: number;\n italic: boolean;\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 cacheKey(url: string, weight: number, italic: boolean): string {\n return `url|${url}|${weight}|${italic ? 'i' : 'r'}`;\n}\n\nexport async function fetchUrlFontSource(\n opts: UrlFetchOptions\n): Promise<{ source?: ResolvedFontSource; warnings?: string[] }> {\n if (!isAllowedFontUrl(opts.url)) {\n return {\n warnings: [\n `URL font fetch rejected (host not in allowlist or non-HTTPS): ${opts.url}`,\n ],\n };\n }\n const key = cacheKey(opts.url, opts.weight, opts.italic);\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 ctrl = new AbortController();\n const timer = setTimeout(() => ctrl.abort(), opts.fetchTimeoutMs ?? 10000);\n try {\n const f = opts.fetcher ?? fetch;\n // redirect: 'manual' prevents the allowlist from being bypassed via\n // a 3xx Location pointing at an off-list host. We re-validate the\n // Location header against the allowlist before following.\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) {\n return {\n warnings: [\n `URL font fetch \"${opts.url}\" ${res.status} with no Location header`,\n ],\n };\n }\n const resolved = new URL(next, opts.url).toString();\n if (!isAllowedFontUrl(resolved)) {\n return {\n warnings: [\n `URL font fetch \"${opts.url}\" redirected to disallowed host: ${resolved}`,\n ],\n };\n }\n if (++hops > 3) {\n return {\n warnings: [`URL font fetch \"${opts.url}\" too many redirects`],\n };\n }\n res = await f(resolved, { signal: ctrl.signal, redirect: 'manual' });\n }\n if (!res.ok) {\n return {\n warnings: [`URL font fetch \"${opts.url}\" returned ${res.status}`],\n };\n }\n const ab = await res.arrayBuffer();\n const raw = Buffer.from(ab);\n // Reject obvious non-font responses — e.g. jsDelivr 404 HTML, 200 OK\n // redirect pages, or aliased directory listings. Without this check the\n // bytes would sail through to the embed step and corrupt the output.\n const format = detectFontFormat(raw);\n if (format === 'unknown' || raw.length < 512) {\n return {\n warnings: [\n `URL font fetch \"${opts.url}\" returned ${raw.length} bytes of ${format} — not a TTF/OTF. Skipping.`,\n ],\n };\n }\n // WOFF/WOFF2 flow through: Office output never embeds bytes; the\n // LibreOffice preview stager handles them via fontconfig.\n // Metadata validation runs centrally in FontRegistry.materializeEntry.\n const buf = raw;\n opts.memoryCache?.set(key, buf);\n await opts.diskCache?.set(key, buf);\n return {\n source: {\n data: buf,\n weight: opts.weight,\n italic: opts.italic,\n format,\n },\n warnings: [],\n };\n } catch (err) {\n return {\n warnings: [\n `URL font fetch \"${opts.url}\" failed: ${(err as Error).message}`,\n ],\n };\n } finally {\n clearTimeout(timer);\n }\n}\n","/**\n * Post-fetch metadata validation for TTF/OTF bytes. Catches the three known\n * classes of defects that surface in Google Fonts' redistribution pipeline:\n *\n * 1. Wrong `OS/2.usWeightClass` (Chivo Light, Mada Regular, Petrona)\n * 2. Duplicate usWeightClass across weights (Exo Thin/ExtraLight)\n * 3. Non-unique `name` subfamily records (Inter, Manrope, Recursive)\n * 4. A family name that isn't the one we resolved the bytes as — an\n * instanced `InterVariable.ttf` still calls itself \"Inter Variable\", a\n * vendor CDN static may call itself anything at all\n *\n * We don't throw on mismatch — the pipeline has already tried to fix the\n * bytes where it can (`rewriteFontSubfamilyNames` after variable-font\n * instancing; `rewriteFontFamilyName` at preview staging). The validator\n * returns human-readable diagnostics so the caller can emit warnings tagged\n * `FONT_METADATA_DEFECT`, pointing users at the upstream override escape\n * hatch before they ship a broken document.\n */\n\nimport {\n readFontFamilyNames,\n readNameRecords,\n standardSubfamilyNames,\n} from './ttf-name';\n\nconst HEADER_SIZE = 12;\nconst TABLE_RECORD_SIZE = 16;\n\nfunction readTable(\n ttf: Buffer,\n tag: string\n): { off: number; len: number } | null {\n if (ttf.length < HEADER_SIZE) return null;\n const version = ttf.readUInt32BE(0);\n if (version !== 0x00010000 && version !== 0x4f54544f) return null;\n const numTables = ttf.readUInt16BE(4);\n for (let i = 0; i < numTables; i++) {\n const r = HEADER_SIZE + i * TABLE_RECORD_SIZE;\n if (r + TABLE_RECORD_SIZE > ttf.length) return null;\n if (ttf.toString('ascii', r, r + 4) === tag) {\n return { off: ttf.readUInt32BE(r + 8), len: ttf.readUInt32BE(r + 12) };\n }\n }\n return null;\n}\n\nfunction readUsWeightClass(ttf: Buffer): number | null {\n const os2 = readTable(ttf, 'OS/2');\n if (!os2) return null;\n if (os2.off + 6 > ttf.length) return null;\n return ttf.readUInt16BE(os2.off + 4);\n}\n\nexport interface FontMetadataDiagnostic {\n code:\n | 'WEIGHT_CLASS_MISMATCH'\n | 'SUBFAMILY_MISMATCH'\n | 'LEGACY_SUBFAMILY_MISMATCH'\n | 'FAMILY_MISMATCH';\n message: string;\n}\n\n/**\n * Inspect a font's metadata against the weight + italic we asked it to\n * represent. Returns one diagnostic per detected defect.\n */\nexport function validateFontMetadata(\n ttf: Buffer,\n weight: number,\n italic: boolean,\n familyLabel: string\n): FontMetadataDiagnostic[] {\n const diags: FontMetadataDiagnostic[] = [];\n const usWeight = readUsWeightClass(ttf);\n if (usWeight != null && usWeight !== weight) {\n diags.push({\n code: 'WEIGHT_CLASS_MISMATCH',\n message: `Font \"${familyLabel}\" weight ${weight}: OS/2.usWeightClass reports ${usWeight}. Likely a defective redistribution — consider adding an upstream override.`,\n });\n }\n\n // OS/2.fsType (embedding-permission bits) deliberately NOT checked. Office\n // output never embeds font bytes anymore — substitute mode rewrites to\n // SAFE_FONTS, custom mode ships references as-is, and the LibreOffice\n // preview stager only registers bytes transiently with the converter's\n // child process. Permission warnings would be pure noise for every\n // Google Fonts resolution.\n\n // The bytes must answer to the family they were resolved as, or nothing\n // downstream can find them: a run says `rFonts w:ascii=\"Inter\"` and the\n // host matches that against the font's own name table, never against the\n // registry entry or the filename. `FontRegistry` repairs this before\n // validating, so reaching here means the repair could not run (no name\n // table, a format-1 one, non-sfnt bytes) — i.e. the face really will be\n // unreachable under `familyLabel`.\n const declaredFamilies = readFontFamilyNames(ttf);\n if (\n declaredFamilies.length > 0 &&\n !declaredFamilies.includes(familyLabel.trim())\n ) {\n diags.push({\n code: 'FAMILY_MISMATCH',\n message: `Font \"${familyLabel}\" weight ${weight}${italic ? ' italic' : ''}: name table declares ${declaredFamilies\n .map((f) => `\"${f}\"`)\n .join(\n ' / '\n )}, not \"${familyLabel}\". Referencing runs will not resolve this face.`,\n });\n }\n\n const std = standardSubfamilyNames(weight, italic);\n if (!std) return diags;\n const expected17 = std.typographic;\n const expected2 = std.legacy;\n\n const names = readNameRecords(ttf, new Set([2, 17]));\n for (const n of names) {\n if (n.nameID === 17 && n.value !== expected17) {\n diags.push({\n code: 'SUBFAMILY_MISMATCH',\n message: `Font \"${familyLabel}\" weight ${weight}${italic ? ' italic' : ''}: name record (platform ${n.platformID}) nameID 17 = \"${n.value}\", expected \"${expected17}\".`,\n });\n }\n if (n.nameID === 2 && n.value !== expected2) {\n diags.push({\n code: 'LEGACY_SUBFAMILY_MISMATCH',\n message: `Font \"${familyLabel}\" weight ${weight}${italic ? ' italic' : ''}: name record (platform ${n.platformID}) nameID 2 = \"${n.value}\", expected \"${expected2}\".`,\n });\n }\n }\n return diags;\n}\n","/**\n * Structural sanity check for TTF/OTF bytes — can a font system load this at\n * all? Answered before `validateFontMetadata`, which asks the narrower\n * question of whether a loadable font's metadata says the right things.\n *\n * The gap this closes: `detectFontFormat` classifies by the four magic bytes\n * alone, so a download truncated anywhere after byte four is still 'ttf' and\n * flows on as if it were a font. Nothing downstream notices. The metadata\n * validator has no name records to check and usually no readable OS/2\n * either, so it stays silent by design; `FontRegistry`'s family stamp finds\n * no declared family to contradict and no-ops; and the bytes stage as a\n * `.ttf` that fontconfig and Core Text refuse, so the document renders in a\n * fallback face with nothing anywhere saying why.\n *\n * Deliberately NOT a full sfnt parser. It answers \"will this load\", not \"is\n * this well-formed\" — checksums, table-specific contents and glyph data are\n * out of scope. Every check here is one a font system performs before it can\n * index a face at all, which is what makes a failure worth a warning rather\n * than a matter of taste.\n */\n\nimport { readNameRecords } from './ttf-name';\n\nexport interface FontStructureDiagnostic {\n code: 'FONT_UNREADABLE';\n message: string;\n}\n\n/** sfnt versions a font system will attempt to load. */\nconst SFNT_VERSIONS = new Set<number>([\n 0x00010000, // TrueType outlines\n 0x4f54544f, // 'OTTO' — CFF outlines\n 0x74727565, // 'true'\n 0x74797031, // 'typ1'\n]);\n\nconst HEADER_SIZE = 12;\nconst TABLE_RECORD_SIZE = 16;\n\n/**\n * Inspect the sfnt envelope of a font we are about to hand to a font system.\n * Returns the first thing that makes it unloadable, or null.\n *\n * One diagnostic, not a list: past the first structural failure every later\n * check is reading rubble, and a caller can only act on the file as a whole\n * anyway.\n */\nexport function validateFontStructure(\n ttf: Buffer,\n weight: number,\n italic: boolean,\n familyLabel: string\n): FontStructureDiagnostic | null {\n const face = `Font \"${familyLabel}\" weight ${weight}${italic ? ' italic' : ''}`;\n const unreadable = (reason: string): FontStructureDiagnostic => ({\n code: 'FONT_UNREADABLE',\n message: `${face}: ${reason} The face will not resolve; text referencing it renders in a fallback.`,\n });\n\n if (ttf.length < HEADER_SIZE) {\n return unreadable(\n `${ttf.length} bytes is shorter than an sfnt header — the download is truncated or empty.`\n );\n }\n const version = ttf.readUInt32BE(0);\n if (!SFNT_VERSIONS.has(version)) {\n return unreadable(\n `sfnt version 0x${version.toString(16).padStart(8, '0')} is neither TrueType nor OpenType.`\n );\n }\n\n const numTables = ttf.readUInt16BE(4);\n if (numTables === 0) return unreadable('its table directory is empty.');\n const directoryEnd = HEADER_SIZE + numTables * TABLE_RECORD_SIZE;\n if (directoryEnd > ttf.length) {\n return unreadable(\n `its directory claims ${numTables} tables (${directoryEnd} bytes) but the file is ${ttf.length} bytes — truncated.`\n );\n }\n\n // Directory offsets in range. A table pointing past the end is the shape a\n // truncated download takes once it keeps enough bytes for the directory.\n const tags = new Set<string>();\n for (let i = 0; i < numTables; i += 1) {\n const ro = HEADER_SIZE + i * TABLE_RECORD_SIZE;\n const tag = ttf.toString('ascii', ro, ro + 4);\n const offset = ttf.readUInt32BE(ro + 8);\n const length = ttf.readUInt32BE(ro + 12);\n if (offset + length > ttf.length) {\n return unreadable(\n `its \"${tag}\" table runs to byte ${offset + length} but the file is ${ttf.length} bytes — truncated.`\n );\n }\n tags.add(tag);\n }\n\n // `name` carries the family every consumer looks a face up by, and `head`\n // the units-per-em every consumer scales it with. Neither is optional.\n for (const required of ['name', 'head']) {\n if (!tags.has(required)) {\n return unreadable(`it has no \"${required}\" table.`);\n }\n }\n\n // Outlines: glyf + loca (TrueType) or a CFF table (OpenType). Without them\n // there is nothing to draw, whatever else the file carries.\n const hasTrueTypeOutlines = tags.has('glyf') && tags.has('loca');\n const hasCffOutlines = tags.has('CFF ') || tags.has('CFF2');\n if (!hasTrueTypeOutlines && !hasCffOutlines) {\n return unreadable(\n 'it carries no glyph outlines (neither \"glyf\" + \"loca\" nor \"CFF \").'\n );\n }\n\n // Present is not the same as readable: the reader bounds every record by\n // the table's own extent, so a `name` table with a corrupt header or\n // out-of-range string offsets yields nothing and the face is unindexable.\n if (readNameRecords(ttf).length === 0) {\n return unreadable('its \"name\" table carries no readable records.');\n }\n\n return null;\n}\n","/**\n * In-process LRU cache for resolved font buffers.\n * Scoped to a single process — do not share across requests on a server.\n */\n\nexport interface MemoryCacheOptions {\n /** Approximate soft cap in bytes. LRU-evict when exceeded. */\n maxBytes?: number;\n}\n\nexport class FontMemoryCache {\n private readonly store = new Map<string, Buffer>();\n private bytes = 0;\n private readonly maxBytes: number;\n\n constructor(opts: MemoryCacheOptions = {}) {\n this.maxBytes = opts.maxBytes ?? 20 * 1024 * 1024; // 20 MB default\n }\n\n get(key: string): Buffer | undefined {\n const v = this.store.get(key);\n if (!v) return undefined;\n // Refresh LRU position.\n this.store.delete(key);\n this.store.set(key, v);\n return v;\n }\n\n set(key: string, value: Buffer): void {\n const existing = this.store.get(key);\n if (existing) this.bytes -= existing.byteLength;\n this.store.set(key, value);\n this.bytes += value.byteLength;\n while (this.bytes > this.maxBytes && this.store.size > 0) {\n const oldest = this.store.keys().next().value as string | undefined;\n if (!oldest) break;\n const removed = this.store.get(oldest);\n this.store.delete(oldest);\n if (removed) this.bytes -= removed.byteLength;\n }\n }\n\n size(): number {\n return this.store.size;\n }\n}\n","/**\n * FontRegistry — merges catalog + document registry + runtime entries\n * and materializes referenced fonts into ResolvedFont records.\n *\n * Resolution rules, per referenced name:\n * 1. Registry match (by family or id, case-insensitive). Runtime entries win\n * on collision with document entries. Materialize each source.\n * 2. SAFE_FONTS membership → empty sources.\n * 3. Otherwise → empty sources with FONT_UNRESOLVED warning.\n */\n\nimport { isSafeFont } from '../schemas/font-catalog';\nimport type { FontRegistryEntry, FontSource } from '../schemas/font-catalog';\nimport type {\n FontRuntimeOpts,\n ResolvedFont,\n ResolvedFontSource,\n} from './types';\nimport { loadDataFontSource } from './sources/data-loader';\nimport { fetchGoogleFontSources } from './sources/google-fetcher';\nimport { fetchUrlFontSource } from './sources/url-fetcher';\nimport { validateFontMetadata } from './sources/ttf-validate';\nimport { validateFontStructure } from './sources/ttf-structure';\nimport {\n readFontFamilyNames,\n rewriteFontFamilyName,\n standardSubfamilyNames,\n} from './sources/ttf-name';\nimport { FontMemoryCache } from './cache/memory-cache';\n\n/**\n * Minimal interface the registry needs from a disk cache. The concrete\n * implementation ships in `./cache/disk-cache` but is Node-only (uses fs/crypto).\n * Callers on Node inject an instance; browser callers pass nothing.\n */\nexport interface FontDiskCacheLike {\n get(key: string): Promise<Buffer | undefined>;\n set(key: string, value: Buffer): Promise<void>;\n}\n\n/**\n * Minimal interface for a file-loader. Same reasoning as FontDiskCacheLike:\n * concrete impl is Node-only, callers inject when on Node.\n */\nexport type FontFileLoader = (input: {\n path: string;\n weight?: number;\n italic?: boolean;\n baseDir?: string;\n}) => Promise<ResolvedFontSource>;\n\n/**\n * Minimal interface for the variable-font fetcher. `subset-font` (the\n * harfbuzz-wasm wrapper we use for axis pinning) reaches for `fs` at\n * init time, which crashes in the browser. Injection keeps that import\n * behind the Node-only subpath; browser bundles never pull it in, and\n * browser callers simply won't see `kind: 'variable'` fonts resolved\n * (the registry warns and skips instead).\n */\nexport type FontVariableLoader = (input: {\n url: string;\n weight: number;\n italic: boolean;\n axes?: Record<string, number>;\n fetchTimeoutMs?: number;\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}) => Promise<{ source?: ResolvedFontSource; warnings?: string[] }>;\n\nexport interface FontRegistryInput {\n /** Runtime options — entries come from opts.extraEntries. */\n opts?: FontRuntimeOpts;\n /** Optional disk cache (Node only). Pass an instance of FontDiskCache. */\n diskCache?: FontDiskCacheLike;\n /**\n * Optional `kind: \"file\"` loader (Node only). Inject `loadFileFontSource`\n * from `@json-to-office/shared/fonts/sources/file-loader` on Node. Browser\n * callers pass nothing; `kind: \"file\"` sources then warn and skip.\n */\n fileLoader?: FontFileLoader;\n /**\n * Optional `kind: \"variable\"` loader (Node only). Inject\n * `fetchVariableFontSource` from `@json-to-office/shared/fonts/node` on\n * Node. Browser callers pass nothing; `kind: \"variable\"` sources then\n * warn and skip. Keeping this injected avoids dragging subset-font (and\n * its `fs.promises.readFile` bootstrap) into client bundles.\n */\n variableLoader?: FontVariableLoader;\n}\n\n/**\n * Make the bytes declare the family they were resolved as.\n *\n * `ResolvedFont.family` is a claim about the bytes that every consumer\n * relies on and that nothing used to enforce. A host — Core Text,\n * fontconfig, GDI — finds a face by the family in its `name` table, never\n * by the registry entry or the filename, so a source whose name table says\n * something else is unreachable from the runs that reference it. It fails\n * silently: on a machine that happens to have the real family installed the\n * reference lands there instead, and only a host without it (a container,\n * a colleague's laptop) shows the fallback.\n *\n * The case that forced this: Inter resolves through an upstream override\n * that instances `InterVariable.ttf` per weight, and harfbuzz keeps the\n * master's name table — so every instance called itself \"Inter Variable\".\n * The weighted faces were saved by the preview stager renaming them to\n * \"Inter Medium\" / \"Inter SemiBold\" on the way out; weights 400 and 700\n * kept the family name unchanged (they ride the run's bold/italic toggles\n * instead), so those alone were staged under a name no run asks for.\n *\n * Matching on ANY declared name (family or typographic family) keeps a\n * legitimately-named static untouched: LifeSans-Medium.ttf registered as\n * \"Life Sans\" already answers to it via nameID 16, even though nameID 1\n * says \"Life Sans Medium\". Only bytes that answer to nothing get rewritten.\n */\nfunction stampResolvedFamily(\n source: ResolvedFontSource,\n family: string\n): ResolvedFontSource {\n if (source.format !== 'ttf' && source.format !== 'otf') return source;\n const declared = readFontFamilyNames(source.data);\n // Nothing declared: no claim to contradict, and nothing to repair against.\n if (declared.length === 0 || declared.includes(family)) return source;\n // The style this face occupies within `family`. The family IDs become\n // `family` for all four RIBBI faces, so full/PostScript names have to\n // carry the style or roman and italic collide on both.\n const subfamily = standardSubfamilyNames(\n source.weight,\n source.italic\n )?.legacy;\n return {\n ...source,\n data: rewriteFontFamilyName(source.data, family, subfamily),\n };\n}\n\nexport class FontRegistry {\n private readonly index: Map<string, FontRegistryEntry>;\n private readonly cache: Map<string, ResolvedFont>;\n private readonly opts: FontRuntimeOpts;\n private readonly memoryCache: FontMemoryCache;\n private readonly diskCache: FontDiskCacheLike | undefined;\n private readonly fileLoader: FontFileLoader | undefined;\n private readonly variableLoader: FontVariableLoader | undefined;\n\n constructor(input: FontRegistryInput = {}) {\n this.opts = input.opts ?? {};\n this.index = new Map();\n this.cache = new Map();\n this.memoryCache = new FontMemoryCache();\n this.diskCache = input.diskCache;\n this.fileLoader = input.fileLoader;\n this.variableLoader = input.variableLoader;\n\n for (const e of this.opts.extraEntries ?? []) this.addEntry(e);\n }\n\n private addEntry(entry: FontRegistryEntry): void {\n this.index.set(entry.family.toLowerCase(), entry);\n this.index.set(entry.id.toLowerCase(), entry);\n }\n\n /** Resolve every referenced name in one pass. Order preserved. */\n async resolveMany(names: Iterable<string>): Promise<ResolvedFont[]> {\n const out: ResolvedFont[] = [];\n for (const n of names) out.push(await this.resolve(n));\n return out;\n }\n\n async resolve(name: string): Promise<ResolvedFont> {\n const key = name.toLowerCase();\n const cached = this.cache.get(key);\n if (cached) return cached;\n\n const entry = this.index.get(key);\n let result: ResolvedFont;\n\n if (entry) {\n result = await this.materializeEntry(entry);\n } else if (isSafeFont(name)) {\n result = { family: name, sources: [], warnings: [] };\n } else {\n result = {\n family: name,\n sources: [],\n warnings: [\n `Font \"${name}\" is not registered and not in SAFE_FONTS; will rely on host fallback.`,\n ],\n };\n }\n this.cache.set(key, result);\n return result;\n }\n\n private async materializeEntry(\n entry: FontRegistryEntry\n ): Promise<ResolvedFont> {\n const sources: ResolvedFontSource[] = [];\n const warnings: string[] = [];\n\n for (const source of entry.sources) {\n try {\n const materialized = await this.materializeSource(source, warnings);\n for (const raw of materialized) {\n const sfnt = raw.format === 'ttf' || raw.format === 'otf';\n // Can a font system load these bytes at all? Asked first, and on\n // failure asked instead of everything below: the stamp has no name\n // table to write into and the metadata checks no records to read,\n // so both would quietly do nothing and report nothing. The source\n // is still returned — this diagnoses the file, it doesn't decide\n // for the caller whether to ship it.\n const broken = sfnt\n ? validateFontStructure(\n raw.data,\n raw.weight,\n raw.italic,\n entry.family\n )\n : null;\n if (broken) {\n warnings.push(`[${broken.code}] ${broken.message}`);\n sources.push(raw);\n continue;\n }\n const s = stampResolvedFamily(raw, entry.family);\n if (sfnt) {\n for (const d of validateFontMetadata(\n s.data,\n s.weight,\n s.italic,\n entry.family\n )) {\n warnings.push(`[FONT_METADATA_DEFECT:${d.code}] ${d.message}`);\n }\n }\n sources.push(s);\n }\n } catch (err) {\n warnings.push(\n `Font \"${entry.family}\" source (${source.kind}) failed: ${\n (err as Error).message\n }`\n );\n }\n }\n\n return {\n family: entry.family,\n sources,\n warnings,\n };\n }\n\n private async materializeSource(\n source: FontSource,\n warnings: string[]\n ): Promise<ResolvedFontSource[]> {\n switch (source.kind) {\n case 'safe':\n // System-installed; no embedding data.\n return [];\n case 'file': {\n if (!this.fileLoader) {\n warnings.push(\n `kind:\"file\" source for \"${source.path}\" requires a fileLoader (Node-only); skipping.`\n );\n return [];\n }\n return [\n await this.fileLoader({\n path: source.path,\n weight: source.weight,\n italic: source.italic,\n baseDir: this.opts.baseDir,\n }),\n ];\n }\n case 'data':\n return [\n loadDataFontSource({\n data: source.data,\n weight: source.weight,\n italic: source.italic,\n }),\n ];\n case 'google': {\n const gf = this.opts.googleFonts;\n if (gf?.enabled === false) {\n warnings.push(\n `Google Fonts fetch disabled — skipping \"${source.family}\".`\n );\n return [];\n }\n const { sources: fetched, warnings: fetchWarnings } =\n await fetchGoogleFontSources({\n family: source.family,\n weights: source.weights ?? [400, 700],\n italics: source.italics ?? false,\n memoryCache: this.memoryCache,\n diskCache: this.diskCache,\n fetchTimeoutMs: gf?.fetchTimeoutMs,\n });\n warnings.push(...fetchWarnings);\n return fetched;\n }\n case 'url': {\n const gf = this.opts.googleFonts;\n const { source: fetched, warnings: fetchWarnings } =\n await fetchUrlFontSource({\n url: source.url,\n weight: source.weight ?? 400,\n italic: source.italic ?? false,\n memoryCache: this.memoryCache,\n diskCache: this.diskCache,\n fetchTimeoutMs: gf?.fetchTimeoutMs,\n });\n if (fetchWarnings) warnings.push(...fetchWarnings);\n return fetched ? [fetched] : [];\n }\n case 'variable': {\n // Variable-font instancing: fetch the variable TTF once (disk-\n // cached), then harfbuzz-pin the `wght` axis to produce a clean\n // static for this weight. Requires a Node-injected loader because\n // `subset-font` pulls in `fs` at init; without it, browser\n // bundles would break. Callers on Node pass `fetchVariableFontSource`\n // from `@json-to-office/shared/fonts/node`.\n if (!this.variableLoader) {\n warnings.push(\n `kind:\"variable\" source for \"${source.url}\" requires a variableLoader (Node-only); skipping.`\n );\n return [];\n }\n const gf = this.opts.googleFonts;\n const { source: fetched, warnings: fetchWarnings } =\n await this.variableLoader({\n url: source.url,\n weight: source.weight,\n italic: source.italic ?? false,\n axes: source.axes,\n memoryCache: this.memoryCache,\n diskCache: this.diskCache,\n fetchTimeoutMs: gf?.fetchTimeoutMs,\n });\n if (fetchWarnings) warnings.push(...fetchWarnings);\n return fetched ? [fetched] : [];\n }\n default:\n // Exhaustiveness guard — new kind added to schema without handler\n throw new Error(\n `Unknown font source kind: ${(source as { kind: string }).kind}`\n );\n }\n }\n}\n","/**\n * Curated list of popular Google Fonts for picker autocomplete.\n *\n * Not exhaustive — the full Google Fonts library has ~1500 families.\n * This is ~37 names known to cover most real-world use cases.\n */\n\nexport interface PopularGoogleFont {\n family: string;\n category: 'sans' | 'serif' | 'mono' | 'display' | 'handwriting';\n /** Weights available on Google Fonts for this family. */\n weights: number[];\n /** Whether italic variants exist. */\n hasItalic: boolean;\n}\n\nexport const POPULAR_GOOGLE_FONTS: readonly PopularGoogleFont[] = [\n // Sans-serif\n {\n family: 'Inter',\n category: 'sans',\n weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: false,\n },\n {\n family: 'Roboto',\n category: 'sans',\n weights: [100, 300, 400, 500, 700, 900],\n hasItalic: true,\n },\n {\n family: 'Open Sans',\n category: 'sans',\n weights: [300, 400, 500, 600, 700, 800],\n hasItalic: true,\n },\n {\n family: 'Lato',\n category: 'sans',\n weights: [100, 300, 400, 700, 900],\n hasItalic: true,\n },\n {\n family: 'Montserrat',\n category: 'sans',\n weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'Poppins',\n category: 'sans',\n weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'Work Sans',\n category: 'sans',\n weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'Nunito',\n category: 'sans',\n weights: [200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'DM Sans',\n category: 'sans',\n weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'Rubik',\n category: 'sans',\n weights: [300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'Source Sans 3',\n category: 'sans',\n weights: [200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'Manrope',\n category: 'sans',\n weights: [200, 300, 400, 500, 600, 700, 800],\n hasItalic: false,\n },\n {\n family: 'Plus Jakarta Sans',\n category: 'sans',\n weights: [200, 300, 400, 500, 600, 700, 800],\n hasItalic: true,\n },\n {\n family: 'IBM Plex Sans',\n category: 'sans',\n weights: [100, 200, 300, 400, 500, 600, 700],\n hasItalic: true,\n },\n {\n family: 'Archivo',\n category: 'sans',\n weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'Geist',\n category: 'sans',\n weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'Space Grotesk',\n category: 'sans',\n weights: [300, 400, 500, 600, 700],\n hasItalic: false,\n },\n\n // Serif\n {\n family: 'Playfair Display',\n category: 'serif',\n weights: [400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'Merriweather',\n category: 'serif',\n weights: [300, 400, 700, 900],\n hasItalic: true,\n },\n {\n family: 'Lora',\n category: 'serif',\n weights: [400, 500, 600, 700],\n hasItalic: true,\n },\n {\n family: 'Source Serif 4',\n category: 'serif',\n weights: [200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'DM Serif Display',\n category: 'serif',\n weights: [400],\n hasItalic: true,\n },\n {\n family: 'Crimson Pro',\n category: 'serif',\n weights: [200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'Cormorant Garamond',\n category: 'serif',\n weights: [300, 400, 500, 600, 700],\n hasItalic: true,\n },\n\n // Monospace\n {\n family: 'JetBrains Mono',\n category: 'mono',\n weights: [100, 200, 300, 400, 500, 600, 700, 800],\n hasItalic: true,\n },\n {\n family: 'Fira Code',\n category: 'mono',\n weights: [300, 400, 500, 600, 700],\n hasItalic: false,\n },\n {\n family: 'IBM Plex Mono',\n category: 'mono',\n weights: [100, 200, 300, 400, 500, 600, 700],\n hasItalic: true,\n },\n {\n family: 'Source Code Pro',\n category: 'mono',\n weights: [200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n {\n family: 'Space Mono',\n category: 'mono',\n weights: [400, 700],\n hasItalic: true,\n },\n {\n family: 'Geist Mono',\n category: 'mono',\n weights: [100, 200, 300, 400, 500, 600, 700, 800, 900],\n hasItalic: true,\n },\n\n // Display\n {\n family: 'Bebas Neue',\n category: 'display',\n weights: [400],\n hasItalic: false,\n },\n {\n family: 'Abril Fatface',\n category: 'display',\n weights: [400],\n hasItalic: false,\n },\n {\n family: 'Archivo Black',\n category: 'display',\n weights: [400],\n hasItalic: false,\n },\n {\n family: 'Oswald',\n category: 'display',\n weights: [200, 300, 400, 500, 600, 700],\n hasItalic: false,\n },\n\n // Handwriting\n {\n family: 'Caveat',\n category: 'handwriting',\n weights: [400, 500, 600, 700],\n hasItalic: false,\n },\n {\n family: 'Pacifico',\n category: 'handwriting',\n weights: [400],\n hasItalic: false,\n },\n];\n","/**\n * Per-family upstream overrides for popular Google Fonts whose\n * redistribution on fonts.google.com has known defects we can't fix via\n * metadata patching alone.\n *\n * When `autoGoogleFontEntries` hits a family present in this table, it\n * builds override sources instead of issuing `kind: \"google\"` CSS requests.\n * Each entry is either:\n *\n * - `{ kind: \"url\", url, weight, italic? }` — a direct HTTPS TTF/OTF.\n * Use when a clean per-weight static exists on a stable CDN.\n *\n * - `{ kind: \"variable\", url, weight, italic? }` — points at a variable\n * TTF with an `fvar` table. The registry downloads the variable font\n * once and harfbuzz-pins the `wght` axis to the specified weight,\n * producing a clean static TTF. Use when the upstream ships a variable\n * font but no per-weight statics on a CDN (rsms/inter is the\n * canonical example — variable font on jsDelivr, per-weight statics\n * only in GitHub release zips).\n *\n * Pick `variable` over `url` when both are available: the instancer\n * produces per-weight glyph outlines that diverge correctly at every\n * axis value. Google's static redistributions collapse adjacent weights\n * onto the same instance — Inter Thin and ExtraLight both source at\n * ~wght=250 in Google's pipeline, so their static TTFs have 98% identical\n * glyph outlines. Instancing the upstream variable font at exactly wght=100\n * vs wght=200 gives properly distinct geometry.\n *\n * Validate new entries with a HEAD request before adding — the fetchers\n * reject non-TTF responses, but a failed override silently falls back to\n * the Google path, defeating the purpose.\n */\n\n/** One upstream variant source. Type matches the FontSource schema so we\n * can pass the entry directly into `FontRegistry`'s materialize pipeline. */\nexport type UpstreamVariant =\n | {\n kind: 'url';\n url: string;\n weight: number;\n italic?: boolean;\n }\n | {\n kind: 'variable';\n url: string;\n weight: number;\n italic?: boolean;\n /** Extra axis pins merged on top of the derived `wght` pin. */\n axes?: Record<string, number>;\n };\n\nexport interface UpstreamOverride {\n /** Human-readable for logs/diagnostics only. */\n reason: string;\n variants: UpstreamVariant[];\n}\n\n/**\n * rsms/inter publishes the upright variable master as `InterVariable.ttf`\n * on jsDelivr, but the italic master ONLY as `InterVariable-Italic.woff2`\n * — no italic `.ttf` exists under `docs/font-files/` at any tag, so a\n * `.ttf` italic URL 404s and every italic Inter run silently falls back\n * to host defaults. The variable fetcher accepts woff2 sources (fontverter\n * converts to sfnt before instancing), so point at the woff2 directly.\n *\n * We instance each master at every advertised weight so Inter Thin (100),\n * ExtraLight (200), Light (300), Medium (500), SemiBold (600), ExtraBold\n * (800), and Black (900) come out with distinct glyph outlines instead of\n * the near-duplicates Google's static redistribution ships. Regular (400)\n * and Bold (700) from Google were already clean, but instancing them from\n * the same variable font keeps the full family visually consistent.\n *\n * Version pin: `@v4.1` — the last stable rsms/inter release at the time\n * of writing. jsDelivr caches the file aggressively; a version bump here\n * invalidates that cache for users on a subsequent generate.\n */\nconst INTER_VARIABLE_URL =\n 'https://cdn.jsdelivr.net/gh/rsms/inter@v4.1/docs/font-files/InterVariable.ttf';\nconst INTER_VARIABLE_ITALIC_URL =\n 'https://cdn.jsdelivr.net/gh/rsms/inter@v4.1/docs/font-files/InterVariable-Italic.woff2';\n\nfunction interVariants(): UpstreamVariant[] {\n const weights = [100, 200, 300, 400, 500, 600, 700, 800, 900];\n const upright = weights.map((weight) => ({\n kind: 'variable' as const,\n url: INTER_VARIABLE_URL,\n weight,\n italic: false,\n }));\n const italic = weights.map((weight) => ({\n kind: 'variable' as const,\n url: INTER_VARIABLE_ITALIC_URL,\n weight,\n italic: true,\n }));\n return [...upright, ...italic];\n}\n\nexport const UPSTREAM_OVERRIDES: Record<string, UpstreamOverride> = {\n inter: {\n reason:\n \"Google's static Inter Thin/ExtraLight both carry usWeightClass=250 and near-identical glyph outlines (xAvgCharWidth differs by 1.8%); instancing the upstream variable font per weight produces properly distinct statics.\",\n variants: interVariants(),\n },\n};\n\n/** Case-insensitive lookup. Returns undefined when the family has no override. */\nexport function getUpstreamOverride(\n family: string\n): UpstreamOverride | undefined {\n return UPSTREAM_OVERRIDES[family.toLowerCase()];\n}\n","/**\n * Font family substitution: rewrite every non-safe family reference in\n * the doc tree + theme to a SAFE_FONTS equivalent. Used by the\n * `'substitute'` export mode (`FontRuntimeOpts.mode`) so that non-safe\n * fonts (Playfair Display, Inter, …) ship as Georgia/Calibri and the\n * document renders identically on every recipient machine — no embed\n * bytes, no Word-for-Mac intermediate-weight surprises.\n *\n * The walker mirrors the shape used by `collectFontNamesFromDocx/Pptx`\n * so the two stay in sync: whatever `collect` scans, `rewrite` will\n * rewrite. Future component-schema additions that introduce new font\n * keys go in `FONT_NAME_KEYS` / `THEME_FONT_KEYS` once, both sides pick\n * them up.\n */\n\nimport { SAFE_FONTS, isSafeFont } from '../schemas/font-catalog';\nimport { POPULAR_GOOGLE_FONTS } from './catalog/popular-google';\nimport {\n FONT_NAME_KEYS,\n THEME_FONT_KEYS,\n FONT_DECLARATION_KEYS,\n} from './collect';\n\n/** One swap recorded during a rewrite. */\nexport interface FontSubstitution {\n from: string;\n to: string;\n}\n\nexport interface ApplyFontSubstitutionResult<T> {\n doc: T;\n substitutions: FontSubstitution[];\n}\n\n/**\n * Walk a doc tree + swap every non-safe family reference per `mapping`.\n * Returns a new tree (structural clone) plus the list of `(from, to)`\n * swaps made, deduped by source name.\n *\n * One deliberate exception to the clone: `fontRegistry` subtrees are carried\n * through by reference, since they declare fonts rather than reference them\n * and nothing downstream mutates them.\n *\n * Families already in SAFE_FONTS are never rewritten (even if a mapping\n * entry targets them as a key — safe fonts don't need substitution).\n * Families with no mapping entry are left untouched — callers should\n * feed the result of `buildDefaultSubstitutionMap` to ensure every\n * non-safe reference gets a fallback.\n */\nexport function applyFontSubstitution<T>(\n doc: T,\n mapping: Record<string, string>\n): ApplyFontSubstitutionResult<T> {\n const seen = new Map<string, string>();\n const rewritten = rewrite(doc, mapping, seen) as T;\n const substitutions: FontSubstitution[] = [];\n for (const [from, to] of seen) substitutions.push({ from, to });\n return { doc: rewritten, substitutions };\n}\n\nfunction rewrite(\n node: unknown,\n mapping: Record<string, string>,\n seen: Map<string, string>,\n parentKey?: string\n): unknown {\n if (node == null) return node;\n\n if (typeof node === 'string') {\n if (\n parentKey &&\n (FONT_NAME_KEYS.has(parentKey) || THEME_FONT_KEYS.has(parentKey))\n ) {\n return maybeSwap(node, mapping, seen);\n }\n return node;\n }\n\n if (Array.isArray(node)) {\n return node.map((item) => rewrite(item, mapping, seen, parentKey));\n }\n\n if (typeof node === 'object') {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(node as Record<string, unknown>)) {\n // Mirror of collect.ts: a fontRegistry subtree declares families, it\n // does not reference them. Rewriting `entry.family` would rename the\n // registration out from under every reference and key an entry on a\n // SAFE_FONTS name. Carry it through by reference (see the clone note\n // on applyFontSubstitution).\n if (FONT_DECLARATION_KEYS.has(k)) {\n out[k] = v;\n continue;\n }\n // Parallel to collect.ts: `theme.fonts` may hold plain strings\n // keyed by heading/body/mono/light. Those strings are font names\n // and need swapping just like `font.family` does.\n if (\n parentKey === 'theme' &&\n k === 'fonts' &&\n v &&\n typeof v === 'object'\n ) {\n const nextFonts: Record<string, unknown> = {};\n for (const [fk, fv] of Object.entries(v as Record<string, unknown>)) {\n if (typeof fv === 'string') {\n nextFonts[fk] = maybeSwap(fv, mapping, seen);\n } else if (fv && typeof fv === 'object') {\n const fam = (fv as Record<string, unknown>).family;\n nextFonts[fk] =\n typeof fam === 'string'\n ? { ...(fv as object), family: maybeSwap(fam, mapping, seen) }\n : rewrite(fv, mapping, seen, fk);\n } else {\n nextFonts[fk] = fv;\n }\n }\n out[k] = nextFonts;\n continue;\n }\n out[k] = rewrite(v, mapping, seen, k);\n }\n return out;\n }\n\n return node;\n}\n\nfunction maybeSwap(\n name: string,\n mapping: Record<string, string>,\n seen: Map<string, string>\n): string {\n const trimmed = name.trim();\n if (trimmed.length === 0) return name;\n if (isSafeFont(trimmed)) return name;\n // Case-insensitive mapping lookup. Users may feed mappings with\n // slightly different casing than the doc's reference (e.g. \"inter\" vs\n // \"Inter\") — honour the target verbatim, don't force case.\n const lowered = trimmed.toLowerCase();\n for (const [from, to] of Object.entries(mapping)) {\n if (from.toLowerCase() === lowered) {\n seen.set(trimmed, to);\n return to;\n }\n }\n return name;\n}\n\n// ---------------------------------------------------------------------------\n// Default-mapping builder\n// ---------------------------------------------------------------------------\n\n/**\n * Explicit overrides for the most common non-safe fonts we see. Chosen\n * for visual similarity: sans serifs map to Calibri, serifs to Georgia\n * or Cambria depending on axis proportion, monospace to Consolas.\n * Extendable as real usage surfaces more families.\n */\nconst EXPLICIT_OVERRIDES: Record<string, string> = {\n Inter: 'Calibri',\n Roboto: 'Calibri',\n 'Open Sans': 'Calibri',\n Lato: 'Calibri',\n 'Source Sans 3': 'Calibri',\n 'Source Sans Pro': 'Calibri',\n 'IBM Plex Sans': 'Calibri',\n Archivo: 'Calibri',\n Geist: 'Calibri',\n 'Space Grotesk': 'Calibri',\n 'Work Sans': 'Calibri',\n Manrope: 'Calibri',\n Nunito: 'Calibri',\n 'Nunito Sans': 'Calibri',\n Poppins: 'Calibri',\n Montserrat: 'Calibri',\n 'Playfair Display': 'Georgia',\n Merriweather: 'Georgia',\n 'Source Serif 4': 'Cambria',\n 'Source Serif Pro': 'Cambria',\n 'IBM Plex Serif': 'Cambria',\n 'Crimson Pro': 'Cambria',\n Lora: 'Georgia',\n 'PT Serif': 'Georgia',\n 'Cormorant Garamond': 'Cambria',\n 'JetBrains Mono': 'Consolas',\n 'Fira Code': 'Consolas',\n 'IBM Plex Mono': 'Consolas',\n 'Source Code Pro': 'Consolas',\n 'Roboto Mono': 'Consolas',\n 'Geist Mono': 'Consolas',\n};\n\nconst CATEGORY_FALLBACK: Record<string, string> = {\n sans: 'Calibri',\n serif: 'Georgia',\n mono: 'Consolas',\n display: 'Georgia',\n handwriting: 'Segoe UI',\n};\n\n/**\n * Pick the safe-font fallback for a single non-safe family. Precedence:\n * 1. Explicit override in `EXPLICIT_OVERRIDES`.\n * 2. Category lookup in `POPULAR_GOOGLE_FONTS`.\n * 3. Final default (`Calibri`).\n *\n * Exposed for the playground dialog so it can pre-populate the per-family\n * picker with the same defaults the CLI would apply.\n */\nexport function defaultSubstituteFor(family: string): string {\n const trimmed = family.trim();\n // Explicit (case-insensitive).\n for (const [from, to] of Object.entries(EXPLICIT_OVERRIDES)) {\n if (from.toLowerCase() === trimmed.toLowerCase()) return to;\n }\n // Category.\n const catalog = POPULAR_GOOGLE_FONTS.find(\n (f) => f.family.toLowerCase() === trimmed.toLowerCase()\n );\n if (catalog) {\n const cat = CATEGORY_FALLBACK[catalog.category];\n if (cat) return cat;\n }\n return 'Calibri';\n}\n\n/**\n * Build a substitution map for every non-safe family in `referencedNames`.\n * Safe fonts are omitted from the result since they don't need swapping.\n * Caller can override individual entries before passing to\n * `applyFontSubstitution`.\n */\nexport function buildDefaultSubstitutionMap(\n referencedNames: Iterable<string>\n): Record<string, string> {\n const out: Record<string, string> = {};\n for (const raw of referencedNames) {\n const name = raw.trim();\n if (name.length === 0) continue;\n if (isSafeFont(name)) continue;\n if (out[name]) continue;\n out[name] = defaultSubstituteFor(name);\n }\n return out;\n}\n\n/** Re-export SAFE_FONTS for dialog/CLI consumers that need the allowlist. */\nexport { SAFE_FONTS };\n\n// ---------------------------------------------------------------------------\n// Export-mode pre-pass\n// ---------------------------------------------------------------------------\n\nimport type { FontRuntimeOpts } from './types';\nimport { collectFontNames } from './collect';\n\nexport interface ApplyExportModeInput<D, T> {\n doc: D;\n theme: T;\n fonts?: FontRuntimeOpts;\n}\n\nexport interface ApplyExportModeWarning {\n code: 'FONT_MODE_CUSTOM' | 'FONT_MODE_SUBSTITUTED';\n message: string;\n}\n\nexport interface ApplyExportModeResult<D, T> {\n doc: D;\n theme: T;\n warnings: ApplyExportModeWarning[];\n}\n\n/**\n * Inspect `fonts.mode` and apply the pre-resolution rewrite for the\n * requested mode.\n *\n * - `'custom'` (default) — no rewrite. Font references stay as authored;\n * recipients need the font installed or Word falls back. The\n * LibreOffice preview stager registers resolved bytes so preview\n * fidelity matches the recipient-side experience when the font is\n * installed.\n * - `'substitute'` — rewrite every non-safe family in doc + theme to its\n * mapped safe equivalent. Fills in defaults via\n * `buildDefaultSubstitutionMap` for any non-safe reference not present\n * in `fonts.substitution`. Emits one `FONT_MODE_SUBSTITUTED` warning\n * listing every swap.\n */\nexport function applyExportMode<D, T>(\n input: ApplyExportModeInput<D, T>\n): ApplyExportModeResult<D, T> {\n const mode = input.fonts?.mode ?? 'custom';\n if (mode === 'custom') {\n // Suppress the advisory entirely when callers never passed a `fonts`\n // option: they opted out of font-mode handling, so noisy per-run\n // warnings would flood existing callers that predate the pipeline.\n if (!input.fonts) {\n return { doc: input.doc, theme: input.theme, warnings: [] };\n }\n // Only emit the advisory when the doc/theme actually references a\n // non-safe family — a safe-only doc has nothing for recipients to be\n // missing, and the warning would be noise.\n const referenced = new Set<string>([\n ...collectFontNames(input.doc),\n ...collectFontNames(input.theme),\n ]);\n const nonSafe = [...referenced].filter((name) => !isSafeFont(name.trim()));\n const warnings: ApplyExportModeWarning[] =\n nonSafe.length > 0\n ? [\n {\n code: 'FONT_MODE_CUSTOM',\n message: `Export mode \"custom\": non-safe font references (${nonSafe.join(', ')}) kept as-is. Recipients need these fonts installed locally; Word falls back to a generic substitute otherwise.`,\n },\n ]\n : [];\n return {\n doc: input.doc,\n theme: input.theme,\n warnings,\n };\n }\n // mode === 'substitute'\n const referenced = new Set<string>([\n ...collectFontNames(input.doc),\n ...collectFontNames(input.theme),\n ]);\n const defaults = buildDefaultSubstitutionMap(referenced);\n const mapping: Record<string, string> = {\n ...defaults,\n ...(input.fonts?.substitution ?? {}),\n };\n\n const docRewrite = applyFontSubstitution(input.doc, mapping);\n const themeRewrite = applyFontSubstitution(input.theme, mapping);\n const combined = new Map<string, string>();\n for (const s of docRewrite.substitutions) combined.set(s.from, s.to);\n for (const s of themeRewrite.substitutions) combined.set(s.from, s.to);\n\n const warnings: ApplyExportModeWarning[] = [];\n if (combined.size > 0) {\n const list = [...combined]\n .map(([from, to]) => `${from} → ${to}`)\n .join(', ');\n warnings.push({\n code: 'FONT_MODE_SUBSTITUTED',\n message: `Export mode \"substitute\": rewrote non-safe families to safe equivalents — ${list}. No fonts embedded; document renders identically on every machine.`,\n });\n }\n return {\n doc: docRewrite.doc,\n theme: themeRewrite.doc,\n warnings,\n };\n}\n","/**\n * Default series-color tokens for charts. Single source of truth for every\n * format: the PPTX `chart` and `highcharts` components and the DOCX\n * `highcharts` component all resolve this list, in this order, against the\n * active theme when the author sets no explicit colors. Both theme schemas\n * declare all six tokens (accent4-6 optional in each), so a theme that fills\n * every slot produces the same palette in a deck and in a document.\n *\n * Slots the theme leaves unset are skipped in both formats: the implicit\n * palette shrinks and the chart library cycles the shorter list rather than\n * repeating `primary` for every empty slot. A theme carrying only\n * primary/secondary/accent — which is what the bundled DOCX themes carry —\n * therefore paints series 4+ identically in a deck and in a document.\n *\n * Skipping compacts holes: a theme defining accent5 but not accent4 yields\n * [primary, secondary, accent, accent5], so accent5 paints series 4. The list\n * is a preference-ordered pool of candidate colors, not fixed per-series slots,\n * so keeping a color the theme did define beats dropping or duplicating one.\n *\n * A slot may also hold another token's name (`\"accent4\": \"primary\"`) — both\n * theme schemas allow it — and both formats walk that reference to hex before\n * using it, so a chained slot lands on the same color in a deck as in a\n * document. A slot whose value reaches no hex (`\"accent4\": \"nonsense\"`, or a\n * reference cycle) is dropped from the implicit palette in both formats rather\n * than emitted verbatim: PowerPoint and Highcharts both answer an unparseable\n * color with silent black. Parity here covers the token names the two schemas\n * share; each format also has private color keys (DOCX `textSecondary`, PPTX\n * `text2`) that only resolve in their own format.\n *\n * Only the implicit palette skips. An author who names a token explicitly\n * (PPTX `chartColors: ['accent4']`) still gets the `primary` fallback and a\n * warning — naming an undefined token is an authoring error and stays loud.\n * PPTX warns THEME_COLOR_FALLBACK for an unset slot and UNKNOWN_COLOR for one\n * holding an unresolvable value; DOCX throws.\n */\nexport const DEFAULT_CHART_THEME_COLORS = [\n 'primary',\n 'secondary',\n 'accent',\n 'accent4',\n 'accent5',\n 'accent6',\n];\n","/**\n * Deep Merge Utilities\n * Generic deep-merge helpers used by both docx and pptx\n * componentDefaults resolution systems.\n */\n\nfunction isObject(item: any): boolean {\n return item !== null && typeof item === 'object' && !Array.isArray(item);\n}\n\nfunction deepMerge<T>(target: any, source: any): T {\n const output = { ...target };\n\n if (isObject(target) && isObject(source)) {\n Object.keys(source).forEach((key) => {\n if (isObject(source[key])) {\n // Recursing into a non-object default would spread it — a scalar\n // theme default (`borderColor: '#f0f0f0'`) turned a user's per-side\n // object into `{0: '#', 1: 'f', …}`, silently erasing the user value\n // it was supposed to yield to.\n if (!(key in target) || !isObject(target[key])) {\n output[key] = source[key];\n } else {\n output[key] = deepMerge(target[key], source[key]);\n }\n } else {\n output[key] = source[key];\n }\n });\n }\n\n return output as T;\n}\n\n/**\n * Merge theme defaults with user-provided configuration.\n * User config takes precedence over theme defaults.\n * Uses deep merge to preserve nested objects.\n * Arrays are replaced wholesale, not merged per-element.\n */\nexport function mergeWithDefaults<T>(\n userConfig: T,\n themeDefaults: Partial<T>\n): T {\n return deepMerge<T>(themeDefaults, userConfig);\n}\n"],"mappings":"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","names":["cacheKey","HEADER_SIZE","TABLE_RECORD_SIZE","referenced","warnings"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@json-to-office/shared",
3
- "version": "1.5.0",
3
+ "version": "1.11.2",
4
4
  "description": "Format-agnostic shared types, schemas and validation utilities",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -52,7 +52,7 @@
52
52
  "ajv": "8.17.1",
53
53
  "ajv-formats": "3.0.1",
54
54
  "subset-font": "^2.5.0",
55
- "@json-to-office/quality": "^1.5.0"
55
+ "@json-to-office/quality": "^1.9.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@types/node": "20.11.0",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/fonts/sources/ttf-name.ts","../src/fonts/sources/url-allowlist.ts","../src/fonts/sources/format.ts"],"sourcesContent":["/**\n * Name-table rewriting for TTF/OTF sfnt fonts. Two public transforms share\n * the rebuild machinery:\n *\n * - `rewriteFontFamilyName` — rewrite `nameID` 1 / 4 / 6 / 16 to a\n * synthetic family name. Used by the preview-side font stagers so that\n * running-text references like `\"Inter Light\"` resolve to the correct\n * face when the stager registers it with Core Text / fontconfig / GDI\n * (all of which index by the font's internal `name` table rather than\n * the filename).\n *\n * - `rewriteFontSubfamilyNames` — rewrite `nameID` 2 / 17 to the standard\n * subfamily strings for a (weight, italic) pair. Used by the variable-\n * font instancer: harfbuzz preserves the source font's name records\n * verbatim, so an instanced Bold would otherwise keep the variable\n * font's default-instance \"Regular\" subfamily and trip\n * `validateFontMetadata`.\n *\n * Each transform rebuilds the whole font: new `name` table bytes, new\n * table directory with shifted offsets, recomputed per-table checksums,\n * and the magic `head.checkSumAdjustment` recomputed against the whole\n * output buffer. Nothing else is touched.\n *\n * OTF (CFF-flavoured) and TTF (glyf-flavoured) share the sfnt outer\n * structure, so the same code handles both.\n */\n\nconst FAMILY_NAME_IDS = new Set<number>([\n 1, // Font Family\n 4, // Full Font Name\n 6, // PostScript Name\n 16, // Typographic/Preferred Family\n]);\n\nconst MAGIC_HEAD_CHECKSUM = 0xb1b0afba;\n\ninterface NameRecord {\n platformID: number;\n encodingID: number;\n languageID: number;\n nameID: number;\n bytes: Buffer;\n}\n\ninterface TableEntry {\n tag: string;\n checksum: number;\n /** Assigned when rebuilding. */\n offset: number;\n data: Buffer;\n originalOffset: number;\n}\n\n/**\n * Compute the 32-bit big-endian uint sum of `buf`, treating it as a\n * stream of uint32s zero-padded to a 4-byte boundary. Used for per-table\n * checksums and the whole-font `head.checkSumAdjustment`.\n */\nfunction sfntChecksum(buf: Buffer): number {\n let sum = 0;\n const end = buf.length;\n const aligned = end - (end % 4);\n for (let i = 0; i < aligned; i += 4) {\n sum = (sum + buf.readUInt32BE(i)) >>> 0;\n }\n if (aligned < end) {\n let chunk = 0;\n const remaining = end - aligned;\n if (remaining >= 1) chunk |= buf[aligned] << 24;\n if (remaining >= 2) chunk |= buf[aligned + 1] << 16;\n if (remaining >= 3) chunk |= buf[aligned + 2] << 8;\n sum = (sum + chunk) >>> 0;\n }\n return sum >>> 0;\n}\n\nfunction encodeString(record: NameRecord, value: string): Buffer {\n // Platform 3 (Microsoft) and 0 (Unicode) use UTF-16 BE. Platform 1\n // (Macintosh) uses a legacy Roman encoding we approximate with ASCII —\n // non-ASCII family names are rare in this code path.\n if (record.platformID === 1) {\n return Buffer.from(value, 'ascii');\n }\n const out = Buffer.alloc(value.length * 2);\n for (let i = 0; i < value.length; i += 1) {\n out.writeUInt16BE(value.charCodeAt(i), i * 2);\n }\n return out;\n}\n\nfunction buildNameTable(records: NameRecord[]): Buffer {\n const count = records.length;\n const headerSize = 6 + count * 12;\n let heapSize = 0;\n for (const r of records) heapSize += r.bytes.length;\n const raw = Buffer.alloc(headerSize + heapSize);\n raw.writeUInt16BE(0, 0); // format 0\n raw.writeUInt16BE(count, 2);\n raw.writeUInt16BE(headerSize, 4); // stringOffset\n let heapCursor = 0;\n for (let i = 0; i < count; i += 1) {\n const r = records[i];\n const ro = 6 + i * 12;\n raw.writeUInt16BE(r.platformID, ro);\n raw.writeUInt16BE(r.encodingID, ro + 2);\n raw.writeUInt16BE(r.languageID, ro + 4);\n raw.writeUInt16BE(r.nameID, ro + 6);\n raw.writeUInt16BE(r.bytes.length, ro + 8);\n raw.writeUInt16BE(heapCursor, ro + 10);\n r.bytes.copy(raw, headerSize + heapCursor);\n heapCursor += r.bytes.length;\n }\n return raw;\n}\n\n/** Per-record decision for `rewriteNameTable`. */\ntype NameRewrite =\n | { action: 'keep' }\n | { action: 'drop' }\n | { action: 'set'; value: string };\n\n/**\n * Return a copy of `input` whose name records have been mapped through\n * `decide`. Returns the original buffer unchanged if the font has no\n * `name` table or the sfnt header is invalid.\n */\nfunction rewriteNameTable(\n input: Buffer,\n decide: (record: NameRecord) => NameRewrite\n): Buffer {\n if (input.length < 12) return input;\n const version = input.readUInt32BE(0);\n // Accept sfnt (0x00010000), OTTO (OpenType CFF), true, typ1.\n const isSfnt =\n version === 0x00010000 ||\n version === 0x4f54544f /* OTTO */ ||\n version === 0x74727565 /* true */ ||\n version === 0x74797031; /* typ1 */\n if (!isSfnt) return input;\n\n const numTables = input.readUInt16BE(4);\n if (numTables === 0 || input.length < 12 + numTables * 16) return input;\n\n // Read every table's directory entry, slurp its data.\n const tables: TableEntry[] = [];\n for (let i = 0; i < numTables; i += 1) {\n const eo = 12 + i * 16;\n const tag = input.toString('ascii', eo, eo + 4);\n const checksum = input.readUInt32BE(eo + 4);\n const offset = input.readUInt32BE(eo + 8);\n const length = input.readUInt32BE(eo + 12);\n if (offset + length > input.length) return input;\n tables.push({\n tag,\n checksum,\n offset: 0,\n originalOffset: offset,\n data: input.slice(offset, offset + length),\n });\n }\n\n const nameIdx = tables.findIndex((t) => t.tag === 'name');\n if (nameIdx === -1) return input;\n\n // Parse existing name records so we preserve all non-family entries.\n const nameBuf = tables[nameIdx].data;\n if (nameBuf.length < 6) return input;\n // `buildNameTable` emits format 0, which has no language-tag section. A\n // format-1 table keeps its tags after the name records, and any record with\n // languageID >= 0x8000 is an index into them — rewriting it as format 0\n // would strip the tags and leave those records pointing at nothing. Leaving\n // the font unstamped is the lesser loss, so hand it back untouched.\n // Preserving the tag section (and re-homing its string offsets) is the\n // follow-up if a real font ever needs the rewrite.\n if (nameBuf.readUInt16BE(0) !== 0) return input;\n const recordCount = nameBuf.readUInt16BE(2);\n const stringOffset = nameBuf.readUInt16BE(4);\n if (nameBuf.length < 6 + recordCount * 12) return input;\n\n const records: NameRecord[] = [];\n for (let i = 0; i < recordCount; i += 1) {\n const ro = 6 + i * 12;\n const platformID = nameBuf.readUInt16BE(ro);\n const encodingID = nameBuf.readUInt16BE(ro + 2);\n const languageID = nameBuf.readUInt16BE(ro + 4);\n const nameID = nameBuf.readUInt16BE(ro + 6);\n const length = nameBuf.readUInt16BE(ro + 8);\n const offset = nameBuf.readUInt16BE(ro + 10);\n const bytes = nameBuf.slice(\n stringOffset + offset,\n stringOffset + offset + length\n );\n records.push({ platformID, encodingID, languageID, nameID, bytes });\n }\n\n const survivors: NameRecord[] = [];\n for (const r of records) {\n const verdict = decide(r);\n if (verdict.action === 'drop') continue;\n if (verdict.action === 'set') {\n r.bytes = encodeString(r, verdict.value);\n }\n survivors.push(r);\n }\n records.length = 0;\n records.push(...survivors);\n\n const rebuiltName = buildNameTable(records);\n tables[nameIdx] = {\n ...tables[nameIdx],\n data: rebuiltName,\n checksum: sfntChecksum(rebuiltName),\n };\n\n // Preserve original physical order so tables whose offsets follow each\n // other stay contiguous (some consumers skim by offset rather than\n // directory). Offsets get reassigned either way — this is purely\n // aesthetic. Sort is stable.\n tables.sort((a, b) => a.originalOffset - b.originalOffset);\n\n // Assign new offsets, 4-byte aligned.\n let cursor = 12 + tables.length * 16;\n for (const t of tables) {\n cursor = (cursor + 3) & ~3;\n t.offset = cursor;\n cursor += t.data.length;\n }\n const totalSize = (cursor + 3) & ~3;\n\n const out = Buffer.alloc(totalSize);\n // Header — copy sfnt version, entrySelector, etc. verbatim; we preserve\n // numTables since we're not adding/removing entries.\n input.copy(out, 0, 0, 12);\n out.writeUInt16BE(tables.length, 4);\n\n // Directory entries go in alphabetical tag order per the sfnt spec.\n const dirTables = [...tables].sort((a, b) =>\n a.tag < b.tag ? -1 : a.tag > b.tag ? 1 : 0\n );\n for (let i = 0; i < dirTables.length; i += 1) {\n const t = dirTables[i];\n const eo = 12 + i * 16;\n out.write(t.tag, eo, 4, 'ascii');\n out.writeUInt32BE(t.checksum, eo + 4);\n out.writeUInt32BE(t.offset, eo + 8);\n out.writeUInt32BE(t.data.length, eo + 12);\n }\n\n // Write each table's bytes at its new offset. `out` is zero-filled, so\n // the 0–3 byte alignment padding after each table is already correct.\n for (const t of tables) {\n t.data.copy(out, t.offset);\n }\n\n // Recompute head.checkSumAdjustment. The algorithm: zero the field,\n // sum the whole font, then set the field to MAGIC - sum.\n const headTable = tables.find((t) => t.tag === 'head');\n if (headTable && headTable.data.length >= 12) {\n out.writeUInt32BE(0, headTable.offset + 8);\n const fontSum = sfntChecksum(out);\n const adjustment = (MAGIC_HEAD_CHECKSUM - fontSum) >>> 0;\n out.writeUInt32BE(adjustment, headTable.offset + 8);\n }\n\n return out;\n}\n\n/**\n * Return a copy of `input` whose name table has `nameID` 1/4/6/16 rewritten\n * to `newFamily`. Returns the original buffer unchanged if the font has no\n * `name` table or the sfnt header is invalid.\n */\nexport function rewriteFontFamilyName(\n input: Buffer,\n newFamily: string\n): Buffer {\n // PostScript names (nameID 6) are restricted to printable ASCII 33-126\n // minus `[](){}<>/%` per the OpenType spec — fold spaces out and strip\n // any forbidden chars so Word doesn't silently reject the font.\n const psForbidden = /[[\\](){}<>/%]/g;\n // eslint-disable-next-line no-control-regex\n const isAscii = /^[\\x00-\\x7f]*$/.test(newFamily);\n const psName = newFamily\n .replace(/\\s+/g, '')\n .replace(psForbidden, '')\n // eslint-disable-next-line no-control-regex\n .replace(/[^\\x21-\\x7e]/g, '');\n return rewriteNameTable(input, (r) => {\n if (!FAMILY_NAME_IDS.has(r.nameID)) return { action: 'keep' };\n // Platform 1 (Macintosh Roman) only round-trips ASCII. For non-ASCII\n // family names (e.g. CJK), `Buffer.from(value, 'ascii')` silently\n // drops the high bytes and produces garbled Roman-encoded strings\n // that Core Text may still index. Drop those records instead —\n // platforms 0 (Unicode) and 3 (Microsoft) carry the UTF-16 form and\n // are what modern consumers prefer anyway.\n if (r.platformID === 1 && !isAscii) return { action: 'drop' };\n return { action: 'set', value: r.nameID === 6 ? psName : newFamily };\n });\n}\n\nconst STANDARD_SUBFAMILY: Record<number, string> = {\n 100: 'Thin',\n 200: 'ExtraLight',\n 300: 'Light',\n 400: 'Regular',\n 500: 'Medium',\n 600: 'SemiBold',\n 700: 'Bold',\n 800: 'ExtraBold',\n 900: 'Black',\n};\n\n/**\n * Standard OpenType subfamily strings for a (weight, italic) pair, or null\n * for a non-standard weight. `typographic` is the nameID 17 form (full\n * weight vocabulary); `legacy` is the nameID 2 form, restricted to the\n * four-style RIBBI model (Regular/Italic/Bold/Bold Italic) that GDI-era\n * consumers expect. Single source of truth shared with\n * `validateFontMetadata` so writer and checker cannot diverge.\n */\nexport function standardSubfamilyNames(\n weight: number,\n italic: boolean\n): { typographic: string; legacy: string } | null {\n const base = STANDARD_SUBFAMILY[weight];\n if (!base) return null;\n return {\n typographic: italic ? `${base} Italic` : base,\n legacy:\n weight >= 600\n ? italic\n ? 'Bold Italic'\n : 'Bold'\n : italic\n ? 'Italic'\n : 'Regular',\n };\n}\n\n/**\n * Return a copy of `input` whose existing `nameID` 2/17 records carry the\n * standard subfamily strings for (weight, italic). Missing records are not\n * added. Returns the original buffer unchanged for non-standard weights,\n * or if the font has no `name` table or the sfnt header is invalid.\n */\nexport function rewriteFontSubfamilyNames(\n input: Buffer,\n weight: number,\n italic: boolean\n): Buffer {\n const std = standardSubfamilyNames(weight, italic);\n if (!std) return input;\n return rewriteNameTable(input, (r) => {\n if (r.nameID === 2) return { action: 'set', value: std.legacy };\n if (r.nameID === 17) return { action: 'set', value: std.typographic };\n return { action: 'keep' };\n });\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","/**\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"],"mappings":"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","names":[]}