@json-to-office/core-docx 0.28.0 → 0.29.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.
@@ -12,6 +12,8 @@ import { ComponentDefinition, RenderContext } from '../types';
12
12
  import { ThemeConfig } from '../styles';
13
13
  import { type ImageOptions } from '../core/content';
14
14
  import type { VisualProps } from '@json-to-office/shared-docx';
15
+ import { type PptxServiceConfig, type PptxRasterizeResult } from '@json-to-office/shared';
16
+ export declare const DEFAULT_RASTERIZE_SERVER_URL = "http://localhost:7802";
15
17
  /**
16
18
  * Build a single-slide pptx presentation component definition from visual props.
17
19
  * The shape mirrors a normal `.pptx.json` document so the pptx engine renders
@@ -35,6 +37,31 @@ export declare function visualToImageOptions(props: VisualProps): ImageOptions;
35
37
  * `flattenVisuals` transform to produce a portable, service-free document).
36
38
  */
37
39
  export declare function visualToImageProps(props: VisualProps, base64DataUri: string): Record<string, unknown>;
40
+ /**
41
+ * Identity of one visual rasterization: content + resolution + (for HTTP
42
+ * services) the target server. Keys the per-document pre-rasterization map
43
+ * (#153); the pre-pass and the render-time lookup MUST use this one function
44
+ * so the two sides cannot drift.
45
+ */
46
+ export declare function visualRasterKey(presentation: unknown, dpi: number, serverUrl?: string): string;
47
+ /**
48
+ * The serverUrl that actually differentiates a visual's rasterization. An
49
+ * in-process `render` callback takes precedence over any serverUrl —
50
+ * per-visual or config — so overrides only matter when rasterization would
51
+ * go over HTTP. Deliberately keyed on `render` alone (NOT `renderBatch`):
52
+ * per-visual fallbacks always go through `rasterizeVisualSlide`, which only
53
+ * consults `render`, and pre-pass inclusion must match what a fallback would
54
+ * actually do or the same visual could rasterize against two different
55
+ * services depending on cache luck.
56
+ */
57
+ export declare function effectiveVisualServerUrl(props: VisualProps, serviceConfig: PptxServiceConfig | undefined): string | undefined;
58
+ /**
59
+ * Rasterize a single-slide presentation to a PNG via the configured service.
60
+ * An in-process `render` callback takes precedence over an HTTP `serverUrl`.
61
+ * Exported for the pre-rasterization pass, whose per-visual fallback must
62
+ * behave exactly like render-time rasterization.
63
+ */
64
+ export declare function rasterizeVisualSlide(presentation: Record<string, unknown>, dpi: number, propsServerUrl: string | undefined, serviceConfig: PptxServiceConfig | undefined, baseDir: string | undefined): Promise<PptxRasterizeResult>;
38
65
  /**
39
66
  * Render visual component
40
67
  */
@@ -1 +1 @@
1
- {"version":3,"file":"visual.d.ts","sourceRoot":"","sources":["../../src/components/visual.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,MAAM,CAAC;AACxC,OAAO,EACL,mBAAmB,EACnB,aAAa,EAEd,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAKjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAY/D;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,WAAW,GACjB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAuBzB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,WAAW,GAAG,YAAY,CAWrE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,WAAW,EAClB,aAAa,EAAE,MAAM,GACpB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAMzB;AA2DD;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,mBAAmB,EAC9B,KAAK,EAAE,WAAW,EAClB,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,aAAa,GACtB,OAAO,CAAC,CAAC,SAAS,GAAG,KAAK,CAAC,EAAE,CAAC,CA+BhC"}
1
+ {"version":3,"file":"visual.d.ts","sourceRoot":"","sources":["../../src/components/visual.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,MAAM,CAAC;AACxC,OAAO,EACL,mBAAmB,EACnB,aAAa,EAEd,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAKjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAGL,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACzB,MAAM,wBAAwB,CAAC;AAEhC,eAAO,MAAM,4BAA4B,0BAA0B,CAAC;AAIpE;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,WAAW,GACjB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAuBzB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,WAAW,GAAG,YAAY,CAWrE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,WAAW,EAClB,aAAa,EAAE,MAAM,GACpB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAMzB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,YAAY,EAAE,OAAO,EACrB,GAAG,EAAE,MAAM,EACX,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CAIR;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,WAAW,EAClB,aAAa,EAAE,iBAAiB,GAAG,SAAS,GAC3C,MAAM,GAAG,SAAS,CAGpB;AAED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,GAAG,EAAE,MAAM,EACX,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,aAAa,EAAE,iBAAiB,GAAG,SAAS,EAC5C,OAAO,EAAE,MAAM,GAAG,SAAS,GAC1B,OAAO,CAAC,mBAAmB,CAAC,CA6C9B;AAED;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,mBAAmB,EAC9B,KAAK,EAAE,WAAW,EAClB,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,aAAa,GACtB,OAAO,CAAC,CAAC,SAAS,GAAG,KAAK,CAAC,EAAE,CAAC,CAkDhC"}
@@ -13,10 +13,16 @@
13
13
  * just `children`: section `props.header`/`props.footer`, and table
14
14
  * `props.columns[].cells[].content` / `props.columns[].header.content`.
15
15
  */
16
- import { type PptxRasterizer } from '@json-to-office/shared';
16
+ import { type PptxRasterizer, type PptxBatchRasterizer } from '@json-to-office/shared';
17
17
  export interface FlattenVisualsOptions {
18
18
  /** Rasterizer that turns a single-slide pptx presentation into a PNG. */
19
19
  rasterize: PptxRasterizer;
20
+ /**
21
+ * Batch rasterizer (#153). When provided, all visuals are collected and
22
+ * rasterized together up front; `rasterize` remains the per-visual
23
+ * fallback for batch failures and anything the collection missed.
24
+ */
25
+ rasterizeBatch?: PptxBatchRasterizer;
20
26
  /** Default DPI applied when a `visual` does not specify one (default 200). */
21
27
  dpi?: number;
22
28
  /** Max concurrent rasterizations (default 4). */
@@ -29,7 +35,9 @@ export interface FlattenVisualsOptions {
29
35
  }
30
36
  /**
31
37
  * Return a deep copy of `doc` with every `visual` node replaced by the `image`
32
- * node it rasterizes to. Visuals rasterize with bounded concurrency. A
38
+ * node it rasterizes to. With `rasterizeBatch`, visuals are pre-rasterized in
39
+ * batches and the tree walk resolves from the batch results; otherwise (and
40
+ * for any batch miss) visuals rasterize per-node with bounded concurrency. A
33
41
  * `visual` with `enabled: false` is left untouched (it's filtered out at render
34
42
  * anyway, so rasterizing it would be wasted work). Non-visual nodes are copied
35
43
  * structurally and otherwise unchanged.
@@ -1 +1 @@
1
- {"version":3,"file":"flattenVisuals.d.ts","sourceRoot":"","sources":["../../src/core/flattenVisuals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,wBAAwB,CAAC;AAShC,MAAM,WAAW,qBAAqB;IACpC,yEAAyE;IACzE,SAAS,EAAE,cAAc,CAAC;IAC1B,8EAA8E;IAC9E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA6BD;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,CAAC,GAAG,OAAO,EAC9C,GAAG,EAAE,CAAC,EACN,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,CAAC,CAAC,CAUZ"}
1
+ {"version":3,"file":"flattenVisuals.d.ts","sourceRoot":"","sources":["../../src/core/flattenVisuals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACzB,MAAM,wBAAwB,CAAC;AAYhC,MAAM,WAAW,qBAAqB;IACpC,yEAAyE;IACzE,SAAS,EAAE,cAAc,CAAC;IAC1B;;;;OAIG;IACH,cAAc,CAAC,EAAE,mBAAmB,CAAC;IACrC,8EAA8E;IAC9E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AASD;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAAC,CAAC,GAAG,OAAO,EAC9C,GAAG,EAAE,CAAC,EACN,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,CAAC,CAAC,CA0CZ"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * prerasterizeVisuals — per-document visual rasterization pre-pass (#153).
3
+ *
4
+ * The renderer walks components strictly in order, so each `visual` used to
5
+ * await its own rasterization round trip: one HTTP call + one LibreOffice
6
+ * launch per visual, ~25 of each for the bundled annual-report templates.
7
+ * This pass runs once per document before rendering: it collects every
8
+ * enabled visual, dedupes identical ones, and rasterizes them together —
9
+ * through an in-process batch rasterizer, a batched HTTP call, or (failing
10
+ * both) bounded-concurrency per-visual calls.
11
+ *
12
+ * Results land in a map keyed by `visualRasterKey`; `renderVisualComponent`
13
+ * consults the map first and falls back to per-visual rasterization on any
14
+ * miss. The pre-pass is therefore purely an accelerator: over-collection
15
+ * wastes a little work, under-collection and every failure mode degrade to
16
+ * the sequential status quo, never to a broken render.
17
+ */
18
+ import { type PptxServiceConfig, type PptxRasterizeBatchSlideResult } from '@json-to-office/shared';
19
+ import type { VisualProps } from '@json-to-office/shared-docx';
20
+ export interface PrerasterizeOptions {
21
+ /** Directory relative asset paths resolve against (#142). */
22
+ baseDir?: string;
23
+ /** Max concurrent per-visual fallback rasterizations (default 4). */
24
+ concurrency?: number;
25
+ }
26
+ /**
27
+ * Collect the props of every enabled `visual` component reachable from
28
+ * `root`. The walk is generic — every array element and object value is
29
+ * visited — so visuals nested anywhere (columns, table cells, section
30
+ * headers/footers, plugin containers) are found without enumerating
31
+ * container shapes. Disabled component subtrees are pruned: they never
32
+ * render, so rasterizing them is wasted work. Over-collection is harmless
33
+ * (an unused map entry); under-collection is harmless (render-time
34
+ * fallback) — which is what makes the generic walk safe.
35
+ */
36
+ export declare function collectVisualProps(root: unknown): VisualProps[];
37
+ /**
38
+ * Batch-rasterize a document's visuals ahead of rendering. Returns a map of
39
+ * `visualRasterKey` → per-visual result (pixels or a recorded error). Never
40
+ * throws for per-visual problems; a thrown error here indicates a pre-pass
41
+ * bug and the caller should continue without the map.
42
+ */
43
+ export declare function prerasterizeVisuals(root: unknown, serviceConfig: PptxServiceConfig | undefined, options?: PrerasterizeOptions): Promise<Map<string, PptxRasterizeBatchSlideResult>>;
44
+ //# sourceMappingURL=prerasterizeVisuals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prerasterizeVisuals.d.ts","sourceRoot":"","sources":["../../src/core/prerasterizeVisuals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAIL,KAAK,iBAAiB,EAEtB,KAAK,6BAA6B,EACnC,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAkB/D,MAAM,WAAW,mBAAmB;IAClC,6DAA6D;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AASD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,GAAG,WAAW,EAAE,CA4B/D;AAsDD;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,OAAO,EACb,aAAa,EAAE,iBAAiB,GAAG,SAAS,EAC5C,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,6BAA6B,CAAC,CAAC,CAmJrD"}
@@ -1 +1 @@
1
- {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/core/render.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,QAAQ,EACR,SAAS,EACT,KAAK,EACL,eAAe,EAEf,eAAe,EAKf,OAAO,EACR,MAAM,MAAM,CAAC;AAcd,OAAO,EACL,mBAAmB,EACnB,aAAa,EAed,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAIxC,OAAO,EAEL,iBAAiB,EAElB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAKrD,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AA6B7D,UAAU,qBAAqB;IAC7B,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA8DD;;GAEG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,iBAAiB,EAC5B,MAAM,EAAE,UAAU,EAClB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,QAAQ,CAAC,CAYnB;AA6XD;;GAEG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,aAAa,EACtB,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,aAAa,EACtB,cAAc,CAAC,EAAE,MAAM,EACvB,aAAa,CAAC,EAAE,OAAO,EACvB,WAAW,UAAQ,GAClB,OAAO,CAAC,eAAe,CAAC,CAiH1B;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,mBAAmB,EAC9B,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,CAAC,SAAS,GAAG,KAAK,GAAG,eAAe,GAAG,OAAO,CAAC,EAAE,CAAC,CAmC5D"}
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/core/render.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,QAAQ,EACR,SAAS,EACT,KAAK,EACL,eAAe,EAEf,eAAe,EAKf,OAAO,EACR,MAAM,MAAM,CAAC;AAcd,OAAO,EACL,mBAAmB,EACnB,aAAa,EAed,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAIxC,OAAO,EAEL,iBAAiB,EAElB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAKrD,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AA+B7D,UAAU,qBAAqB;IAC7B,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA8DD;;GAEG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,iBAAiB,EAC5B,MAAM,EAAE,UAAU,EAClB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,QAAQ,CAAC,CAYnB;AAkZD;;GAEG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,aAAa,EACtB,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,aAAa,EACtB,cAAc,CAAC,EAAE,MAAM,EACvB,aAAa,CAAC,EAAE,OAAO,EACvB,WAAW,UAAQ,GAClB,OAAO,CAAC,eAAe,CAAC,CAiH1B;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,mBAAmB,EAC9B,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,CAAC,SAAS,GAAG,KAAK,GAAG,eAAe,GAAG,OAAO,CAAC,EAAE,CAAC,CAmC5D"}
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),