@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.
- package/LICENSE +21 -0
- package/dist/chunk-NINRTPOJ.mjs +104 -0
- package/dist/chunk-NINRTPOJ.mjs.map +1 -0
- package/dist/index.d.mts +144 -225
- package/dist/index.d.ts +144 -225
- package/dist/index.global.js +506 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +1109 -94
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1020 -92
- package/dist/index.mjs.map +1 -1
- package/dist/node.d.mts +37 -0
- package/dist/node.d.ts +37 -0
- package/dist/node.js +281 -0
- package/dist/node.js.map +1 -0
- package/dist/node.mjs +233 -0
- package/dist/node.mjs.map +1 -0
- package/dist/types-bOY7oYVq.d.mts +383 -0
- package/dist/types-bOY7oYVq.d.ts +383 -0
- package/package.json +13 -1
package/dist/node.d.mts
ADDED
|
@@ -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
|
package/dist/node.js.map
ADDED
|
@@ -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":"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","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":[]}
|