@json-to-office/core-docx 7.5.0 → 7.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4237,6 +4237,8 @@ var init_normalizeTextCase = __esm({
4237
4237
 
4238
4238
  // src/utils/packageDocument.ts
4239
4239
  import AdmZip from "adm-zip";
4240
+ import { createHash as createHash3 } from "crypto";
4241
+ import { posix } from "path";
4240
4242
  function resolveGenerationDate(options) {
4241
4243
  if (options?.generatedAt !== void 0) {
4242
4244
  const date = new Date(options.generatedAt);
@@ -4304,6 +4306,70 @@ function canonicalizeRelationshipIds(zip) {
4304
4306
  if (entry) zip.updateFile(entry, Buffer.from(update.xml, "utf8"));
4305
4307
  }
4306
4308
  }
4309
+ function xorFontKey(bytes, guid) {
4310
+ const key = Buffer.from(guid.replace(/-/g, ""), "hex").reverse();
4311
+ const out = Buffer.from(bytes);
4312
+ for (let i = 0; i < Math.min(32, out.length); i++) out[i] ^= key[i % 16];
4313
+ return out;
4314
+ }
4315
+ function canonicalizeEmbeddedFontKeys(zip) {
4316
+ const table2 = zip.getEntry("word/fontTable.xml");
4317
+ if (!table2) return;
4318
+ const xml = table2.getData().toString("utf8");
4319
+ const embeds = [...xml.matchAll(FONT_EMBED)].flatMap(([element]) => {
4320
+ const id = /\br:id="([^"]+)"/.exec(element)?.[1];
4321
+ const key = /\bw:fontKey="\{([0-9A-Fa-f-]{36})\}"/.exec(element)?.[1];
4322
+ return id && key ? [{ id, key }] : [];
4323
+ });
4324
+ if (embeds.length === 0) return;
4325
+ const rels = zip.getEntry("word/_rels/fontTable.xml.rels")?.getData().toString("utf8") ?? "";
4326
+ const targets = /* @__PURE__ */ new Map();
4327
+ for (const [relationship] of rels.matchAll(/<Relationship\b[^>]*>/g)) {
4328
+ const id = /\bId="([^"]+)"/.exec(relationship)?.[1];
4329
+ const target = /\bTarget="([^"]+)"/.exec(relationship)?.[1];
4330
+ if (id && target) targets.set(id, target);
4331
+ }
4332
+ const rekey = /* @__PURE__ */ new Map();
4333
+ const parts = [];
4334
+ for (const { id, key } of embeds) {
4335
+ const target = targets.get(id);
4336
+ if (!target) continue;
4337
+ const name = target.startsWith("/") ? target.slice(1) : posix.normalize(posix.join("word", target));
4338
+ const part = zip.getEntry(name);
4339
+ if (!part) continue;
4340
+ const font = xorFontKey(part.getData(), key);
4341
+ const hex = createHash3("sha256").update(font).digest("hex").slice(0, 32).toUpperCase();
4342
+ const fresh = [
4343
+ hex.slice(0, 8),
4344
+ hex.slice(8, 12),
4345
+ hex.slice(12, 16),
4346
+ hex.slice(16, 20),
4347
+ hex.slice(20)
4348
+ ].join("-");
4349
+ rekey.set(key.toUpperCase(), fresh);
4350
+ parts.push({ name, data: xorFontKey(font, fresh) });
4351
+ }
4352
+ if (rekey.size === 0) return;
4353
+ for (const { name, data } of parts) {
4354
+ const entry = zip.getEntry(name);
4355
+ if (entry) zip.updateFile(entry, data);
4356
+ }
4357
+ const fontTable = zip.getEntry("word/fontTable.xml");
4358
+ if (!fontTable) return;
4359
+ zip.updateFile(
4360
+ fontTable,
4361
+ Buffer.from(
4362
+ xml.replace(
4363
+ /\bw:fontKey="\{([0-9A-Fa-f-]{36})\}"/g,
4364
+ (whole, key) => {
4365
+ const fresh = rekey.get(key.toUpperCase());
4366
+ return fresh ? `w:fontKey="{${fresh}}"` : whole;
4367
+ }
4368
+ ),
4369
+ "utf8"
4370
+ )
4371
+ );
4372
+ }
4307
4373
  function canonicalizeEmbeddedPackages(zip, generatedAt, depth = 1) {
4308
4374
  const names = zip.getEntries().map((entry) => entry.entryName).filter((name) => EMBEDDED_OFFICE_PACKAGE.test(name));
4309
4375
  let changed = false;
@@ -4356,6 +4422,7 @@ function canonicalizeDocxBuffer(buffer, generatedAt = DEFAULT_GENERATION_DATE) {
4356
4422
  normalizeTextCase(zip);
4357
4423
  canonicalizeRelationshipIds(zip);
4358
4424
  canonicalizeEmbeddedPackages(zip, generatedAt);
4425
+ canonicalizeEmbeddedFontKeys(zip);
4359
4426
  const isoTimestamp = generatedAt.toISOString();
4360
4427
  const coreProperties3 = zip.getEntry("docProps/core.xml");
4361
4428
  if (coreProperties3) {
@@ -4372,13 +4439,14 @@ function normalizeDocxCaseBuffer(buffer) {
4372
4439
  const zip = new AdmZip(buffer);
4373
4440
  return normalizeTextCase(zip) ? zip.toBuffer() : buffer;
4374
4441
  }
4375
- var DEFAULT_GENERATION_DATE, STABLE_RELATIONSHIP_ID, CORE_TIMESTAMP, EMBEDDED_OFFICE_PACKAGE, MAX_EMBEDDED_DEPTH;
4442
+ var DEFAULT_GENERATION_DATE, STABLE_RELATIONSHIP_ID, FONT_EMBED, CORE_TIMESTAMP, EMBEDDED_OFFICE_PACKAGE, MAX_EMBEDDED_DEPTH;
4376
4443
  var init_packageDocument = __esm({
4377
4444
  "src/utils/packageDocument.ts"() {
4378
4445
  "use strict";
4379
4446
  init_normalizeTextCase();
4380
4447
  DEFAULT_GENERATION_DATE = /* @__PURE__ */ new Date("2000-01-01T00:00:00.000Z");
4381
4448
  STABLE_RELATIONSHIP_ID = /^rId\d+$/;
4449
+ FONT_EMBED = /<w:embed(?:Regular|Bold|Italic|BoldItalic)\b[^>]*>/g;
4382
4450
  CORE_TIMESTAMP = /(<dcterms:(?:created|modified)\b[^>]*>)[^<]*(<\/dcterms:(?:created|modified)>)/g;
4383
4451
  EMBEDDED_OFFICE_PACKAGE = /\.(?:docx|pptx|xlsx|xlsm)$/i;
4384
4452
  MAX_EMBEDDED_DEPTH = 3;
@@ -4979,7 +5047,12 @@ function createDocxJsRenderer() {
4979
5047
  });
4980
5048
  }
4981
5049
  const pageNumbers = needsPageNumbers(ir) ? await createPageNumberEstimator(ir) : void 0;
4982
- const document = buildDocument(ir, resources, pageNumbers);
5050
+ const document = buildDocument(
5051
+ ir,
5052
+ resources,
5053
+ pageNumbers,
5054
+ renderOptions?.embeddedFonts
5055
+ );
4983
5056
  const packed = await Packer.toBuffer(document);
4984
5057
  const fixed = fixFloatingImageIdsInBuffer(packed);
4985
5058
  const repaired = markers ? repairDrawingGroupsInBuffer(fixed, markers) : fixed;
@@ -4997,10 +5070,17 @@ function createDocxJsRenderer() {
4997
5070
  }
4998
5071
  };
4999
5072
  }
5000
- function buildDocument(ir, resources = /* @__PURE__ */ new Map(), pageNumbers) {
5073
+ function buildDocument(ir, resources = /* @__PURE__ */ new Map(), pageNumbers, embeddedFonts) {
5001
5074
  return new Document({
5002
5075
  ...pageNumbers ? { pageNumbers } : {},
5003
5076
  styles: emitStyles(ir.styles),
5077
+ // word/fonts/font<N>.odttf, their `w:embed*` entries in the font table and
5078
+ // `w:embedTrueTypeFonts` in the settings, all from this one key. Passed
5079
+ // only when a face embeds, so a document in any other mode is built from
5080
+ // exactly the options it always was. `docx/layout` measures the text of an
5081
+ // embedded family from these files, so a contents field's page numbers
5082
+ // need nothing more.
5083
+ ...embeddedFonts && embeddedFonts.length > 0 ? { fonts: embeddedFonts.map(fontOptions) } : {},
5004
5084
  ...ir.theme ? { theme: themeOptions(ir.theme) } : {},
5005
5085
  sections: ir.sections.map((section2) => sectionOptions(section2, resources)),
5006
5086
  ...coreProperties(ir),
@@ -5031,6 +5111,15 @@ function buildDocument(ir, resources = /* @__PURE__ */ new Map(), pageNumbers) {
5031
5111
  } : {}
5032
5112
  });
5033
5113
  }
5114
+ function fontOptions(face) {
5115
+ return {
5116
+ name: face.family,
5117
+ data: face.regular,
5118
+ ...face.bold ? { bold: face.bold } : {},
5119
+ ...face.italic ? { italic: face.italic } : {},
5120
+ ...face.boldItalic ? { boldItalic: face.boldItalic } : {}
5121
+ };
5122
+ }
5034
5123
  function themeOptions(theme) {
5035
5124
  const colors = {};
5036
5125
  for (const [slot, color] of Object.entries(theme.colors)) {
@@ -7326,6 +7415,67 @@ import {
7326
7415
  toRasterizeFontFaces
7327
7416
  } from "@json-to-office/shared/fonts/node";
7328
7417
 
7418
+ // src/core/fontEmbedding.ts
7419
+ import { isSafeFont } from "@json-to-office/shared";
7420
+ import {
7421
+ toEmbeddedFontFaces
7422
+ } from "@json-to-office/shared/fonts/node";
7423
+
7424
+ // src/renderers/types.ts
7425
+ import {
7426
+ DEFAULT_DOCX_RENDERER_ID
7427
+ } from "@json-to-office/shared-docx";
7428
+
7429
+ // src/core/fontEmbedding.ts
7430
+ var EMBEDDING_RENDERER = "docxjs";
7431
+ var warning = (code, message) => ({
7432
+ component: "fontRegistry",
7433
+ message,
7434
+ severity: "warning",
7435
+ context: { code }
7436
+ });
7437
+ function embedsFonts(fonts, renderer, warnings) {
7438
+ if (fonts?.mode !== "embed") return false;
7439
+ const id = renderer ?? DEFAULT_DOCX_RENDERER_ID;
7440
+ if (id === EMBEDDING_RENDERER) return true;
7441
+ warnings.push(
7442
+ warning(
7443
+ "FONT_EMBED_UNSUPPORTED_RENDERER",
7444
+ `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.`
7445
+ )
7446
+ );
7447
+ return false;
7448
+ }
7449
+ function selectEmbeddedFonts(resolved, warnings) {
7450
+ const { faces, report } = toEmbeddedFontFaces(resolved, warnings);
7451
+ const summary = embedSummary(resolved, report);
7452
+ if (summary) warnings.push(warning("FONT_MODE_EMBED", summary));
7453
+ return faces;
7454
+ }
7455
+ function embedSummary(resolved, report) {
7456
+ const byFamily = new Map(report.map((r) => [r.family.toLowerCase(), r]));
7457
+ const seen = /* @__PURE__ */ new Set();
7458
+ const lines = [];
7459
+ for (const font of resolved) {
7460
+ const key = font.family.toLowerCase();
7461
+ if (isSafeFont(font.family.trim()) || seen.has(key)) continue;
7462
+ seen.add(key);
7463
+ const entry = byFamily.get(key);
7464
+ if (!entry) {
7465
+ lines.push(`${font.family}: not embedded (unresolved)`);
7466
+ } else if (entry.slots.length === 0) {
7467
+ lines.push(
7468
+ `${font.family}: not embedded (${entry.reason === "no-regular-face" ? "no regular face" : "no TTF/OTF face it may embed"})`
7469
+ );
7470
+ } else {
7471
+ const nearest = entry.nearest.length ? ` (${entry.nearest.map((n) => `${n.slot} \u2190 ${n.weight}`).join(", ")})` : "";
7472
+ lines.push(`${font.family}: ${entry.slots.join(", ")}${nearest}`);
7473
+ }
7474
+ }
7475
+ if (lines.length === 0) return void 0;
7476
+ 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.`;
7477
+ }
7478
+
7329
7479
  // src/core/prerasterizeVisuals.ts
7330
7480
  import {
7331
7481
  clampVisualDpi,
@@ -7797,7 +7947,7 @@ function assertExportServerAllowed(serverUrl, servicesConfig, warnings) {
7797
7947
  const notice = remoteExportNotice(serverUrl, servicesConfig?.allowRemote);
7798
7948
  if (!notice || !warnings) return;
7799
7949
  const alreadySaid = warnings.some(
7800
- (warning) => warning.context?.code === REMOTE_EXPORT_WARNING && warning.context?.serverUrl === serverUrl
7950
+ (warning2) => warning2.context?.code === REMOTE_EXPORT_WARNING && warning2.context?.serverUrl === serverUrl
7801
7951
  );
7802
7952
  if (alreadySaid) return;
7803
7953
  warnings.push({
@@ -13843,13 +13993,6 @@ function warnOnce(ctx, component, message, code) {
13843
13993
  import {
13844
13994
  RendererRegistry
13845
13995
  } 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
13996
  var registry = new RendererRegistry(
13854
13997
  "docx",
13855
13998
  DEFAULT_DOCX_RENDERER_ID
@@ -16302,13 +16445,15 @@ async function compileDocumentScoped(document, options, warnings) {
16302
16445
  const { context } = prepared.model;
16303
16446
  const hasVisual = collectVisualProps(context.document).length > 0;
16304
16447
  const hasHighcharts = containsComponent(context.document, "highcharts");
16448
+ const embedding = embedsFonts(options.fonts, options.renderer, warnings);
16305
16449
  const resolvedFonts = await resolveDocumentFonts(
16306
16450
  context.document,
16307
16451
  context.theme,
16308
16452
  options.fonts,
16309
16453
  warnings,
16310
- hasVisual || hasHighcharts
16454
+ hasVisual || hasHighcharts || embedding
16311
16455
  );
16456
+ const embeddedFonts = embedding ? selectEmbeddedFonts(resolvedFonts, warnings) : [];
16312
16457
  const visualFonts = hasVisual ? toRasterizeFontFaces(resolvedFonts, warnings) : [];
16313
16458
  const chartFonts = hasHighcharts ? toChartFontFaces(resolvedFonts) : [];
16314
16459
  const desugared = await desugarExternals(context.document, {
@@ -16348,7 +16493,8 @@ async function compileDocumentScoped(document, options, warnings) {
16348
16493
  theme: context.theme,
16349
16494
  warnings: compiled.warnings,
16350
16495
  required: compiled.required,
16351
- unsupported: compiled.unsupported
16496
+ unsupported: compiled.unsupported,
16497
+ embeddedFonts
16352
16498
  };
16353
16499
  }
16354
16500
  async function generateBufferViaIr(document, options = {}) {
@@ -16372,6 +16518,7 @@ async function renderScoped(document, options, warnings) {
16372
16518
  ...options.deterministic !== void 0 ? { deterministic: options.deterministic } : {},
16373
16519
  ...options.generatedAt !== void 0 ? { generatedAt: toDate(options.generatedAt) } : {},
16374
16520
  ...options.svgRasterFallback !== void 0 ? { svgRasterFallback: options.svgRasterFallback } : {},
16521
+ ...compiled.embeddedFonts.length > 0 ? { embeddedFonts: compiled.embeddedFonts } : {},
16375
16522
  warnings: compiled.warnings
16376
16523
  });
16377
16524
  return { buffer: Buffer.from(bytes), warnings: compiled.warnings };
@@ -17368,10 +17515,10 @@ async function runPluginDemo() {
17368
17515
  if (result.warnings && result.warnings.length > 0) {
17369
17516
  console.log(`
17370
17517
  \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)}`);
17518
+ result.warnings.forEach((warning2) => {
17519
+ console.log(` - [${warning2.component}] ${warning2.message}`);
17520
+ if (warning2.context) {
17521
+ console.log(` Context: ${JSON.stringify(warning2.context)}`);
17375
17522
  }
17376
17523
  });
17377
17524
  }