@json-to-office/core-docx 7.5.1 → 7.6.4

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.
@@ -1666,6 +1666,46 @@ var init_themes = __esm({
1666
1666
  }
1667
1667
  });
1668
1668
 
1669
+ // src/utils/generationContext.ts
1670
+ import { AsyncLocalStorage } from "async_hooks";
1671
+ import { isAbsolute, resolve } from "path";
1672
+ function runWithBaseDir(baseDir, callback) {
1673
+ return baseDir === void 0 ? callback() : baseDirStorage.run(resolve(baseDir), callback);
1674
+ }
1675
+ function getBaseDir() {
1676
+ return baseDirStorage.getStore();
1677
+ }
1678
+ function resolveFromBaseDir(filePath) {
1679
+ const base = baseDirStorage.getStore();
1680
+ if (!base || isAbsolute(filePath)) return filePath;
1681
+ return resolve(base, filePath);
1682
+ }
1683
+ function runWithWarnings(warnings, callback) {
1684
+ return warnings === void 0 ? callback() : warningsStorage.run(warnings, callback);
1685
+ }
1686
+ function reportWarning(component, code, message, context) {
1687
+ const warnings = warningsStorage.getStore();
1688
+ if (warnings) {
1689
+ warnings.push({
1690
+ component,
1691
+ message,
1692
+ severity: "warning",
1693
+ context: { code, ...context }
1694
+ });
1695
+ return;
1696
+ }
1697
+ console.warn(`[json-to-docx] [${code}] ${message}`);
1698
+ }
1699
+ var generationDateStorage, baseDirStorage, warningsStorage;
1700
+ var init_generationContext = __esm({
1701
+ "src/utils/generationContext.ts"() {
1702
+ "use strict";
1703
+ generationDateStorage = new AsyncLocalStorage();
1704
+ baseDirStorage = new AsyncLocalStorage();
1705
+ warningsStorage = new AsyncLocalStorage();
1706
+ }
1707
+ });
1708
+
1669
1709
  // src/styles/utils/colorUtils.ts
1670
1710
  import {
1671
1711
  DEFAULT_CHART_THEME_COLORS,
@@ -2419,43 +2459,6 @@ var init_widthUtils = __esm({
2419
2459
  }
2420
2460
  });
2421
2461
 
2422
- // src/utils/generationContext.ts
2423
- import { AsyncLocalStorage } from "async_hooks";
2424
- import { isAbsolute, resolve } from "path";
2425
- function runWithBaseDir(baseDir, callback) {
2426
- return baseDir === void 0 ? callback() : baseDirStorage.run(resolve(baseDir), callback);
2427
- }
2428
- function resolveFromBaseDir(filePath) {
2429
- const base = baseDirStorage.getStore();
2430
- if (!base || isAbsolute(filePath)) return filePath;
2431
- return resolve(base, filePath);
2432
- }
2433
- function runWithWarnings(warnings, callback) {
2434
- return warnings === void 0 ? callback() : warningsStorage.run(warnings, callback);
2435
- }
2436
- function reportWarning(component, code, message, context) {
2437
- const warnings = warningsStorage.getStore();
2438
- if (warnings) {
2439
- warnings.push({
2440
- component,
2441
- message,
2442
- severity: "warning",
2443
- context: { code, ...context }
2444
- });
2445
- return;
2446
- }
2447
- console.warn(`[json-to-docx] [${code}] ${message}`);
2448
- }
2449
- var generationDateStorage, baseDirStorage, warningsStorage;
2450
- var init_generationContext = __esm({
2451
- "src/utils/generationContext.ts"() {
2452
- "use strict";
2453
- generationDateStorage = new AsyncLocalStorage();
2454
- baseDirStorage = new AsyncLocalStorage();
2455
- warningsStorage = new AsyncLocalStorage();
2456
- }
2457
- });
2458
-
2459
2462
  // src/utils/imageUtils.ts
2460
2463
  import { readFileSync } from "fs";
2461
2464
  import { availableParallelism } from "os";
@@ -4237,6 +4240,8 @@ var init_normalizeTextCase = __esm({
4237
4240
 
4238
4241
  // src/utils/packageDocument.ts
4239
4242
  import AdmZip from "adm-zip";
4243
+ import { createHash as createHash3 } from "crypto";
4244
+ import { posix } from "path";
4240
4245
  function resolveGenerationDate(options) {
4241
4246
  if (options?.generatedAt !== void 0) {
4242
4247
  const date = new Date(options.generatedAt);
@@ -4304,6 +4309,70 @@ function canonicalizeRelationshipIds(zip) {
4304
4309
  if (entry) zip.updateFile(entry, Buffer.from(update.xml, "utf8"));
4305
4310
  }
4306
4311
  }
4312
+ function xorFontKey(bytes, guid) {
4313
+ const key = Buffer.from(guid.replace(/-/g, ""), "hex").reverse();
4314
+ const out = Buffer.from(bytes);
4315
+ for (let i = 0; i < Math.min(32, out.length); i++) out[i] ^= key[i % 16];
4316
+ return out;
4317
+ }
4318
+ function canonicalizeEmbeddedFontKeys(zip) {
4319
+ const table2 = zip.getEntry("word/fontTable.xml");
4320
+ if (!table2) return;
4321
+ const xml = table2.getData().toString("utf8");
4322
+ const embeds = [...xml.matchAll(FONT_EMBED)].flatMap(([element]) => {
4323
+ const id = /\br:id="([^"]+)"/.exec(element)?.[1];
4324
+ const key = /\bw:fontKey="\{([0-9A-Fa-f-]{36})\}"/.exec(element)?.[1];
4325
+ return id && key ? [{ id, key }] : [];
4326
+ });
4327
+ if (embeds.length === 0) return;
4328
+ const rels = zip.getEntry("word/_rels/fontTable.xml.rels")?.getData().toString("utf8") ?? "";
4329
+ const targets = /* @__PURE__ */ new Map();
4330
+ for (const [relationship] of rels.matchAll(/<Relationship\b[^>]*>/g)) {
4331
+ const id = /\bId="([^"]+)"/.exec(relationship)?.[1];
4332
+ const target = /\bTarget="([^"]+)"/.exec(relationship)?.[1];
4333
+ if (id && target) targets.set(id, target);
4334
+ }
4335
+ const rekey = /* @__PURE__ */ new Map();
4336
+ const parts = [];
4337
+ for (const { id, key } of embeds) {
4338
+ const target = targets.get(id);
4339
+ if (!target) continue;
4340
+ const name = target.startsWith("/") ? target.slice(1) : posix.normalize(posix.join("word", target));
4341
+ const part = zip.getEntry(name);
4342
+ if (!part) continue;
4343
+ const font = xorFontKey(part.getData(), key);
4344
+ const hex = createHash3("sha256").update(font).digest("hex").slice(0, 32).toUpperCase();
4345
+ const fresh = [
4346
+ hex.slice(0, 8),
4347
+ hex.slice(8, 12),
4348
+ hex.slice(12, 16),
4349
+ hex.slice(16, 20),
4350
+ hex.slice(20)
4351
+ ].join("-");
4352
+ rekey.set(key.toUpperCase(), fresh);
4353
+ parts.push({ name, data: xorFontKey(font, fresh) });
4354
+ }
4355
+ if (rekey.size === 0) return;
4356
+ for (const { name, data } of parts) {
4357
+ const entry = zip.getEntry(name);
4358
+ if (entry) zip.updateFile(entry, data);
4359
+ }
4360
+ const fontTable = zip.getEntry("word/fontTable.xml");
4361
+ if (!fontTable) return;
4362
+ zip.updateFile(
4363
+ fontTable,
4364
+ Buffer.from(
4365
+ xml.replace(
4366
+ /\bw:fontKey="\{([0-9A-Fa-f-]{36})\}"/g,
4367
+ (whole, key) => {
4368
+ const fresh = rekey.get(key.toUpperCase());
4369
+ return fresh ? `w:fontKey="{${fresh}}"` : whole;
4370
+ }
4371
+ ),
4372
+ "utf8"
4373
+ )
4374
+ );
4375
+ }
4307
4376
  function canonicalizeEmbeddedPackages(zip, generatedAt, depth = 1) {
4308
4377
  const names = zip.getEntries().map((entry) => entry.entryName).filter((name) => EMBEDDED_OFFICE_PACKAGE.test(name));
4309
4378
  let changed = false;
@@ -4356,6 +4425,7 @@ function canonicalizeDocxBuffer(buffer, generatedAt = DEFAULT_GENERATION_DATE) {
4356
4425
  normalizeTextCase(zip);
4357
4426
  canonicalizeRelationshipIds(zip);
4358
4427
  canonicalizeEmbeddedPackages(zip, generatedAt);
4428
+ canonicalizeEmbeddedFontKeys(zip);
4359
4429
  const isoTimestamp = generatedAt.toISOString();
4360
4430
  const coreProperties3 = zip.getEntry("docProps/core.xml");
4361
4431
  if (coreProperties3) {
@@ -4372,13 +4442,14 @@ function normalizeDocxCaseBuffer(buffer) {
4372
4442
  const zip = new AdmZip(buffer);
4373
4443
  return normalizeTextCase(zip) ? zip.toBuffer() : buffer;
4374
4444
  }
4375
- var DEFAULT_GENERATION_DATE, STABLE_RELATIONSHIP_ID, CORE_TIMESTAMP, EMBEDDED_OFFICE_PACKAGE, MAX_EMBEDDED_DEPTH;
4445
+ var DEFAULT_GENERATION_DATE, STABLE_RELATIONSHIP_ID, FONT_EMBED, CORE_TIMESTAMP, EMBEDDED_OFFICE_PACKAGE, MAX_EMBEDDED_DEPTH;
4376
4446
  var init_packageDocument = __esm({
4377
4447
  "src/utils/packageDocument.ts"() {
4378
4448
  "use strict";
4379
4449
  init_normalizeTextCase();
4380
4450
  DEFAULT_GENERATION_DATE = /* @__PURE__ */ new Date("2000-01-01T00:00:00.000Z");
4381
4451
  STABLE_RELATIONSHIP_ID = /^rId\d+$/;
4452
+ FONT_EMBED = /<w:embed(?:Regular|Bold|Italic|BoldItalic)\b[^>]*>/g;
4382
4453
  CORE_TIMESTAMP = /(<dcterms:(?:created|modified)\b[^>]*>)[^<]*(<\/dcterms:(?:created|modified)>)/g;
4383
4454
  EMBEDDED_OFFICE_PACKAGE = /\.(?:docx|pptx|xlsx|xlsm)$/i;
4384
4455
  MAX_EMBEDDED_DEPTH = 3;
@@ -4979,7 +5050,12 @@ function createDocxJsRenderer() {
4979
5050
  });
4980
5051
  }
4981
5052
  const pageNumbers = needsPageNumbers(ir) ? await createPageNumberEstimator(ir) : void 0;
4982
- const document = buildDocument(ir, resources, pageNumbers);
5053
+ const document = buildDocument(
5054
+ ir,
5055
+ resources,
5056
+ pageNumbers,
5057
+ renderOptions?.embeddedFonts
5058
+ );
4983
5059
  const packed = await Packer.toBuffer(document);
4984
5060
  const fixed = fixFloatingImageIdsInBuffer(packed);
4985
5061
  const repaired = markers ? repairDrawingGroupsInBuffer(fixed, markers) : fixed;
@@ -4997,10 +5073,17 @@ function createDocxJsRenderer() {
4997
5073
  }
4998
5074
  };
4999
5075
  }
5000
- function buildDocument(ir, resources = /* @__PURE__ */ new Map(), pageNumbers) {
5076
+ function buildDocument(ir, resources = /* @__PURE__ */ new Map(), pageNumbers, embeddedFonts) {
5001
5077
  return new Document({
5002
5078
  ...pageNumbers ? { pageNumbers } : {},
5003
5079
  styles: emitStyles(ir.styles),
5080
+ // word/fonts/font<N>.odttf, their `w:embed*` entries in the font table and
5081
+ // `w:embedTrueTypeFonts` in the settings, all from this one key. Passed
5082
+ // only when a face embeds, so a document in any other mode is built from
5083
+ // exactly the options it always was. `docx/layout` measures the text of an
5084
+ // embedded family from these files, so a contents field's page numbers
5085
+ // need nothing more.
5086
+ ...embeddedFonts && embeddedFonts.length > 0 ? { fonts: embeddedFonts.map(fontOptions) } : {},
5004
5087
  ...ir.theme ? { theme: themeOptions(ir.theme) } : {},
5005
5088
  sections: ir.sections.map((section2) => sectionOptions(section2, resources)),
5006
5089
  ...coreProperties(ir),
@@ -5031,6 +5114,15 @@ function buildDocument(ir, resources = /* @__PURE__ */ new Map(), pageNumbers) {
5031
5114
  } : {}
5032
5115
  });
5033
5116
  }
5117
+ function fontOptions(face) {
5118
+ return {
5119
+ name: face.family,
5120
+ data: face.regular,
5121
+ ...face.bold ? { bold: face.bold } : {},
5122
+ ...face.italic ? { italic: face.italic } : {},
5123
+ ...face.boldItalic ? { boldItalic: face.boldItalic } : {}
5124
+ };
5125
+ }
5034
5126
  function themeOptions(theme) {
5035
5127
  const colors = {};
5036
5128
  for (const [slot, color] of Object.entries(theme.colors)) {
@@ -7251,6 +7343,7 @@ async function exportPluginSchema(customComponents, outputPath, options = {}) {
7251
7343
  import { assertRendererSupports } from "@json-to-office/shared/rendering";
7252
7344
 
7253
7345
  // src/core/fontResolution.ts
7346
+ init_generationContext();
7254
7347
  import {
7255
7348
  collectFontNamesFromDocx,
7256
7349
  validateFontReferences,
@@ -7304,8 +7397,14 @@ async function resolveDocumentFonts(document, theme, fonts, warnings, forceMater
7304
7397
  if (!fonts?.onResolved && !forceMaterialize) return [];
7305
7398
  const registry2 = new FontRegistry({
7306
7399
  // Spread keeps baseDir/googleFonts/mode/substitution intact for
7307
- // materializeSource; extraEntries carries the merged registry.
7308
- opts: { ...fonts, extraEntries: registryEntries },
7400
+ // materializeSource; extraEntries carries the merged registry. Without a
7401
+ // `fonts.baseDir` of its own, kind:"file" sources resolve against the
7402
+ // render's baseDir, as images do — not against process.cwd().
7403
+ opts: {
7404
+ ...fonts,
7405
+ baseDir: fonts?.baseDir ?? getBaseDir(),
7406
+ extraEntries: registryEntries
7407
+ },
7309
7408
  fileLoader: loadFileFontSource,
7310
7409
  variableLoader: fetchVariableFontSource,
7311
7410
  diskCache: fonts?.googleFonts?.cacheDir ? new FontDiskCache(fonts.googleFonts.cacheDir) : void 0
@@ -7326,6 +7425,67 @@ import {
7326
7425
  toRasterizeFontFaces
7327
7426
  } from "@json-to-office/shared/fonts/node";
7328
7427
 
7428
+ // src/core/fontEmbedding.ts
7429
+ import { isSafeFont } from "@json-to-office/shared";
7430
+ import {
7431
+ toEmbeddedFontFaces
7432
+ } from "@json-to-office/shared/fonts/node";
7433
+
7434
+ // src/renderers/types.ts
7435
+ import {
7436
+ DEFAULT_DOCX_RENDERER_ID
7437
+ } from "@json-to-office/shared-docx";
7438
+
7439
+ // src/core/fontEmbedding.ts
7440
+ var EMBEDDING_RENDERER = "docxjs";
7441
+ var warning = (code, message) => ({
7442
+ component: "fontRegistry",
7443
+ message,
7444
+ severity: "warning",
7445
+ context: { code }
7446
+ });
7447
+ function embedsFonts(fonts, renderer, warnings) {
7448
+ if (fonts?.mode !== "embed") return false;
7449
+ const id = renderer ?? DEFAULT_DOCX_RENDERER_ID;
7450
+ if (id === EMBEDDING_RENDERER) return true;
7451
+ warnings.push(
7452
+ warning(
7453
+ "FONT_EMBED_UNSUPPORTED_RENDERER",
7454
+ `Export mode "embed" needs the ${EMBEDDING_RENDERER} renderer; the ${id} renderer does not embed fonts, so references are kept as in custom mode and nothing is embedded.`
7455
+ )
7456
+ );
7457
+ return false;
7458
+ }
7459
+ function selectEmbeddedFonts(resolved, warnings) {
7460
+ const { faces, report } = toEmbeddedFontFaces(resolved, warnings);
7461
+ const summary = embedSummary(resolved, report);
7462
+ if (summary) warnings.push(warning("FONT_MODE_EMBED", summary));
7463
+ return faces;
7464
+ }
7465
+ function embedSummary(resolved, report) {
7466
+ const byFamily = new Map(report.map((r) => [r.family.toLowerCase(), r]));
7467
+ const seen = /* @__PURE__ */ new Set();
7468
+ const lines = [];
7469
+ for (const font of resolved) {
7470
+ const key = font.family.toLowerCase();
7471
+ if (isSafeFont(font.family.trim()) || seen.has(key)) continue;
7472
+ seen.add(key);
7473
+ const entry = byFamily.get(key);
7474
+ if (!entry) {
7475
+ lines.push(`${font.family}: not embedded (unresolved)`);
7476
+ } else if (entry.slots.length === 0) {
7477
+ lines.push(
7478
+ `${font.family}: not embedded (${entry.reason === "no-regular-face" ? "no regular face" : "no TTF/OTF face it may embed"})`
7479
+ );
7480
+ } else {
7481
+ const nearest = entry.nearest.length ? ` (${entry.nearest.map((n) => `${n.slot} \u2190 ${n.weight}`).join(", ")})` : "";
7482
+ lines.push(`${font.family}: ${entry.slots.join(", ")}${nearest}`);
7483
+ }
7484
+ }
7485
+ if (lines.length === 0) return void 0;
7486
+ return `Export mode "embed": ${lines.join("; ")}. Readers without these fonts installed see the embedded faces; any face or family not embedded stays a reference, as in custom mode.`;
7487
+ }
7488
+
7329
7489
  // src/core/prerasterizeVisuals.ts
7330
7490
  import {
7331
7491
  clampVisualDpi,
@@ -7797,7 +7957,7 @@ function assertExportServerAllowed(serverUrl, servicesConfig, warnings) {
7797
7957
  const notice = remoteExportNotice(serverUrl, servicesConfig?.allowRemote);
7798
7958
  if (!notice || !warnings) return;
7799
7959
  const alreadySaid = warnings.some(
7800
- (warning) => warning.context?.code === REMOTE_EXPORT_WARNING && warning.context?.serverUrl === serverUrl
7960
+ (warning2) => warning2.context?.code === REMOTE_EXPORT_WARNING && warning2.context?.serverUrl === serverUrl
7801
7961
  );
7802
7962
  if (alreadySaid) return;
7803
7963
  warnings.push({
@@ -13843,13 +14003,6 @@ function warnOnce(ctx, component, message, code) {
13843
14003
  import {
13844
14004
  RendererRegistry
13845
14005
  } from "@json-to-office/shared/rendering";
13846
-
13847
- // src/renderers/types.ts
13848
- import {
13849
- DEFAULT_DOCX_RENDERER_ID
13850
- } from "@json-to-office/shared-docx";
13851
-
13852
- // src/renderers/registry.ts
13853
14006
  var registry = new RendererRegistry(
13854
14007
  "docx",
13855
14008
  DEFAULT_DOCX_RENDERER_ID
@@ -16302,13 +16455,15 @@ async function compileDocumentScoped(document, options, warnings) {
16302
16455
  const { context } = prepared.model;
16303
16456
  const hasVisual = collectVisualProps(context.document).length > 0;
16304
16457
  const hasHighcharts = containsComponent(context.document, "highcharts");
16458
+ const embedding = embedsFonts(options.fonts, options.renderer, warnings);
16305
16459
  const resolvedFonts = await resolveDocumentFonts(
16306
16460
  context.document,
16307
16461
  context.theme,
16308
16462
  options.fonts,
16309
16463
  warnings,
16310
- hasVisual || hasHighcharts
16464
+ hasVisual || hasHighcharts || embedding
16311
16465
  );
16466
+ const embeddedFonts = embedding ? selectEmbeddedFonts(resolvedFonts, warnings) : [];
16312
16467
  const visualFonts = hasVisual ? toRasterizeFontFaces(resolvedFonts, warnings) : [];
16313
16468
  const chartFonts = hasHighcharts ? toChartFontFaces(resolvedFonts) : [];
16314
16469
  const desugared = await desugarExternals(context.document, {
@@ -16348,7 +16503,8 @@ async function compileDocumentScoped(document, options, warnings) {
16348
16503
  theme: context.theme,
16349
16504
  warnings: compiled.warnings,
16350
16505
  required: compiled.required,
16351
- unsupported: compiled.unsupported
16506
+ unsupported: compiled.unsupported,
16507
+ embeddedFonts
16352
16508
  };
16353
16509
  }
16354
16510
  async function generateBufferViaIr(document, options = {}) {
@@ -16372,6 +16528,7 @@ async function renderScoped(document, options, warnings) {
16372
16528
  ...options.deterministic !== void 0 ? { deterministic: options.deterministic } : {},
16373
16529
  ...options.generatedAt !== void 0 ? { generatedAt: toDate(options.generatedAt) } : {},
16374
16530
  ...options.svgRasterFallback !== void 0 ? { svgRasterFallback: options.svgRasterFallback } : {},
16531
+ ...compiled.embeddedFonts.length > 0 ? { embeddedFonts: compiled.embeddedFonts } : {},
16375
16532
  warnings: compiled.warnings
16376
16533
  });
16377
16534
  return { buffer: Buffer.from(bytes), warnings: compiled.warnings };
@@ -17368,10 +17525,10 @@ async function runPluginDemo() {
17368
17525
  if (result.warnings && result.warnings.length > 0) {
17369
17526
  console.log(`
17370
17527
  \u26A0\uFE0F ${result.warnings.length} warning(s) generated:`);
17371
- result.warnings.forEach((warning) => {
17372
- console.log(` - [${warning.component}] ${warning.message}`);
17373
- if (warning.context) {
17374
- console.log(` Context: ${JSON.stringify(warning.context)}`);
17528
+ result.warnings.forEach((warning2) => {
17529
+ console.log(` - [${warning2.component}] ${warning2.message}`);
17530
+ if (warning2.context) {
17531
+ console.log(` Context: ${JSON.stringify(warning2.context)}`);
17375
17532
  }
17376
17533
  });
17377
17534
  }