@overtone-art/canvas-editor-core 0.2.6 → 0.2.7

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.
@@ -0,0 +1,37 @@
1
+ import { c as EditorState } from './types-bOY7oYVq.mjs';
2
+ import 'fabric';
3
+
4
+ interface NodeRenderOptions {
5
+ format?: 'png' | 'jpeg' | 'webp' | 'svg';
6
+ multiplier?: number;
7
+ quality?: number;
8
+ /**
9
+ * Gate every image URL referenced by the state before it is loaded. States
10
+ * are authored in the browser and therefore untrusted on a server; the
11
+ * default policy allows only `http:`, `https:` and `data:`. Supply a
12
+ * narrower predicate (e.g. an origin allowlist) to also stop requests to
13
+ * internal hosts.
14
+ */
15
+ allowImageUrl?: (url: string) => boolean;
16
+ }
17
+ interface NodeRenderResult {
18
+ format: 'png' | 'jpeg' | 'webp' | 'svg';
19
+ mimeType: string;
20
+ data: Uint8Array | string;
21
+ width: number;
22
+ height: number;
23
+ }
24
+ /** Render browser-authored EditorState without a DOM or live editor instance. */
25
+ declare function renderEditorState(state: EditorState, options?: NodeRenderOptions): Promise<NodeRenderResult>;
26
+ /** Render a saved product mockup with the transparent design composited above it. */
27
+ declare function renderMockupState(state: EditorState, options?: NodeRenderOptions): Promise<NodeRenderResult>;
28
+ /** Render multiple states with bounded concurrency for fulfillment jobs. */
29
+ declare function renderEditorStateBatch(states: EditorState[], options?: NodeRenderOptions & {
30
+ concurrency?: number;
31
+ }): Promise<NodeRenderResult[]>;
32
+ /** Render multiple mockup states with bounded concurrency for fulfillment jobs. */
33
+ declare function renderMockupStateBatch(states: EditorState[], options?: NodeRenderOptions & {
34
+ concurrency?: number;
35
+ }): Promise<NodeRenderResult[]>;
36
+
37
+ export { type NodeRenderOptions, type NodeRenderResult, renderEditorState, renderEditorStateBatch, renderMockupState, renderMockupStateBatch };
package/dist/node.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { c as EditorState } from './types-bOY7oYVq.js';
2
+ import 'fabric';
3
+
4
+ interface NodeRenderOptions {
5
+ format?: 'png' | 'jpeg' | 'webp' | 'svg';
6
+ multiplier?: number;
7
+ quality?: number;
8
+ /**
9
+ * Gate every image URL referenced by the state before it is loaded. States
10
+ * are authored in the browser and therefore untrusted on a server; the
11
+ * default policy allows only `http:`, `https:` and `data:`. Supply a
12
+ * narrower predicate (e.g. an origin allowlist) to also stop requests to
13
+ * internal hosts.
14
+ */
15
+ allowImageUrl?: (url: string) => boolean;
16
+ }
17
+ interface NodeRenderResult {
18
+ format: 'png' | 'jpeg' | 'webp' | 'svg';
19
+ mimeType: string;
20
+ data: Uint8Array | string;
21
+ width: number;
22
+ height: number;
23
+ }
24
+ /** Render browser-authored EditorState without a DOM or live editor instance. */
25
+ declare function renderEditorState(state: EditorState, options?: NodeRenderOptions): Promise<NodeRenderResult>;
26
+ /** Render a saved product mockup with the transparent design composited above it. */
27
+ declare function renderMockupState(state: EditorState, options?: NodeRenderOptions): Promise<NodeRenderResult>;
28
+ /** Render multiple states with bounded concurrency for fulfillment jobs. */
29
+ declare function renderEditorStateBatch(states: EditorState[], options?: NodeRenderOptions & {
30
+ concurrency?: number;
31
+ }): Promise<NodeRenderResult[]>;
32
+ /** Render multiple mockup states with bounded concurrency for fulfillment jobs. */
33
+ declare function renderMockupStateBatch(states: EditorState[], options?: NodeRenderOptions & {
34
+ concurrency?: number;
35
+ }): Promise<NodeRenderResult[]>;
36
+
37
+ export { type NodeRenderOptions, type NodeRenderResult, renderEditorState, renderEditorStateBatch, renderMockupState, renderMockupStateBatch };
package/dist/node.js ADDED
@@ -0,0 +1,281 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/node.ts
21
+ var node_exports = {};
22
+ __export(node_exports, {
23
+ renderEditorState: () => renderEditorState,
24
+ renderEditorStateBatch: () => renderEditorStateBatch,
25
+ renderMockupState: () => renderMockupState,
26
+ renderMockupStateBatch: () => renderMockupStateBatch
27
+ });
28
+ module.exports = __toCommonJS(node_exports);
29
+ var import_node = require("fabric/node");
30
+
31
+ // src/export.ts
32
+ function computePrintAreaClip(area, scaleX, scaleY, targetWidth, targetHeight) {
33
+ const left = Math.max(0, Math.min(targetWidth, area.left * scaleX));
34
+ const top = Math.max(0, Math.min(targetHeight, area.top * scaleY));
35
+ const right = Math.max(left, Math.min(targetWidth, (area.left + area.width) * scaleX));
36
+ const bottom = Math.max(top, Math.min(targetHeight, (area.top + area.height) * scaleY));
37
+ return { left, top, width: right - left, height: bottom - top };
38
+ }
39
+ function computeCoverPlacement(sourceWidth, sourceHeight, targetWidth, targetHeight) {
40
+ if (sourceWidth <= 0 || sourceHeight <= 0 || targetWidth <= 0 || targetHeight <= 0) {
41
+ throw new Error("Cover dimensions must be positive");
42
+ }
43
+ const scale = Math.max(targetWidth / sourceWidth, targetHeight / sourceHeight);
44
+ const width = sourceWidth * scale;
45
+ const height = sourceHeight * scale;
46
+ return {
47
+ left: (targetWidth - width) / 2,
48
+ top: (targetHeight - height) / 2,
49
+ width,
50
+ height
51
+ };
52
+ }
53
+
54
+ // src/node.ts
55
+ var ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "data:"]);
56
+ var URL_KEYS = /* @__PURE__ */ new Set(["src", "image"]);
57
+ function defaultAllowImageUrl(url) {
58
+ const scheme = /^([a-z][a-z\d+.-]*):/i.exec(url.trim());
59
+ return scheme !== null && ALLOWED_PROTOCOLS.has(`${scheme[1].toLowerCase()}:`);
60
+ }
61
+ function assertImageUrlsAllowed(value, allow) {
62
+ if (Array.isArray(value)) {
63
+ for (const item of value) assertImageUrlsAllowed(item, allow);
64
+ return;
65
+ }
66
+ if (!value || typeof value !== "object") return;
67
+ for (const [key, entry] of Object.entries(value)) {
68
+ if (typeof entry === "string") {
69
+ if (URL_KEYS.has(key) && !allow(entry)) {
70
+ throw new Error(`Blocked disallowed image URL: ${entry.slice(0, 120)}`);
71
+ }
72
+ } else {
73
+ assertImageUrlsAllowed(entry, allow);
74
+ }
75
+ }
76
+ }
77
+ function validateState(state) {
78
+ if (!state?.canvas || !Array.isArray(state.layers) || !Number.isFinite(state.canvas.width) || !Number.isFinite(state.canvas.height) || state.canvas.width <= 0 || state.canvas.height <= 0) {
79
+ throw new Error("Invalid editor state");
80
+ }
81
+ const major = Number.parseInt(state.version?.split(".")[0] ?? "1", 10);
82
+ if (!Number.isFinite(major) || major > 2) {
83
+ throw new Error(`Unsupported editor state version: ${state.version}`);
84
+ }
85
+ }
86
+ function dataUrlBytes(dataUrl) {
87
+ const encoded = dataUrl.slice(dataUrl.indexOf(",") + 1);
88
+ const binary = atob(encoded);
89
+ const bytes = new Uint8Array(binary.length);
90
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
91
+ return bytes;
92
+ }
93
+ function validateOptions(options) {
94
+ const format = options.format ?? "png";
95
+ const multiplier = options.multiplier ?? 1;
96
+ const quality = options.quality ?? 1;
97
+ const allowImageUrl = options.allowImageUrl ?? defaultAllowImageUrl;
98
+ if (!Number.isFinite(multiplier) || multiplier <= 0) {
99
+ throw new Error("Render multiplier must be positive");
100
+ }
101
+ if (!Number.isFinite(quality) || quality < 0 || quality > 1) {
102
+ throw new Error("Render quality must be between 0 and 1");
103
+ }
104
+ return { format, multiplier, quality, allowImageUrl };
105
+ }
106
+ async function createStateCanvas(state, transparent = false) {
107
+ const canvas = new import_node.StaticCanvas(void 0, {
108
+ width: state.canvas.width,
109
+ height: state.canvas.height,
110
+ backgroundColor: transparent ? "" : state.background ?? "",
111
+ preserveObjectStacking: true
112
+ });
113
+ try {
114
+ if (!transparent && state.backgroundImage) {
115
+ canvas.backgroundImage = (await import_node.util.enlivenObjects([state.backgroundImage]))[0];
116
+ }
117
+ const objects = await import_node.util.enlivenObjects(
118
+ state.layers.map((layer) => layer.fabricObject)
119
+ );
120
+ objects.forEach((object, index) => {
121
+ const layer = state.layers[index];
122
+ object.set({ visible: layer.visible, opacity: layer.opacity });
123
+ canvas.add(object);
124
+ });
125
+ canvas.renderAll();
126
+ return canvas;
127
+ } catch (error) {
128
+ canvas.dispose();
129
+ throw error;
130
+ }
131
+ }
132
+ function encodeCanvas(canvas, options) {
133
+ const { format, multiplier, quality } = options;
134
+ const width = Math.round(canvas.getWidth() * multiplier);
135
+ const height = Math.round(canvas.getHeight() * multiplier);
136
+ if (format === "svg") {
137
+ return { format, mimeType: "image/svg+xml", data: canvas.toSVG(), width, height };
138
+ }
139
+ const dataUrl = canvas.toDataURL({ format, multiplier, quality });
140
+ return {
141
+ format,
142
+ mimeType: format === "jpeg" ? "image/jpeg" : `image/${format}`,
143
+ data: dataUrlBytes(dataUrl),
144
+ width,
145
+ height
146
+ };
147
+ }
148
+ function compositeOperation(mode) {
149
+ return !mode || mode === "normal" ? "source-over" : mode;
150
+ }
151
+ function clampOpacity(value = 1) {
152
+ return Math.max(0, Math.min(1, value));
153
+ }
154
+ async function coverImage(url, width, height) {
155
+ let image;
156
+ try {
157
+ image = await import_node.FabricImage.fromURL(url);
158
+ } catch (error) {
159
+ throw new Error(`Failed to load mockup image: ${url}`, { cause: error });
160
+ }
161
+ const sourceWidth = image.width;
162
+ const sourceHeight = image.height;
163
+ const placement = computeCoverPlacement(sourceWidth, sourceHeight, width, height);
164
+ image.set({
165
+ originX: "left",
166
+ originY: "top",
167
+ left: placement.left,
168
+ top: placement.top,
169
+ scaleX: placement.width / sourceWidth,
170
+ scaleY: placement.height / sourceHeight,
171
+ selectable: false,
172
+ evented: false
173
+ });
174
+ return image;
175
+ }
176
+ async function renderEditorState(state, options = {}) {
177
+ validateState(state);
178
+ const resolved = validateOptions(options);
179
+ assertImageUrlsAllowed(state, resolved.allowImageUrl);
180
+ const canvas = await createStateCanvas(state);
181
+ try {
182
+ return encodeCanvas(canvas, resolved);
183
+ } finally {
184
+ canvas.dispose();
185
+ }
186
+ }
187
+ async function renderMockupState(state, options = {}) {
188
+ validateState(state);
189
+ const resolved = validateOptions(options);
190
+ assertImageUrlsAllowed(state, resolved.allowImageUrl);
191
+ if (resolved.format === "svg") {
192
+ throw new Error("Mockup rendering supports PNG, JPEG, and WebP output");
193
+ }
194
+ const mockup = state.mockup;
195
+ if (!mockup?.image) throw new Error("No mockup is configured");
196
+ const designCanvas = await createStateCanvas(state, true);
197
+ const output = new import_node.StaticCanvas(void 0, {
198
+ width: state.canvas.width,
199
+ height: state.canvas.height,
200
+ preserveObjectStacking: true
201
+ });
202
+ try {
203
+ output.add(await coverImage(mockup.image, state.canvas.width, state.canvas.height));
204
+ const designUrl = designCanvas.toDataURL({ format: "png", multiplier: 1 });
205
+ const design = await import_node.FabricImage.fromURL(designUrl);
206
+ design.set({
207
+ originX: "left",
208
+ originY: "top",
209
+ left: 0,
210
+ top: 0,
211
+ opacity: clampOpacity(mockup.designOpacity),
212
+ globalCompositeOperation: compositeOperation(mockup.designBlendMode),
213
+ selectable: false,
214
+ evented: false
215
+ });
216
+ if (mockup.printArea && mockup.clipToPrintArea !== false) {
217
+ const clip = computePrintAreaClip(
218
+ mockup.printArea,
219
+ 1,
220
+ 1,
221
+ state.canvas.width,
222
+ state.canvas.height
223
+ );
224
+ design.clipPath = new import_node.Rect({
225
+ originX: "left",
226
+ originY: "top",
227
+ left: clip.left,
228
+ top: clip.top,
229
+ width: clip.width,
230
+ height: clip.height,
231
+ absolutePositioned: true
232
+ });
233
+ }
234
+ output.add(design);
235
+ if (mockup.overlay) {
236
+ const overlay = await coverImage(
237
+ mockup.overlay.image,
238
+ state.canvas.width,
239
+ state.canvas.height
240
+ );
241
+ overlay.set({
242
+ opacity: clampOpacity(mockup.overlay.opacity),
243
+ globalCompositeOperation: compositeOperation(mockup.overlay.blendMode ?? "multiply")
244
+ });
245
+ output.add(overlay);
246
+ }
247
+ output.renderAll();
248
+ return encodeCanvas(output, resolved);
249
+ } finally {
250
+ designCanvas.dispose();
251
+ output.dispose();
252
+ }
253
+ }
254
+ async function renderBatch(states, options, renderer) {
255
+ const concurrency = Math.max(1, Math.floor(options.concurrency ?? 2));
256
+ const results = new Array(states.length);
257
+ let nextIndex = 0;
258
+ await Promise.all(
259
+ Array.from({ length: Math.min(concurrency, states.length) }, async () => {
260
+ while (nextIndex < states.length) {
261
+ const index = nextIndex++;
262
+ results[index] = await renderer(states[index], options);
263
+ }
264
+ })
265
+ );
266
+ return results;
267
+ }
268
+ async function renderEditorStateBatch(states, options = {}) {
269
+ return renderBatch(states, options, renderEditorState);
270
+ }
271
+ async function renderMockupStateBatch(states, options = {}) {
272
+ return renderBatch(states, options, renderMockupState);
273
+ }
274
+ // Annotate the CommonJS export names for ESM import in node:
275
+ 0 && (module.exports = {
276
+ renderEditorState,
277
+ renderEditorStateBatch,
278
+ renderMockupState,
279
+ renderMockupStateBatch
280
+ });
281
+ //# sourceMappingURL=node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/node.ts","../src/export.ts"],"sourcesContent":["import { FabricImage, Rect, StaticCanvas, util } from 'fabric/node';\nimport type { FabricObject, ImageFormat } from 'fabric/node';\nimport { computeCoverPlacement, computePrintAreaClip } from './export';\nimport type { EditorState, MockupBlendMode, MockupConfig } from './types';\n\nexport interface NodeRenderOptions {\n format?: 'png' | 'jpeg' | 'webp' | 'svg';\n multiplier?: number;\n quality?: number;\n /**\n * Gate every image URL referenced by the state before it is loaded. States\n * are authored in the browser and therefore untrusted on a server; the\n * default policy allows only `http:`, `https:` and `data:`. Supply a\n * narrower predicate (e.g. an origin allowlist) to also stop requests to\n * internal hosts.\n */\n allowImageUrl?: (url: string) => boolean;\n}\n\nexport interface NodeRenderResult {\n format: 'png' | 'jpeg' | 'webp' | 'svg';\n mimeType: string;\n data: Uint8Array | string;\n width: number;\n height: number;\n}\n\n// `fabric/node` resolves bare paths and `file:` URLs against the local disk, so\n// an unrestricted state is a local-file-read and SSRF primitive on a server.\nconst ALLOWED_PROTOCOLS = new Set(['http:', 'https:', 'data:']);\n\n/** Keys whose string values are fetched as images during a render. */\nconst URL_KEYS = new Set(['src', 'image']);\n\nfunction defaultAllowImageUrl(url: string): boolean {\n const scheme = /^([a-z][a-z\\d+.-]*):/i.exec(url.trim());\n return scheme !== null && ALLOWED_PROTOCOLS.has(`${scheme[1].toLowerCase()}:`);\n}\n\nfunction assertImageUrlsAllowed(value: unknown, allow: (url: string) => boolean): void {\n if (Array.isArray(value)) {\n for (const item of value) assertImageUrlsAllowed(item, allow);\n return;\n }\n if (!value || typeof value !== 'object') return;\n for (const [key, entry] of Object.entries(value)) {\n if (typeof entry === 'string') {\n if (URL_KEYS.has(key) && !allow(entry)) {\n throw new Error(`Blocked disallowed image URL: ${entry.slice(0, 120)}`);\n }\n } else {\n assertImageUrlsAllowed(entry, allow);\n }\n }\n}\n\nfunction validateState(state: EditorState): void {\n if (\n !state?.canvas ||\n !Array.isArray(state.layers) ||\n !Number.isFinite(state.canvas.width) ||\n !Number.isFinite(state.canvas.height) ||\n state.canvas.width <= 0 ||\n state.canvas.height <= 0\n ) {\n throw new Error('Invalid editor state');\n }\n const major = Number.parseInt(state.version?.split('.')[0] ?? '1', 10);\n if (!Number.isFinite(major) || major > 2) {\n throw new Error(`Unsupported editor state version: ${state.version}`);\n }\n}\n\nfunction dataUrlBytes(dataUrl: string): Uint8Array {\n const encoded = dataUrl.slice(dataUrl.indexOf(',') + 1);\n const binary = atob(encoded);\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);\n return bytes;\n}\n\nfunction validateOptions(options: NodeRenderOptions): Required<NodeRenderOptions> {\n const format = options.format ?? 'png';\n const multiplier = options.multiplier ?? 1;\n const quality = options.quality ?? 1;\n const allowImageUrl = options.allowImageUrl ?? defaultAllowImageUrl;\n if (!Number.isFinite(multiplier) || multiplier <= 0) {\n throw new Error('Render multiplier must be positive');\n }\n if (!Number.isFinite(quality) || quality < 0 || quality > 1) {\n throw new Error('Render quality must be between 0 and 1');\n }\n return { format, multiplier, quality, allowImageUrl };\n}\n\nasync function createStateCanvas(state: EditorState, transparent = false): Promise<StaticCanvas> {\n const canvas = new StaticCanvas(undefined, {\n width: state.canvas.width,\n height: state.canvas.height,\n backgroundColor: transparent ? '' : (state.background ?? ''),\n preserveObjectStacking: true,\n });\n try {\n if (!transparent && state.backgroundImage) {\n canvas.backgroundImage = (\n await util.enlivenObjects([state.backgroundImage])\n )[0] as FabricObject;\n }\n const objects = (await util.enlivenObjects(\n state.layers.map((layer) => layer.fabricObject),\n )) as FabricObject[];\n objects.forEach((object, index) => {\n const layer = state.layers[index];\n object.set({ visible: layer.visible, opacity: layer.opacity });\n canvas.add(object);\n });\n canvas.renderAll();\n return canvas;\n } catch (error) {\n canvas.dispose();\n throw error;\n }\n}\n\nfunction encodeCanvas(\n canvas: StaticCanvas,\n options: Required<NodeRenderOptions>,\n): NodeRenderResult {\n const { format, multiplier, quality } = options;\n const width = Math.round(canvas.getWidth() * multiplier);\n const height = Math.round(canvas.getHeight() * multiplier);\n if (format === 'svg') {\n return { format, mimeType: 'image/svg+xml', data: canvas.toSVG(), width, height };\n }\n const dataUrl = canvas.toDataURL({ format: format as ImageFormat, multiplier, quality });\n return {\n format,\n mimeType: format === 'jpeg' ? 'image/jpeg' : `image/${format}`,\n data: dataUrlBytes(dataUrl),\n width,\n height,\n };\n}\n\nfunction compositeOperation(mode?: MockupBlendMode): GlobalCompositeOperation {\n return !mode || mode === 'normal' ? 'source-over' : mode;\n}\n\nfunction clampOpacity(value = 1): number {\n return Math.max(0, Math.min(1, value));\n}\n\nasync function coverImage(url: string, width: number, height: number): Promise<FabricImage> {\n let image: FabricImage;\n try {\n image = await FabricImage.fromURL(url);\n } catch (error) {\n throw new Error(`Failed to load mockup image: ${url}`, { cause: error });\n }\n const sourceWidth = image.width;\n const sourceHeight = image.height;\n const placement = computeCoverPlacement(sourceWidth, sourceHeight, width, height);\n image.set({\n originX: 'left',\n originY: 'top',\n left: placement.left,\n top: placement.top,\n scaleX: placement.width / sourceWidth,\n scaleY: placement.height / sourceHeight,\n selectable: false,\n evented: false,\n });\n return image;\n}\n\n/** Render browser-authored EditorState without a DOM or live editor instance. */\nexport async function renderEditorState(\n state: EditorState,\n options: NodeRenderOptions = {},\n): Promise<NodeRenderResult> {\n validateState(state);\n const resolved = validateOptions(options);\n assertImageUrlsAllowed(state, resolved.allowImageUrl);\n const canvas = await createStateCanvas(state);\n try {\n return encodeCanvas(canvas, resolved);\n } finally {\n canvas.dispose();\n }\n}\n\n/** Render a saved product mockup with the transparent design composited above it. */\nexport async function renderMockupState(\n state: EditorState,\n options: NodeRenderOptions = {},\n): Promise<NodeRenderResult> {\n validateState(state);\n const resolved = validateOptions(options);\n assertImageUrlsAllowed(state, resolved.allowImageUrl);\n if (resolved.format === 'svg') {\n throw new Error('Mockup rendering supports PNG, JPEG, and WebP output');\n }\n const mockup: MockupConfig | null | undefined = state.mockup;\n if (!mockup?.image) throw new Error('No mockup is configured');\n\n const designCanvas = await createStateCanvas(state, true);\n const output = new StaticCanvas(undefined, {\n width: state.canvas.width,\n height: state.canvas.height,\n preserveObjectStacking: true,\n });\n try {\n output.add(await coverImage(mockup.image, state.canvas.width, state.canvas.height));\n\n const designUrl = designCanvas.toDataURL({ format: 'png', multiplier: 1 });\n const design = await FabricImage.fromURL(designUrl);\n design.set({\n originX: 'left',\n originY: 'top',\n left: 0,\n top: 0,\n opacity: clampOpacity(mockup.designOpacity),\n globalCompositeOperation: compositeOperation(mockup.designBlendMode),\n selectable: false,\n evented: false,\n });\n if (mockup.printArea && mockup.clipToPrintArea !== false) {\n const clip = computePrintAreaClip(\n mockup.printArea,\n 1,\n 1,\n state.canvas.width,\n state.canvas.height,\n );\n design.clipPath = new Rect({\n originX: 'left',\n originY: 'top',\n left: clip.left,\n top: clip.top,\n width: clip.width,\n height: clip.height,\n absolutePositioned: true,\n });\n }\n output.add(design);\n\n if (mockup.overlay) {\n const overlay = await coverImage(\n mockup.overlay.image,\n state.canvas.width,\n state.canvas.height,\n );\n overlay.set({\n opacity: clampOpacity(mockup.overlay.opacity),\n globalCompositeOperation: compositeOperation(mockup.overlay.blendMode ?? 'multiply'),\n });\n output.add(overlay);\n }\n output.renderAll();\n return encodeCanvas(output, resolved);\n } finally {\n designCanvas.dispose();\n output.dispose();\n }\n}\n\nasync function renderBatch(\n states: EditorState[],\n options: NodeRenderOptions & { concurrency?: number },\n renderer: (state: EditorState, options: NodeRenderOptions) => Promise<NodeRenderResult>,\n): Promise<NodeRenderResult[]> {\n const concurrency = Math.max(1, Math.floor(options.concurrency ?? 2));\n const results = new Array<NodeRenderResult>(states.length);\n let nextIndex = 0;\n await Promise.all(\n Array.from({ length: Math.min(concurrency, states.length) }, async () => {\n while (nextIndex < states.length) {\n const index = nextIndex++;\n results[index] = await renderer(states[index], options);\n }\n }),\n );\n return results;\n}\n\n/** Render multiple states with bounded concurrency for fulfillment jobs. */\nexport async function renderEditorStateBatch(\n states: EditorState[],\n options: NodeRenderOptions & { concurrency?: number } = {},\n): Promise<NodeRenderResult[]> {\n return renderBatch(states, options, renderEditorState);\n}\n\n/** Render multiple mockup states with bounded concurrency for fulfillment jobs. */\nexport async function renderMockupStateBatch(\n states: EditorState[],\n options: NodeRenderOptions & { concurrency?: number } = {},\n): Promise<NodeRenderResult[]> {\n return renderBatch(states, options, renderMockupState);\n}\n","import type { Canvas, ImageFormat } from 'fabric';\nimport type { MockupConfig, MockupPrintArea } from './types';\n\nexport interface PngExportOptions {\n multiplier?: number;\n format?: ImageFormat;\n quality?: number;\n}\n\nexport interface CoverPlacement {\n left: number;\n top: number;\n width: number;\n height: number;\n}\n\nexport function computePrintAreaClip(\n area: MockupPrintArea,\n scaleX: number,\n scaleY: number,\n targetWidth: number,\n targetHeight: number,\n): MockupPrintArea {\n const left = Math.max(0, Math.min(targetWidth, area.left * scaleX));\n const top = Math.max(0, Math.min(targetHeight, area.top * scaleY));\n const right = Math.max(left, Math.min(targetWidth, (area.left + area.width) * scaleX));\n const bottom = Math.max(top, Math.min(targetHeight, (area.top + area.height) * scaleY));\n return { left, top, width: right - left, height: bottom - top };\n}\n\n/** Object-fit: cover geometry, exported for deterministic preview/composite tests. */\nexport function computeCoverPlacement(\n sourceWidth: number,\n sourceHeight: number,\n targetWidth: number,\n targetHeight: number,\n): CoverPlacement {\n if (sourceWidth <= 0 || sourceHeight <= 0 || targetWidth <= 0 || targetHeight <= 0) {\n throw new Error('Cover dimensions must be positive');\n }\n const scale = Math.max(targetWidth / sourceWidth, targetHeight / sourceHeight);\n const width = sourceWidth * scale;\n const height = sourceHeight * scale;\n return {\n left: (targetWidth - width) / 2,\n top: (targetHeight - height) / 2,\n width,\n height,\n };\n}\n\nfunction canvasElementToBlob(\n output: HTMLCanvasElement,\n format: ImageFormat,\n quality: number,\n): Promise<Blob> {\n const mime = format === 'jpeg' ? 'image/jpeg' : `image/${format}`;\n return new Promise<Blob>((resolve, reject) => {\n output.toBlob(\n (blob) => (blob ? resolve(blob) : reject(new Error(`Failed to export ${format}`))),\n mime,\n quality,\n );\n });\n}\n\nexport async function exportPNG(canvas: Canvas, options: PngExportOptions = {}): Promise<Blob> {\n const { multiplier = 1, format = 'png' as ImageFormat, quality = 1 } = options;\n const output = canvas.toCanvasElement(multiplier);\n return canvasElementToBlob(output, format, quality);\n}\n\n/** Rasterize the browser mockup preview together with the transparent design. */\nexport async function exportMockup(\n canvas: Canvas,\n mockup: MockupConfig,\n options: PngExportOptions = {},\n): Promise<Blob> {\n const { multiplier = 1, format = 'png' as ImageFormat, quality = 1 } = options;\n const design = canvas.toCanvasElement(multiplier);\n const output = design.ownerDocument.createElement('canvas');\n output.width = design.width;\n output.height = design.height;\n const context = output.getContext('2d');\n if (!context) throw new Error('2D canvas context is unavailable');\n\n const loadImage = (url: string) =>\n new Promise<HTMLImageElement>((resolve, reject) => {\n const element = new Image();\n element.crossOrigin = 'anonymous';\n element.onload = () => resolve(element);\n element.onerror = () => reject(new Error(`Failed to load mockup image: ${url}`));\n element.src = url;\n });\n const drawCover = (image: HTMLImageElement) => {\n const placement = computeCoverPlacement(\n image.naturalWidth || image.width,\n image.naturalHeight || image.height,\n output.width,\n output.height,\n );\n context.drawImage(image, placement.left, placement.top, placement.width, placement.height);\n };\n\n drawCover(await loadImage(mockup.image));\n context.save();\n if (mockup.printArea && mockup.clipToPrintArea !== false) {\n const clip = computePrintAreaClip(\n mockup.printArea,\n output.width / canvas.getWidth(),\n output.height / canvas.getHeight(),\n output.width,\n output.height,\n );\n context.beginPath();\n context.rect(clip.left, clip.top, clip.width, clip.height);\n context.clip();\n }\n context.globalAlpha = Math.max(0, Math.min(1, mockup.designOpacity ?? 1));\n context.globalCompositeOperation =\n !mockup.designBlendMode || mockup.designBlendMode === 'normal'\n ? 'source-over'\n : mockup.designBlendMode;\n context.drawImage(design, 0, 0);\n context.restore();\n\n if (mockup.overlay) {\n context.save();\n context.globalAlpha = Math.max(0, Math.min(1, mockup.overlay.opacity ?? 1));\n context.globalCompositeOperation =\n mockup.overlay.blendMode === 'normal'\n ? 'source-over'\n : (mockup.overlay.blendMode ?? 'multiply');\n drawCover(await loadImage(mockup.overlay.image));\n context.restore();\n }\n return canvasElementToBlob(output, format, quality);\n}\n\nexport function exportSVG(canvas: Canvas): string {\n return canvas.toSVG();\n}\n\nexport function exportDataURL(canvas: Canvas, format: ImageFormat = 'png', multiplier = 1): string {\n return canvas.toDataURL({ format, multiplier });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAAsD;;;ACgB/C,SAAS,qBACd,MACA,QACA,QACA,aACA,cACiB;AACjB,QAAM,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,aAAa,KAAK,OAAO,MAAM,CAAC;AAClE,QAAM,MAAM,KAAK,IAAI,GAAG,KAAK,IAAI,cAAc,KAAK,MAAM,MAAM,CAAC;AACjE,QAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,IAAI,cAAc,KAAK,OAAO,KAAK,SAAS,MAAM,CAAC;AACrF,QAAM,SAAS,KAAK,IAAI,KAAK,KAAK,IAAI,eAAe,KAAK,MAAM,KAAK,UAAU,MAAM,CAAC;AACtF,SAAO,EAAE,MAAM,KAAK,OAAO,QAAQ,MAAM,QAAQ,SAAS,IAAI;AAChE;AAGO,SAAS,sBACd,aACA,cACA,aACA,cACgB;AAChB,MAAI,eAAe,KAAK,gBAAgB,KAAK,eAAe,KAAK,gBAAgB,GAAG;AAClF,UAAM,IAAI,MAAM,mCAAmC;AAAA,EACrD;AACA,QAAM,QAAQ,KAAK,IAAI,cAAc,aAAa,eAAe,YAAY;AAC7E,QAAM,QAAQ,cAAc;AAC5B,QAAM,SAAS,eAAe;AAC9B,SAAO;AAAA,IACL,OAAO,cAAc,SAAS;AAAA,IAC9B,MAAM,eAAe,UAAU;AAAA,IAC/B;AAAA,IACA;AAAA,EACF;AACF;;;ADpBA,IAAM,oBAAoB,oBAAI,IAAI,CAAC,SAAS,UAAU,OAAO,CAAC;AAG9D,IAAM,WAAW,oBAAI,IAAI,CAAC,OAAO,OAAO,CAAC;AAEzC,SAAS,qBAAqB,KAAsB;AAClD,QAAM,SAAS,wBAAwB,KAAK,IAAI,KAAK,CAAC;AACtD,SAAO,WAAW,QAAQ,kBAAkB,IAAI,GAAG,OAAO,CAAC,EAAE,YAAY,CAAC,GAAG;AAC/E;AAEA,SAAS,uBAAuB,OAAgB,OAAuC;AACrF,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,eAAW,QAAQ,MAAO,wBAAuB,MAAM,KAAK;AAC5D;AAAA,EACF;AACA,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU;AACzC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,QAAI,OAAO,UAAU,UAAU;AAC7B,UAAI,SAAS,IAAI,GAAG,KAAK,CAAC,MAAM,KAAK,GAAG;AACtC,cAAM,IAAI,MAAM,iCAAiC,MAAM,MAAM,GAAG,GAAG,CAAC,EAAE;AAAA,MACxE;AAAA,IACF,OAAO;AACL,6BAAuB,OAAO,KAAK;AAAA,IACrC;AAAA,EACF;AACF;AAEA,SAAS,cAAc,OAA0B;AAC/C,MACE,CAAC,OAAO,UACR,CAAC,MAAM,QAAQ,MAAM,MAAM,KAC3B,CAAC,OAAO,SAAS,MAAM,OAAO,KAAK,KACnC,CAAC,OAAO,SAAS,MAAM,OAAO,MAAM,KACpC,MAAM,OAAO,SAAS,KACtB,MAAM,OAAO,UAAU,GACvB;AACA,UAAM,IAAI,MAAM,sBAAsB;AAAA,EACxC;AACA,QAAM,QAAQ,OAAO,SAAS,MAAM,SAAS,MAAM,GAAG,EAAE,CAAC,KAAK,KAAK,EAAE;AACrE,MAAI,CAAC,OAAO,SAAS,KAAK,KAAK,QAAQ,GAAG;AACxC,UAAM,IAAI,MAAM,qCAAqC,MAAM,OAAO,EAAE;AAAA,EACtE;AACF;AAEA,SAAS,aAAa,SAA6B;AACjD,QAAM,UAAU,QAAQ,MAAM,QAAQ,QAAQ,GAAG,IAAI,CAAC;AACtD,QAAM,SAAS,KAAK,OAAO;AAC3B,QAAM,QAAQ,IAAI,WAAW,OAAO,MAAM;AAC1C,WAAS,QAAQ,GAAG,QAAQ,OAAO,QAAQ,SAAS,EAAG,OAAM,KAAK,IAAI,OAAO,WAAW,KAAK;AAC7F,SAAO;AACT;AAEA,SAAS,gBAAgB,SAAyD;AAChF,QAAM,SAAS,QAAQ,UAAU;AACjC,QAAM,aAAa,QAAQ,cAAc;AACzC,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,gBAAgB,QAAQ,iBAAiB;AAC/C,MAAI,CAAC,OAAO,SAAS,UAAU,KAAK,cAAc,GAAG;AACnD,UAAM,IAAI,MAAM,oCAAoC;AAAA,EACtD;AACA,MAAI,CAAC,OAAO,SAAS,OAAO,KAAK,UAAU,KAAK,UAAU,GAAG;AAC3D,UAAM,IAAI,MAAM,wCAAwC;AAAA,EAC1D;AACA,SAAO,EAAE,QAAQ,YAAY,SAAS,cAAc;AACtD;AAEA,eAAe,kBAAkB,OAAoB,cAAc,OAA8B;AAC/F,QAAM,SAAS,IAAI,yBAAa,QAAW;AAAA,IACzC,OAAO,MAAM,OAAO;AAAA,IACpB,QAAQ,MAAM,OAAO;AAAA,IACrB,iBAAiB,cAAc,KAAM,MAAM,cAAc;AAAA,IACzD,wBAAwB;AAAA,EAC1B,CAAC;AACD,MAAI;AACF,QAAI,CAAC,eAAe,MAAM,iBAAiB;AACzC,aAAO,mBACL,MAAM,iBAAK,eAAe,CAAC,MAAM,eAAe,CAAC,GACjD,CAAC;AAAA,IACL;AACA,UAAM,UAAW,MAAM,iBAAK;AAAA,MAC1B,MAAM,OAAO,IAAI,CAAC,UAAU,MAAM,YAAY;AAAA,IAChD;AACA,YAAQ,QAAQ,CAAC,QAAQ,UAAU;AACjC,YAAM,QAAQ,MAAM,OAAO,KAAK;AAChC,aAAO,IAAI,EAAE,SAAS,MAAM,SAAS,SAAS,MAAM,QAAQ,CAAC;AAC7D,aAAO,IAAI,MAAM;AAAA,IACnB,CAAC;AACD,WAAO,UAAU;AACjB,WAAO;AAAA,EACT,SAAS,OAAO;AACd,WAAO,QAAQ;AACf,UAAM;AAAA,EACR;AACF;AAEA,SAAS,aACP,QACA,SACkB;AAClB,QAAM,EAAE,QAAQ,YAAY,QAAQ,IAAI;AACxC,QAAM,QAAQ,KAAK,MAAM,OAAO,SAAS,IAAI,UAAU;AACvD,QAAM,SAAS,KAAK,MAAM,OAAO,UAAU,IAAI,UAAU;AACzD,MAAI,WAAW,OAAO;AACpB,WAAO,EAAE,QAAQ,UAAU,iBAAiB,MAAM,OAAO,MAAM,GAAG,OAAO,OAAO;AAAA,EAClF;AACA,QAAM,UAAU,OAAO,UAAU,EAAE,QAA+B,YAAY,QAAQ,CAAC;AACvF,SAAO;AAAA,IACL;AAAA,IACA,UAAU,WAAW,SAAS,eAAe,SAAS,MAAM;AAAA,IAC5D,MAAM,aAAa,OAAO;AAAA,IAC1B;AAAA,IACA;AAAA,EACF;AACF;AAEA,SAAS,mBAAmB,MAAkD;AAC5E,SAAO,CAAC,QAAQ,SAAS,WAAW,gBAAgB;AACtD;AAEA,SAAS,aAAa,QAAQ,GAAW;AACvC,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,CAAC;AACvC;AAEA,eAAe,WAAW,KAAa,OAAe,QAAsC;AAC1F,MAAI;AACJ,MAAI;AACF,YAAQ,MAAM,wBAAY,QAAQ,GAAG;AAAA,EACvC,SAAS,OAAO;AACd,UAAM,IAAI,MAAM,gCAAgC,GAAG,IAAI,EAAE,OAAO,MAAM,CAAC;AAAA,EACzE;AACA,QAAM,cAAc,MAAM;AAC1B,QAAM,eAAe,MAAM;AAC3B,QAAM,YAAY,sBAAsB,aAAa,cAAc,OAAO,MAAM;AAChF,QAAM,IAAI;AAAA,IACR,SAAS;AAAA,IACT,SAAS;AAAA,IACT,MAAM,UAAU;AAAA,IAChB,KAAK,UAAU;AAAA,IACf,QAAQ,UAAU,QAAQ;AAAA,IAC1B,QAAQ,UAAU,SAAS;AAAA,IAC3B,YAAY;AAAA,IACZ,SAAS;AAAA,EACX,CAAC;AACD,SAAO;AACT;AAGA,eAAsB,kBACpB,OACA,UAA6B,CAAC,GACH;AAC3B,gBAAc,KAAK;AACnB,QAAM,WAAW,gBAAgB,OAAO;AACxC,yBAAuB,OAAO,SAAS,aAAa;AACpD,QAAM,SAAS,MAAM,kBAAkB,KAAK;AAC5C,MAAI;AACF,WAAO,aAAa,QAAQ,QAAQ;AAAA,EACtC,UAAE;AACA,WAAO,QAAQ;AAAA,EACjB;AACF;AAGA,eAAsB,kBACpB,OACA,UAA6B,CAAC,GACH;AAC3B,gBAAc,KAAK;AACnB,QAAM,WAAW,gBAAgB,OAAO;AACxC,yBAAuB,OAAO,SAAS,aAAa;AACpD,MAAI,SAAS,WAAW,OAAO;AAC7B,UAAM,IAAI,MAAM,sDAAsD;AAAA,EACxE;AACA,QAAM,SAA0C,MAAM;AACtD,MAAI,CAAC,QAAQ,MAAO,OAAM,IAAI,MAAM,yBAAyB;AAE7D,QAAM,eAAe,MAAM,kBAAkB,OAAO,IAAI;AACxD,QAAM,SAAS,IAAI,yBAAa,QAAW;AAAA,IACzC,OAAO,MAAM,OAAO;AAAA,IACpB,QAAQ,MAAM,OAAO;AAAA,IACrB,wBAAwB;AAAA,EAC1B,CAAC;AACD,MAAI;AACF,WAAO,IAAI,MAAM,WAAW,OAAO,OAAO,MAAM,OAAO,OAAO,MAAM,OAAO,MAAM,CAAC;AAElF,UAAM,YAAY,aAAa,UAAU,EAAE,QAAQ,OAAO,YAAY,EAAE,CAAC;AACzE,UAAM,SAAS,MAAM,wBAAY,QAAQ,SAAS;AAClD,WAAO,IAAI;AAAA,MACT,SAAS;AAAA,MACT,SAAS;AAAA,MACT,MAAM;AAAA,MACN,KAAK;AAAA,MACL,SAAS,aAAa,OAAO,aAAa;AAAA,MAC1C,0BAA0B,mBAAmB,OAAO,eAAe;AAAA,MACnE,YAAY;AAAA,MACZ,SAAS;AAAA,IACX,CAAC;AACD,QAAI,OAAO,aAAa,OAAO,oBAAoB,OAAO;AACxD,YAAM,OAAO;AAAA,QACX,OAAO;AAAA,QACP;AAAA,QACA;AAAA,QACA,MAAM,OAAO;AAAA,QACb,MAAM,OAAO;AAAA,MACf;AACA,aAAO,WAAW,IAAI,iBAAK;AAAA,QACzB,SAAS;AAAA,QACT,SAAS;AAAA,QACT,MAAM,KAAK;AAAA,QACX,KAAK,KAAK;AAAA,QACV,OAAO,KAAK;AAAA,QACZ,QAAQ,KAAK;AAAA,QACb,oBAAoB;AAAA,MACtB,CAAC;AAAA,IACH;AACA,WAAO,IAAI,MAAM;AAEjB,QAAI,OAAO,SAAS;AAClB,YAAM,UAAU,MAAM;AAAA,QACpB,OAAO,QAAQ;AAAA,QACf,MAAM,OAAO;AAAA,QACb,MAAM,OAAO;AAAA,MACf;AACA,cAAQ,IAAI;AAAA,QACV,SAAS,aAAa,OAAO,QAAQ,OAAO;AAAA,QAC5C,0BAA0B,mBAAmB,OAAO,QAAQ,aAAa,UAAU;AAAA,MACrF,CAAC;AACD,aAAO,IAAI,OAAO;AAAA,IACpB;AACA,WAAO,UAAU;AACjB,WAAO,aAAa,QAAQ,QAAQ;AAAA,EACtC,UAAE;AACA,iBAAa,QAAQ;AACrB,WAAO,QAAQ;AAAA,EACjB;AACF;AAEA,eAAe,YACb,QACA,SACA,UAC6B;AAC7B,QAAM,cAAc,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,eAAe,CAAC,CAAC;AACpE,QAAM,UAAU,IAAI,MAAwB,OAAO,MAAM;AACzD,MAAI,YAAY;AAChB,QAAM,QAAQ;AAAA,IACZ,MAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,aAAa,OAAO,MAAM,EAAE,GAAG,YAAY;AACvE,aAAO,YAAY,OAAO,QAAQ;AAChC,cAAM,QAAQ;AACd,gBAAQ,KAAK,IAAI,MAAM,SAAS,OAAO,KAAK,GAAG,OAAO;AAAA,MACxD;AAAA,IACF,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAGA,eAAsB,uBACpB,QACA,UAAwD,CAAC,GAC5B;AAC7B,SAAO,YAAY,QAAQ,SAAS,iBAAiB;AACvD;AAGA,eAAsB,uBACpB,QACA,UAAwD,CAAC,GAC5B;AAC7B,SAAO,YAAY,QAAQ,SAAS,iBAAiB;AACvD;","names":[]}
package/dist/node.mjs ADDED
@@ -0,0 +1,233 @@
1
+ import {
2
+ computeCoverPlacement,
3
+ computePrintAreaClip
4
+ } from "./chunk-NINRTPOJ.mjs";
5
+
6
+ // src/node.ts
7
+ import { FabricImage, Rect, StaticCanvas, util } from "fabric/node";
8
+ var ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "data:"]);
9
+ var URL_KEYS = /* @__PURE__ */ new Set(["src", "image"]);
10
+ function defaultAllowImageUrl(url) {
11
+ const scheme = /^([a-z][a-z\d+.-]*):/i.exec(url.trim());
12
+ return scheme !== null && ALLOWED_PROTOCOLS.has(`${scheme[1].toLowerCase()}:`);
13
+ }
14
+ function assertImageUrlsAllowed(value, allow) {
15
+ if (Array.isArray(value)) {
16
+ for (const item of value) assertImageUrlsAllowed(item, allow);
17
+ return;
18
+ }
19
+ if (!value || typeof value !== "object") return;
20
+ for (const [key, entry] of Object.entries(value)) {
21
+ if (typeof entry === "string") {
22
+ if (URL_KEYS.has(key) && !allow(entry)) {
23
+ throw new Error(`Blocked disallowed image URL: ${entry.slice(0, 120)}`);
24
+ }
25
+ } else {
26
+ assertImageUrlsAllowed(entry, allow);
27
+ }
28
+ }
29
+ }
30
+ function validateState(state) {
31
+ if (!state?.canvas || !Array.isArray(state.layers) || !Number.isFinite(state.canvas.width) || !Number.isFinite(state.canvas.height) || state.canvas.width <= 0 || state.canvas.height <= 0) {
32
+ throw new Error("Invalid editor state");
33
+ }
34
+ const major = Number.parseInt(state.version?.split(".")[0] ?? "1", 10);
35
+ if (!Number.isFinite(major) || major > 2) {
36
+ throw new Error(`Unsupported editor state version: ${state.version}`);
37
+ }
38
+ }
39
+ function dataUrlBytes(dataUrl) {
40
+ const encoded = dataUrl.slice(dataUrl.indexOf(",") + 1);
41
+ const binary = atob(encoded);
42
+ const bytes = new Uint8Array(binary.length);
43
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
44
+ return bytes;
45
+ }
46
+ function validateOptions(options) {
47
+ const format = options.format ?? "png";
48
+ const multiplier = options.multiplier ?? 1;
49
+ const quality = options.quality ?? 1;
50
+ const allowImageUrl = options.allowImageUrl ?? defaultAllowImageUrl;
51
+ if (!Number.isFinite(multiplier) || multiplier <= 0) {
52
+ throw new Error("Render multiplier must be positive");
53
+ }
54
+ if (!Number.isFinite(quality) || quality < 0 || quality > 1) {
55
+ throw new Error("Render quality must be between 0 and 1");
56
+ }
57
+ return { format, multiplier, quality, allowImageUrl };
58
+ }
59
+ async function createStateCanvas(state, transparent = false) {
60
+ const canvas = new StaticCanvas(void 0, {
61
+ width: state.canvas.width,
62
+ height: state.canvas.height,
63
+ backgroundColor: transparent ? "" : state.background ?? "",
64
+ preserveObjectStacking: true
65
+ });
66
+ try {
67
+ if (!transparent && state.backgroundImage) {
68
+ canvas.backgroundImage = (await util.enlivenObjects([state.backgroundImage]))[0];
69
+ }
70
+ const objects = await util.enlivenObjects(
71
+ state.layers.map((layer) => layer.fabricObject)
72
+ );
73
+ objects.forEach((object, index) => {
74
+ const layer = state.layers[index];
75
+ object.set({ visible: layer.visible, opacity: layer.opacity });
76
+ canvas.add(object);
77
+ });
78
+ canvas.renderAll();
79
+ return canvas;
80
+ } catch (error) {
81
+ canvas.dispose();
82
+ throw error;
83
+ }
84
+ }
85
+ function encodeCanvas(canvas, options) {
86
+ const { format, multiplier, quality } = options;
87
+ const width = Math.round(canvas.getWidth() * multiplier);
88
+ const height = Math.round(canvas.getHeight() * multiplier);
89
+ if (format === "svg") {
90
+ return { format, mimeType: "image/svg+xml", data: canvas.toSVG(), width, height };
91
+ }
92
+ const dataUrl = canvas.toDataURL({ format, multiplier, quality });
93
+ return {
94
+ format,
95
+ mimeType: format === "jpeg" ? "image/jpeg" : `image/${format}`,
96
+ data: dataUrlBytes(dataUrl),
97
+ width,
98
+ height
99
+ };
100
+ }
101
+ function compositeOperation(mode) {
102
+ return !mode || mode === "normal" ? "source-over" : mode;
103
+ }
104
+ function clampOpacity(value = 1) {
105
+ return Math.max(0, Math.min(1, value));
106
+ }
107
+ async function coverImage(url, width, height) {
108
+ let image;
109
+ try {
110
+ image = await FabricImage.fromURL(url);
111
+ } catch (error) {
112
+ throw new Error(`Failed to load mockup image: ${url}`, { cause: error });
113
+ }
114
+ const sourceWidth = image.width;
115
+ const sourceHeight = image.height;
116
+ const placement = computeCoverPlacement(sourceWidth, sourceHeight, width, height);
117
+ image.set({
118
+ originX: "left",
119
+ originY: "top",
120
+ left: placement.left,
121
+ top: placement.top,
122
+ scaleX: placement.width / sourceWidth,
123
+ scaleY: placement.height / sourceHeight,
124
+ selectable: false,
125
+ evented: false
126
+ });
127
+ return image;
128
+ }
129
+ async function renderEditorState(state, options = {}) {
130
+ validateState(state);
131
+ const resolved = validateOptions(options);
132
+ assertImageUrlsAllowed(state, resolved.allowImageUrl);
133
+ const canvas = await createStateCanvas(state);
134
+ try {
135
+ return encodeCanvas(canvas, resolved);
136
+ } finally {
137
+ canvas.dispose();
138
+ }
139
+ }
140
+ async function renderMockupState(state, options = {}) {
141
+ validateState(state);
142
+ const resolved = validateOptions(options);
143
+ assertImageUrlsAllowed(state, resolved.allowImageUrl);
144
+ if (resolved.format === "svg") {
145
+ throw new Error("Mockup rendering supports PNG, JPEG, and WebP output");
146
+ }
147
+ const mockup = state.mockup;
148
+ if (!mockup?.image) throw new Error("No mockup is configured");
149
+ const designCanvas = await createStateCanvas(state, true);
150
+ const output = new StaticCanvas(void 0, {
151
+ width: state.canvas.width,
152
+ height: state.canvas.height,
153
+ preserveObjectStacking: true
154
+ });
155
+ try {
156
+ output.add(await coverImage(mockup.image, state.canvas.width, state.canvas.height));
157
+ const designUrl = designCanvas.toDataURL({ format: "png", multiplier: 1 });
158
+ const design = await FabricImage.fromURL(designUrl);
159
+ design.set({
160
+ originX: "left",
161
+ originY: "top",
162
+ left: 0,
163
+ top: 0,
164
+ opacity: clampOpacity(mockup.designOpacity),
165
+ globalCompositeOperation: compositeOperation(mockup.designBlendMode),
166
+ selectable: false,
167
+ evented: false
168
+ });
169
+ if (mockup.printArea && mockup.clipToPrintArea !== false) {
170
+ const clip = computePrintAreaClip(
171
+ mockup.printArea,
172
+ 1,
173
+ 1,
174
+ state.canvas.width,
175
+ state.canvas.height
176
+ );
177
+ design.clipPath = new Rect({
178
+ originX: "left",
179
+ originY: "top",
180
+ left: clip.left,
181
+ top: clip.top,
182
+ width: clip.width,
183
+ height: clip.height,
184
+ absolutePositioned: true
185
+ });
186
+ }
187
+ output.add(design);
188
+ if (mockup.overlay) {
189
+ const overlay = await coverImage(
190
+ mockup.overlay.image,
191
+ state.canvas.width,
192
+ state.canvas.height
193
+ );
194
+ overlay.set({
195
+ opacity: clampOpacity(mockup.overlay.opacity),
196
+ globalCompositeOperation: compositeOperation(mockup.overlay.blendMode ?? "multiply")
197
+ });
198
+ output.add(overlay);
199
+ }
200
+ output.renderAll();
201
+ return encodeCanvas(output, resolved);
202
+ } finally {
203
+ designCanvas.dispose();
204
+ output.dispose();
205
+ }
206
+ }
207
+ async function renderBatch(states, options, renderer) {
208
+ const concurrency = Math.max(1, Math.floor(options.concurrency ?? 2));
209
+ const results = new Array(states.length);
210
+ let nextIndex = 0;
211
+ await Promise.all(
212
+ Array.from({ length: Math.min(concurrency, states.length) }, async () => {
213
+ while (nextIndex < states.length) {
214
+ const index = nextIndex++;
215
+ results[index] = await renderer(states[index], options);
216
+ }
217
+ })
218
+ );
219
+ return results;
220
+ }
221
+ async function renderEditorStateBatch(states, options = {}) {
222
+ return renderBatch(states, options, renderEditorState);
223
+ }
224
+ async function renderMockupStateBatch(states, options = {}) {
225
+ return renderBatch(states, options, renderMockupState);
226
+ }
227
+ export {
228
+ renderEditorState,
229
+ renderEditorStateBatch,
230
+ renderMockupState,
231
+ renderMockupStateBatch
232
+ };
233
+ //# sourceMappingURL=node.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/node.ts"],"sourcesContent":["import { FabricImage, Rect, StaticCanvas, util } from 'fabric/node';\nimport type { FabricObject, ImageFormat } from 'fabric/node';\nimport { computeCoverPlacement, computePrintAreaClip } from './export';\nimport type { EditorState, MockupBlendMode, MockupConfig } from './types';\n\nexport interface NodeRenderOptions {\n format?: 'png' | 'jpeg' | 'webp' | 'svg';\n multiplier?: number;\n quality?: number;\n /**\n * Gate every image URL referenced by the state before it is loaded. States\n * are authored in the browser and therefore untrusted on a server; the\n * default policy allows only `http:`, `https:` and `data:`. Supply a\n * narrower predicate (e.g. an origin allowlist) to also stop requests to\n * internal hosts.\n */\n allowImageUrl?: (url: string) => boolean;\n}\n\nexport interface NodeRenderResult {\n format: 'png' | 'jpeg' | 'webp' | 'svg';\n mimeType: string;\n data: Uint8Array | string;\n width: number;\n height: number;\n}\n\n// `fabric/node` resolves bare paths and `file:` URLs against the local disk, so\n// an unrestricted state is a local-file-read and SSRF primitive on a server.\nconst ALLOWED_PROTOCOLS = new Set(['http:', 'https:', 'data:']);\n\n/** Keys whose string values are fetched as images during a render. */\nconst URL_KEYS = new Set(['src', 'image']);\n\nfunction defaultAllowImageUrl(url: string): boolean {\n const scheme = /^([a-z][a-z\\d+.-]*):/i.exec(url.trim());\n return scheme !== null && ALLOWED_PROTOCOLS.has(`${scheme[1].toLowerCase()}:`);\n}\n\nfunction assertImageUrlsAllowed(value: unknown, allow: (url: string) => boolean): void {\n if (Array.isArray(value)) {\n for (const item of value) assertImageUrlsAllowed(item, allow);\n return;\n }\n if (!value || typeof value !== 'object') return;\n for (const [key, entry] of Object.entries(value)) {\n if (typeof entry === 'string') {\n if (URL_KEYS.has(key) && !allow(entry)) {\n throw new Error(`Blocked disallowed image URL: ${entry.slice(0, 120)}`);\n }\n } else {\n assertImageUrlsAllowed(entry, allow);\n }\n }\n}\n\nfunction validateState(state: EditorState): void {\n if (\n !state?.canvas ||\n !Array.isArray(state.layers) ||\n !Number.isFinite(state.canvas.width) ||\n !Number.isFinite(state.canvas.height) ||\n state.canvas.width <= 0 ||\n state.canvas.height <= 0\n ) {\n throw new Error('Invalid editor state');\n }\n const major = Number.parseInt(state.version?.split('.')[0] ?? '1', 10);\n if (!Number.isFinite(major) || major > 2) {\n throw new Error(`Unsupported editor state version: ${state.version}`);\n }\n}\n\nfunction dataUrlBytes(dataUrl: string): Uint8Array {\n const encoded = dataUrl.slice(dataUrl.indexOf(',') + 1);\n const binary = atob(encoded);\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);\n return bytes;\n}\n\nfunction validateOptions(options: NodeRenderOptions): Required<NodeRenderOptions> {\n const format = options.format ?? 'png';\n const multiplier = options.multiplier ?? 1;\n const quality = options.quality ?? 1;\n const allowImageUrl = options.allowImageUrl ?? defaultAllowImageUrl;\n if (!Number.isFinite(multiplier) || multiplier <= 0) {\n throw new Error('Render multiplier must be positive');\n }\n if (!Number.isFinite(quality) || quality < 0 || quality > 1) {\n throw new Error('Render quality must be between 0 and 1');\n }\n return { format, multiplier, quality, allowImageUrl };\n}\n\nasync function createStateCanvas(state: EditorState, transparent = false): Promise<StaticCanvas> {\n const canvas = new StaticCanvas(undefined, {\n width: state.canvas.width,\n height: state.canvas.height,\n backgroundColor: transparent ? '' : (state.background ?? ''),\n preserveObjectStacking: true,\n });\n try {\n if (!transparent && state.backgroundImage) {\n canvas.backgroundImage = (\n await util.enlivenObjects([state.backgroundImage])\n )[0] as FabricObject;\n }\n const objects = (await util.enlivenObjects(\n state.layers.map((layer) => layer.fabricObject),\n )) as FabricObject[];\n objects.forEach((object, index) => {\n const layer = state.layers[index];\n object.set({ visible: layer.visible, opacity: layer.opacity });\n canvas.add(object);\n });\n canvas.renderAll();\n return canvas;\n } catch (error) {\n canvas.dispose();\n throw error;\n }\n}\n\nfunction encodeCanvas(\n canvas: StaticCanvas,\n options: Required<NodeRenderOptions>,\n): NodeRenderResult {\n const { format, multiplier, quality } = options;\n const width = Math.round(canvas.getWidth() * multiplier);\n const height = Math.round(canvas.getHeight() * multiplier);\n if (format === 'svg') {\n return { format, mimeType: 'image/svg+xml', data: canvas.toSVG(), width, height };\n }\n const dataUrl = canvas.toDataURL({ format: format as ImageFormat, multiplier, quality });\n return {\n format,\n mimeType: format === 'jpeg' ? 'image/jpeg' : `image/${format}`,\n data: dataUrlBytes(dataUrl),\n width,\n height,\n };\n}\n\nfunction compositeOperation(mode?: MockupBlendMode): GlobalCompositeOperation {\n return !mode || mode === 'normal' ? 'source-over' : mode;\n}\n\nfunction clampOpacity(value = 1): number {\n return Math.max(0, Math.min(1, value));\n}\n\nasync function coverImage(url: string, width: number, height: number): Promise<FabricImage> {\n let image: FabricImage;\n try {\n image = await FabricImage.fromURL(url);\n } catch (error) {\n throw new Error(`Failed to load mockup image: ${url}`, { cause: error });\n }\n const sourceWidth = image.width;\n const sourceHeight = image.height;\n const placement = computeCoverPlacement(sourceWidth, sourceHeight, width, height);\n image.set({\n originX: 'left',\n originY: 'top',\n left: placement.left,\n top: placement.top,\n scaleX: placement.width / sourceWidth,\n scaleY: placement.height / sourceHeight,\n selectable: false,\n evented: false,\n });\n return image;\n}\n\n/** Render browser-authored EditorState without a DOM or live editor instance. */\nexport async function renderEditorState(\n state: EditorState,\n options: NodeRenderOptions = {},\n): Promise<NodeRenderResult> {\n validateState(state);\n const resolved = validateOptions(options);\n assertImageUrlsAllowed(state, resolved.allowImageUrl);\n const canvas = await createStateCanvas(state);\n try {\n return encodeCanvas(canvas, resolved);\n } finally {\n canvas.dispose();\n }\n}\n\n/** Render a saved product mockup with the transparent design composited above it. */\nexport async function renderMockupState(\n state: EditorState,\n options: NodeRenderOptions = {},\n): Promise<NodeRenderResult> {\n validateState(state);\n const resolved = validateOptions(options);\n assertImageUrlsAllowed(state, resolved.allowImageUrl);\n if (resolved.format === 'svg') {\n throw new Error('Mockup rendering supports PNG, JPEG, and WebP output');\n }\n const mockup: MockupConfig | null | undefined = state.mockup;\n if (!mockup?.image) throw new Error('No mockup is configured');\n\n const designCanvas = await createStateCanvas(state, true);\n const output = new StaticCanvas(undefined, {\n width: state.canvas.width,\n height: state.canvas.height,\n preserveObjectStacking: true,\n });\n try {\n output.add(await coverImage(mockup.image, state.canvas.width, state.canvas.height));\n\n const designUrl = designCanvas.toDataURL({ format: 'png', multiplier: 1 });\n const design = await FabricImage.fromURL(designUrl);\n design.set({\n originX: 'left',\n originY: 'top',\n left: 0,\n top: 0,\n opacity: clampOpacity(mockup.designOpacity),\n globalCompositeOperation: compositeOperation(mockup.designBlendMode),\n selectable: false,\n evented: false,\n });\n if (mockup.printArea && mockup.clipToPrintArea !== false) {\n const clip = computePrintAreaClip(\n mockup.printArea,\n 1,\n 1,\n state.canvas.width,\n state.canvas.height,\n );\n design.clipPath = new Rect({\n originX: 'left',\n originY: 'top',\n left: clip.left,\n top: clip.top,\n width: clip.width,\n height: clip.height,\n absolutePositioned: true,\n });\n }\n output.add(design);\n\n if (mockup.overlay) {\n const overlay = await coverImage(\n mockup.overlay.image,\n state.canvas.width,\n state.canvas.height,\n );\n overlay.set({\n opacity: clampOpacity(mockup.overlay.opacity),\n globalCompositeOperation: compositeOperation(mockup.overlay.blendMode ?? 'multiply'),\n });\n output.add(overlay);\n }\n output.renderAll();\n return encodeCanvas(output, resolved);\n } finally {\n designCanvas.dispose();\n output.dispose();\n }\n}\n\nasync function renderBatch(\n states: EditorState[],\n options: NodeRenderOptions & { concurrency?: number },\n renderer: (state: EditorState, options: NodeRenderOptions) => Promise<NodeRenderResult>,\n): Promise<NodeRenderResult[]> {\n const concurrency = Math.max(1, Math.floor(options.concurrency ?? 2));\n const results = new Array<NodeRenderResult>(states.length);\n let nextIndex = 0;\n await Promise.all(\n Array.from({ length: Math.min(concurrency, states.length) }, async () => {\n while (nextIndex < states.length) {\n const index = nextIndex++;\n results[index] = await renderer(states[index], options);\n }\n }),\n );\n return results;\n}\n\n/** Render multiple states with bounded concurrency for fulfillment jobs. */\nexport async function renderEditorStateBatch(\n states: EditorState[],\n options: NodeRenderOptions & { concurrency?: number } = {},\n): Promise<NodeRenderResult[]> {\n return renderBatch(states, options, renderEditorState);\n}\n\n/** Render multiple mockup states with bounded concurrency for fulfillment jobs. */\nexport async function renderMockupStateBatch(\n states: EditorState[],\n options: NodeRenderOptions & { concurrency?: number } = {},\n): Promise<NodeRenderResult[]> {\n return renderBatch(states, options, renderMockupState);\n}\n"],"mappings":"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","names":[]}