@json-to-office/core-docx 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7013,6 +7013,7 @@ async function renderHighchartsComponent(component, theme, themeName, context) {
7013
7013
  }
7014
7014
 
7015
7015
  // src/components/visual.ts
7016
+ import { createHash as createHash4 } from "crypto";
7016
7017
  import {
7017
7018
  clampVisualDpi,
7018
7019
  DEFAULT_VISUAL_DPI
@@ -7062,7 +7063,14 @@ function visualToImageProps(props, base64DataUri) {
7062
7063
  ...props.alt !== void 0 && { alt: props.alt }
7063
7064
  };
7064
7065
  }
7065
- async function rasterize(presentation, dpi, propsServerUrl, serviceConfig, baseDir) {
7066
+ function visualRasterKey(presentation, dpi, serverUrl) {
7067
+ return createHash4("sha256").update(JSON.stringify({ p: presentation, d: dpi, u: serverUrl ?? null })).digest("hex");
7068
+ }
7069
+ function effectiveVisualServerUrl(props, serviceConfig) {
7070
+ if (serviceConfig?.render) return void 0;
7071
+ return props.serverUrl;
7072
+ }
7073
+ async function rasterizeVisualSlide(presentation, dpi, propsServerUrl, serviceConfig, baseDir) {
7066
7074
  if (!isNodeEnvironment()) {
7067
7075
  throw new Error(
7068
7076
  "Visual rasterization requires a Node.js environment. It is not available in browser environments."
@@ -7108,13 +7116,26 @@ async function renderVisualComponent(component, theme, themeName, context) {
7108
7116
  const dpi = clampVisualDpi(
7109
7117
  props.dpi ?? serviceConfig?.dpi ?? DEFAULT_VISUAL_DPI
7110
7118
  );
7111
- const result = await rasterize(
7112
- presentation,
7113
- dpi,
7114
- props.serverUrl,
7115
- serviceConfig,
7116
- getBaseDir()
7119
+ const preRasterized = context?.visualRasterResults?.get(
7120
+ visualRasterKey(
7121
+ presentation,
7122
+ dpi,
7123
+ effectiveVisualServerUrl(props, serviceConfig)
7124
+ )
7117
7125
  );
7126
+ let result;
7127
+ if (preRasterized) {
7128
+ if (!preRasterized.ok) throw new Error(preRasterized.error);
7129
+ result = preRasterized;
7130
+ } else {
7131
+ result = await rasterizeVisualSlide(
7132
+ presentation,
7133
+ dpi,
7134
+ props.serverUrl,
7135
+ serviceConfig,
7136
+ getBaseDir()
7137
+ );
7138
+ }
7118
7139
  return await createImage(
7119
7140
  result.base64DataUri,
7120
7141
  theme,
@@ -7176,6 +7197,203 @@ var textSpaceAfterComponent = createComponent({
7176
7197
 
7177
7198
  // src/core/render.ts
7178
7199
  init_docxImagePositioning();
7200
+
7201
+ // src/core/prerasterizeVisuals.ts
7202
+ import {
7203
+ clampVisualDpi as clampVisualDpi2,
7204
+ DEFAULT_VISUAL_DPI as DEFAULT_VISUAL_DPI2,
7205
+ MAX_RASTERIZE_BATCH_SLIDES
7206
+ } from "@json-to-office/shared";
7207
+
7208
+ // src/utils/promiseLimiter.ts
7209
+ function createLimiter(max) {
7210
+ let active = 0;
7211
+ const queue = [];
7212
+ const release = () => {
7213
+ active--;
7214
+ queue.shift()?.();
7215
+ };
7216
+ return async function limit(fn) {
7217
+ while (active >= max)
7218
+ await new Promise((resolve4) => queue.push(resolve4));
7219
+ active++;
7220
+ try {
7221
+ return await fn();
7222
+ } finally {
7223
+ release();
7224
+ }
7225
+ };
7226
+ }
7227
+
7228
+ // src/core/prerasterizeVisuals.ts
7229
+ var DEFAULT_FALLBACK_CONCURRENCY = 4;
7230
+ var BATCH_TIMEOUT_BASE_MS = 3e4;
7231
+ var BATCH_TIMEOUT_PER_SLIDE_MS = 1e4;
7232
+ function collectVisualProps(root) {
7233
+ const found = [];
7234
+ const seen = /* @__PURE__ */ new WeakSet();
7235
+ const visit = (node) => {
7236
+ if (!node || typeof node !== "object") return;
7237
+ if (seen.has(node)) return;
7238
+ seen.add(node);
7239
+ if (Array.isArray(node)) {
7240
+ for (const item of node) visit(item);
7241
+ return;
7242
+ }
7243
+ const obj = node;
7244
+ if (typeof obj.name === "string" && obj.enabled === false) return;
7245
+ if (obj.name === "visual" && obj.props && typeof obj.props === "object") {
7246
+ found.push(obj.props);
7247
+ return;
7248
+ }
7249
+ for (const value of Object.values(obj)) visit(value);
7250
+ };
7251
+ visit(root);
7252
+ return found;
7253
+ }
7254
+ function toErrorMessage(error) {
7255
+ return error instanceof Error ? error.message : String(error);
7256
+ }
7257
+ function* chunksOf(items, size) {
7258
+ for (let i = 0; i < items.length; i += size) yield items.slice(i, i + size);
7259
+ }
7260
+ function applyBatchResponse(chunk, response, map) {
7261
+ const results = response?.results;
7262
+ if (!Array.isArray(results) || results.length !== chunk.length) return false;
7263
+ const entries = [];
7264
+ for (let i = 0; i < chunk.length; i++) {
7265
+ const item = results[i];
7266
+ if (item && item.ok === true && typeof item.base64DataUri === "string" && item.base64DataUri.length > 0 && typeof item.width === "number" && typeof item.height === "number") {
7267
+ entries.push([
7268
+ chunk[i].key,
7269
+ {
7270
+ ok: true,
7271
+ base64DataUri: item.base64DataUri,
7272
+ width: item.width,
7273
+ height: item.height
7274
+ }
7275
+ ]);
7276
+ } else if (item && item.ok === false && typeof item.error === "string") {
7277
+ entries.push([chunk[i].key, { ok: false, error: item.error }]);
7278
+ } else {
7279
+ return false;
7280
+ }
7281
+ }
7282
+ for (const [key, value] of entries) map.set(key, value);
7283
+ return true;
7284
+ }
7285
+ async function prerasterizeVisuals(root, serviceConfig, options = {}) {
7286
+ const map = /* @__PURE__ */ new Map();
7287
+ if (!isNodeEnvironment() && !serviceConfig?.renderBatch && !serviceConfig?.render) {
7288
+ return map;
7289
+ }
7290
+ const visuals = collectVisualProps(root);
7291
+ if (visuals.length === 0) return map;
7292
+ const targets = /* @__PURE__ */ new Map();
7293
+ for (const props of visuals) {
7294
+ try {
7295
+ const serverUrl2 = effectiveVisualServerUrl(props, serviceConfig);
7296
+ if (serverUrl2 !== void 0) continue;
7297
+ const presentation = buildVisualPresentation(props);
7298
+ const dpi = clampVisualDpi2(
7299
+ props.dpi ?? serviceConfig?.dpi ?? DEFAULT_VISUAL_DPI2
7300
+ );
7301
+ const key = visualRasterKey(presentation, dpi, serverUrl2);
7302
+ if (!targets.has(key)) targets.set(key, { key, presentation, dpi });
7303
+ } catch {
7304
+ continue;
7305
+ }
7306
+ }
7307
+ if (targets.size === 0) return map;
7308
+ const unique = [...targets.values()];
7309
+ const limit = createLimiter(
7310
+ Math.max(1, options.concurrency ?? DEFAULT_FALLBACK_CONCURRENCY)
7311
+ );
7312
+ const rasterizeIndividually = async (chunk) => {
7313
+ await Promise.all(
7314
+ chunk.map(
7315
+ (target) => limit(async () => {
7316
+ try {
7317
+ const result = await rasterizeVisualSlide(
7318
+ target.presentation,
7319
+ target.dpi,
7320
+ void 0,
7321
+ serviceConfig,
7322
+ options.baseDir
7323
+ );
7324
+ map.set(target.key, { ok: true, ...result });
7325
+ } catch (error) {
7326
+ map.set(target.key, { ok: false, error: toErrorMessage(error) });
7327
+ }
7328
+ })
7329
+ )
7330
+ );
7331
+ };
7332
+ if (serviceConfig?.renderBatch) {
7333
+ for (const chunk of chunksOf(unique, MAX_RASTERIZE_BATCH_SLIDES)) {
7334
+ try {
7335
+ const response = await serviceConfig.renderBatch({
7336
+ slides: chunk.map((target) => ({
7337
+ presentation: target.presentation,
7338
+ dpi: target.dpi
7339
+ })),
7340
+ ...options.baseDir !== void 0 && { baseDir: options.baseDir }
7341
+ });
7342
+ if (!applyBatchResponse(chunk, response, map)) {
7343
+ throw new Error(
7344
+ "batch rasterizer returned a malformed result (expected index-aligned results[])"
7345
+ );
7346
+ }
7347
+ } catch (error) {
7348
+ if (serviceConfig.render || serviceConfig.serverUrl) {
7349
+ await rasterizeIndividually(chunk);
7350
+ } else {
7351
+ const message = toErrorMessage(error);
7352
+ for (const target of chunk) {
7353
+ map.set(target.key, { ok: false, error: message });
7354
+ }
7355
+ }
7356
+ }
7357
+ }
7358
+ return map;
7359
+ }
7360
+ if (serviceConfig?.render) {
7361
+ await rasterizeIndividually(unique);
7362
+ return map;
7363
+ }
7364
+ const serverUrl = resolveServiceUrl(
7365
+ void 0,
7366
+ serviceConfig?.serverUrl,
7367
+ DEFAULT_RASTERIZE_SERVER_URL
7368
+ );
7369
+ for (const chunk of chunksOf(unique, MAX_RASTERIZE_BATCH_SLIDES)) {
7370
+ let applied = false;
7371
+ try {
7372
+ const response = await postJsonToService({
7373
+ url: serverUrl,
7374
+ path: "/rasterize/batch",
7375
+ body: {
7376
+ slides: chunk.map((target) => ({
7377
+ presentation: target.presentation,
7378
+ dpi: target.dpi
7379
+ })),
7380
+ ...options.baseDir !== void 0 && { baseDir: options.baseDir }
7381
+ },
7382
+ headers: serviceConfig?.headers,
7383
+ timeoutMs: BATCH_TIMEOUT_BASE_MS + BATCH_TIMEOUT_PER_SLIDE_MS * chunk.length,
7384
+ serviceLabel: "PPTX batch rasterization service",
7385
+ onUnreachable: (url, cause) => `PPTX rasterization service is not reachable at ${url}. Cause: ${cause}`
7386
+ });
7387
+ applied = applyBatchResponse(chunk, await response.json(), map);
7388
+ } catch {
7389
+ applied = false;
7390
+ }
7391
+ if (!applied) await rasterizeIndividually(chunk);
7392
+ }
7393
+ return map;
7394
+ }
7395
+
7396
+ // src/core/render.ts
7179
7397
  function getAlignment3(alignment) {
7180
7398
  switch (alignment) {
7181
7399
  case "center":
@@ -7232,6 +7450,21 @@ async function renderDocumentScoped(structure, layout, options) {
7232
7450
  structure.themeName
7233
7451
  );
7234
7452
  context.services = options?.services;
7453
+ try {
7454
+ const visualRasterResults = await prerasterizeVisuals(
7455
+ layout.sections,
7456
+ options?.services?.pptx,
7457
+ { baseDir: getBaseDir() }
7458
+ );
7459
+ if (visualRasterResults.size > 0) {
7460
+ context.visualRasterResults = visualRasterResults;
7461
+ }
7462
+ } catch (error) {
7463
+ console.warn(
7464
+ "[core-docx] Visual pre-rasterization failed; falling back to per-visual rasterization:",
7465
+ error instanceof Error ? error.message : error
7466
+ );
7467
+ }
7235
7468
  let sectionBookmarkCounter = 0;
7236
7469
  let previousHeader = void 0;
7237
7470
  let previousFooter = void 0;
@@ -8015,31 +8248,39 @@ var DocumentGenerator = {
8015
8248
 
8016
8249
  // src/core/flattenVisuals.ts
8017
8250
  import {
8018
- clampVisualDpi as clampVisualDpi2,
8019
- DEFAULT_VISUAL_DPI as DEFAULT_VISUAL_DPI2
8251
+ clampVisualDpi as clampVisualDpi3,
8252
+ DEFAULT_VISUAL_DPI as DEFAULT_VISUAL_DPI3
8020
8253
  } from "@json-to-office/shared";
8021
8254
  var DEFAULT_CONCURRENCY = 4;
8022
- function createLimiter(max) {
8023
- let active = 0;
8024
- const queue = [];
8025
- const release = () => {
8026
- active--;
8027
- queue.shift()?.();
8028
- };
8029
- return async function limit(fn) {
8030
- if (active >= max)
8031
- await new Promise((resolve4) => queue.push(resolve4));
8032
- active++;
8033
- try {
8034
- return await fn();
8035
- } finally {
8036
- release();
8037
- }
8038
- };
8039
- }
8040
8255
  async function flattenVisuals(doc, options) {
8256
+ let rasterize = options.rasterize;
8257
+ if (options.rasterizeBatch) {
8258
+ const preRasterized = await prerasterizeVisuals(
8259
+ doc,
8260
+ {
8261
+ render: options.rasterize,
8262
+ renderBatch: options.rasterizeBatch,
8263
+ dpi: options.dpi
8264
+ },
8265
+ { baseDir: options.baseDir, concurrency: options.concurrency }
8266
+ ).catch(() => /* @__PURE__ */ new Map());
8267
+ rasterize = async (request) => {
8268
+ const hit = preRasterized.get(
8269
+ visualRasterKey(request.presentation, request.dpi)
8270
+ );
8271
+ if (hit) {
8272
+ if (!hit.ok) throw new Error(hit.error);
8273
+ return {
8274
+ base64DataUri: hit.base64DataUri,
8275
+ width: hit.width,
8276
+ height: hit.height
8277
+ };
8278
+ }
8279
+ return options.rasterize(request);
8280
+ };
8281
+ }
8041
8282
  const ctx = {
8042
- rasterize: options.rasterize,
8283
+ rasterize,
8043
8284
  dpi: options.dpi,
8044
8285
  baseDir: options.baseDir,
8045
8286
  limit: createLimiter(
@@ -8050,7 +8291,7 @@ async function flattenVisuals(doc, options) {
8050
8291
  }
8051
8292
  async function rasterizeVisual(obj, ctx) {
8052
8293
  const props = obj.props;
8053
- const dpi = clampVisualDpi2(props.dpi ?? ctx.dpi ?? DEFAULT_VISUAL_DPI2);
8294
+ const dpi = clampVisualDpi3(props.dpi ?? ctx.dpi ?? DEFAULT_VISUAL_DPI3);
8054
8295
  const result = await ctx.limit(
8055
8296
  () => ctx.rasterize({
8056
8297
  presentation: buildVisualPresentation(props),
@@ -8760,70 +9001,104 @@ function createBuilderImpl(state) {
8760
9001
  }
8761
9002
  return new Set(list);
8762
9003
  }
8763
- async function generate(document, options) {
8764
- try {
8765
- const preserveSet = resolvePreserveSet(options);
8766
- const rootIn = document;
8767
- const internalDocument = rootIn.props === void 0 ? { ...rootIn, props: {} } : rootIn;
8768
- const vOpts = {
8769
- ...state.validation,
8770
- ...options?.validation
8771
- };
8772
- if (vOpts.enabled !== false) {
8773
- const result = validateDocument(
8774
- internalDocument,
8775
- state.components,
8776
- { allowUnknownFields: vOpts.allowUnknownFields }
9004
+ async function expandDocument(document, options) {
9005
+ const preserveSet = resolvePreserveSet(options);
9006
+ const rootIn = document;
9007
+ const internalDocument = rootIn.props === void 0 ? { ...rootIn, props: {} } : rootIn;
9008
+ const vOpts = {
9009
+ ...state.validation,
9010
+ ...options?.validation
9011
+ };
9012
+ if (vOpts.enabled !== false) {
9013
+ const result = validateDocument(
9014
+ internalDocument,
9015
+ state.components,
9016
+ { allowUnknownFields: vOpts.allowUnknownFields }
9017
+ );
9018
+ if (!result.valid) {
9019
+ throw new ComponentValidationError2(
9020
+ (result.errors ?? []).map((e) => ({
9021
+ path: e.path ?? "",
9022
+ message: e.message
9023
+ })),
9024
+ internalDocument
8777
9025
  );
8778
- if (!result.valid) {
8779
- throw new ComponentValidationError2(
8780
- (result.errors ?? []).map((e) => ({
8781
- path: e.path ?? "",
8782
- message: e.message
8783
- })),
8784
- internalDocument
8785
- );
8786
- }
8787
9026
  }
8788
- const validateEmitted = vOpts.enabled === false ? void 0 : (emitted, componentLabel) => {
8789
- const result = validateDocument(
8790
- { ...internalDocument, children: emitted },
8791
- state.components,
8792
- { allowUnknownFields: vOpts.allowUnknownFields }
9027
+ }
9028
+ const validateEmitted = vOpts.enabled === false ? void 0 : (emitted, componentLabel) => {
9029
+ const result = validateDocument(
9030
+ { ...internalDocument, children: emitted },
9031
+ state.components,
9032
+ { allowUnknownFields: vOpts.allowUnknownFields }
9033
+ );
9034
+ if (!result.valid) {
9035
+ throw new ComponentValidationError2(
9036
+ (result.errors ?? []).map((e) => ({
9037
+ path: e.path ?? "",
9038
+ message: `custom component '${componentLabel}' emitted invalid output \u2014 ${e.message}`
9039
+ })),
9040
+ emitted
8793
9041
  );
8794
- if (!result.valid) {
8795
- throw new ComponentValidationError2(
8796
- (result.errors ?? []).map((e) => ({
8797
- path: e.path ?? "",
8798
- message: `custom component '${componentLabel}' emitted invalid output \u2014 ${e.message}`
8799
- })),
8800
- emitted
8801
- );
8802
- }
9042
+ }
9043
+ };
9044
+ const warnings = [];
9045
+ const {
9046
+ document: modedRoot,
9047
+ theme: modedTheme,
9048
+ themeName
9049
+ } = resolveThemeContext(internalDocument, {
9050
+ customThemes: state.customThemes,
9051
+ fonts: state.fonts,
9052
+ warnings,
9053
+ resolveNamedTheme: resolveDocumentTheme
9054
+ });
9055
+ const processed = await processDocumentComponents(
9056
+ modedRoot.children || [],
9057
+ preserveSet,
9058
+ warnings,
9059
+ modedTheme,
9060
+ validateEmitted
9061
+ );
9062
+ const processedDocument = {
9063
+ ...modedRoot,
9064
+ children: processed.standard
9065
+ };
9066
+ const [modedDoc] = normalizeDocument(processedDocument);
9067
+ return {
9068
+ modedRoot,
9069
+ modedTheme,
9070
+ themeName,
9071
+ processed,
9072
+ modedDoc,
9073
+ warnings,
9074
+ preserveSet
9075
+ };
9076
+ }
9077
+ async function expandStandardDefinition(document, options) {
9078
+ try {
9079
+ const { modedDoc, warnings } = await expandDocument(document, options);
9080
+ return {
9081
+ standardDefinition: modedDoc,
9082
+ warnings: warnings.length > 0 ? warnings : null
8803
9083
  };
8804
- const warnings = [];
9084
+ } catch (error) {
9085
+ if (state.debug) {
9086
+ console.error("Document expansion error:", error);
9087
+ }
9088
+ throw error;
9089
+ }
9090
+ }
9091
+ async function generate(document, options) {
9092
+ try {
8805
9093
  const {
8806
- document: modedRoot,
8807
- theme: modedTheme,
8808
- themeName
8809
- } = resolveThemeContext(internalDocument, {
8810
- customThemes: state.customThemes,
8811
- fonts: state.fonts,
8812
- warnings,
8813
- resolveNamedTheme: resolveDocumentTheme
8814
- });
8815
- const processed = await processDocumentComponents(
8816
- modedRoot.children || [],
8817
- preserveSet,
8818
- warnings,
9094
+ modedRoot,
8819
9095
  modedTheme,
8820
- validateEmitted
8821
- );
8822
- const processedDocument = {
8823
- ...modedRoot,
8824
- children: processed.standard
8825
- };
8826
- const [modedDoc] = normalizeDocument(processedDocument);
9096
+ themeName,
9097
+ processed,
9098
+ modedDoc,
9099
+ warnings,
9100
+ preserveSet
9101
+ } = await expandDocument(document, options);
8827
9102
  await resolveDocumentFonts(modedDoc, modedTheme, state.fonts, warnings);
8828
9103
  const packageOptions = {
8829
9104
  deterministic: options?.deterministic ?? state.deterministic,
@@ -8950,6 +9225,7 @@ function createBuilderImpl(state) {
8950
9225
  generate,
8951
9226
  generateBuffer,
8952
9227
  generateFile,
9228
+ expandStandardDefinition,
8953
9229
  getComponentNames,
8954
9230
  validate,
8955
9231
  generateSchema,