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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/node.js CHANGED
@@ -1,8 +1,13 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
6
11
  var __export = (target, all) => {
7
12
  for (var name in all)
8
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -15,18 +20,74 @@ var __copyProps = (to, from, except, desc) => {
15
20
  }
16
21
  return to;
17
22
  };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
+ // If the importer is in node compatibility mode or this is not an ESM
25
+ // file that has been converted to a CommonJS file using a Babel-
26
+ // compatible transform (i.e. "__esModule" has not been set), then set
27
+ // "default" to the CommonJS "module.exports" for node compatibility.
28
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
+ mod
30
+ ));
18
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
32
 
20
- // src/node.ts
21
- var node_exports = {};
22
- __export(node_exports, {
23
- renderEditorState: () => renderEditorState,
24
- renderEditorStateBatch: () => renderEditorStateBatch,
25
- renderMockupState: () => renderMockupState,
26
- renderMockupStateBatch: () => renderMockupStateBatch
33
+ // src/displacement.ts
34
+ function finiteScale(value, fallback, label) {
35
+ const resolved = value ?? fallback;
36
+ if (!Number.isFinite(resolved)) throw new Error(`${label} must be finite`);
37
+ return resolved;
38
+ }
39
+ function sample(source, width, height, x, y, channel) {
40
+ const clampedX = Math.max(0, Math.min(width - 1, x));
41
+ const clampedY = Math.max(0, Math.min(height - 1, y));
42
+ const x0 = Math.floor(clampedX);
43
+ const y0 = Math.floor(clampedY);
44
+ const x1 = Math.min(width - 1, x0 + 1);
45
+ const y1 = Math.min(height - 1, y0 + 1);
46
+ const tx = clampedX - x0;
47
+ const ty = clampedY - y0;
48
+ const top = source[(y0 * width + x0) * 4 + channel] * (1 - tx) + source[(y0 * width + x1) * 4 + channel] * tx;
49
+ const bottom = source[(y1 * width + x0) * 4 + channel] * (1 - tx) + source[(y1 * width + x1) * 4 + channel] * tx;
50
+ return top * (1 - ty) + bottom * ty;
51
+ }
52
+ function displaceRgba(source, map, width, height, options) {
53
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {
54
+ throw new Error("Displacement dimensions must be positive integers");
55
+ }
56
+ const expectedLength = width * height * 4;
57
+ if (source.length !== expectedLength || map.length !== expectedLength) {
58
+ throw new Error("Displacement source and map must match the requested dimensions");
59
+ }
60
+ const scaleX = finiteScale(options.scaleX, 10, "Displacement scaleX");
61
+ const scaleY = finiteScale(options.scaleY, 10, "Displacement scaleY");
62
+ const channelX = CHANNEL_INDEX[options.channelX ?? "red"];
63
+ const channelY = CHANNEL_INDEX[options.channelY ?? "green"];
64
+ const output = new Uint8ClampedArray(expectedLength);
65
+ for (let y = 0; y < height; y += 1) {
66
+ for (let x = 0; x < width; x += 1) {
67
+ const offset = (y * width + x) * 4;
68
+ const sourceX = x + (map[offset + channelX] - 128) / 127 * scaleX;
69
+ const sourceY = y + (map[offset + channelY] - 128) / 127 * scaleY;
70
+ for (let channel = 0; channel < 4; channel += 1) {
71
+ output[offset + channel] = Math.round(
72
+ sample(source, width, height, sourceX, sourceY, channel)
73
+ );
74
+ }
75
+ }
76
+ }
77
+ return output;
78
+ }
79
+ var CHANNEL_INDEX;
80
+ var init_displacement = __esm({
81
+ "src/displacement.ts"() {
82
+ "use strict";
83
+ CHANNEL_INDEX = {
84
+ red: 0,
85
+ green: 1,
86
+ blue: 2,
87
+ alpha: 3
88
+ };
89
+ }
27
90
  });
28
- module.exports = __toCommonJS(node_exports);
29
- var import_node = require("fabric/node");
30
91
 
31
92
  // src/export.ts
32
93
  function computePrintAreaClip(area, scaleX, scaleY, targetWidth, targetHeight) {
@@ -50,10 +111,440 @@ function computeCoverPlacement(sourceWidth, sourceHeight, targetWidth, targetHei
50
111
  height
51
112
  };
52
113
  }
114
+ var import_fabric;
115
+ var init_export = __esm({
116
+ "src/export.ts"() {
117
+ "use strict";
118
+ import_fabric = require("fabric");
119
+ init_displacement();
120
+ }
121
+ });
122
+
123
+ // src/print.ts
124
+ function svgAttributes(source) {
125
+ return Object.fromEntries(
126
+ [...source.matchAll(/([\w:-]+)=(?:"([^"]*)"|'([^']*)')/g)].map((match) => [
127
+ match[1],
128
+ match[2] ?? match[3] ?? ""
129
+ ])
130
+ );
131
+ }
132
+ function decodeXmlText(source) {
133
+ return source.replace(/<[^>]+>/g, "").replace(
134
+ /&#x([\da-f]+);/gi,
135
+ (_, value) => String.fromCodePoint(Number.parseInt(value, 16))
136
+ ).replace(/&#(\d+);/g, (_, value) => String.fromCodePoint(Number(value))).replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&");
137
+ }
138
+ function escapeXmlAttribute(value) {
139
+ return value.replace(/[<>&"']/g, (character) => `&#${character.charCodeAt(0)};`);
140
+ }
141
+ function fontSource(files, family, bold, italic) {
142
+ const suffix = bold && italic ? "-BoldItalic" : bold ? "-Bold" : italic ? "-Italic" : "";
143
+ const own = (key) => Object.hasOwn(files, key) ? files[key] : void 0;
144
+ return own(`${family}${suffix}`) ?? own(family);
145
+ }
146
+ async function outlineSvgText(svg, files, outlined) {
147
+ const fontkit = await import("fontkit");
148
+ let output = "";
149
+ let cursor = 0;
150
+ for (const textMatch of svg.matchAll(/<text\b([^>]*)>([\s\S]*?)<\/text>/gi)) {
151
+ const index = textMatch.index ?? 0;
152
+ output += svg.slice(cursor, index);
153
+ cursor = index + textMatch[0].length;
154
+ const textAttributes = svgAttributes(textMatch[1]);
155
+ const spans = [...textMatch[2].matchAll(/<tspan\b([^>]*)>([\s\S]*?)<\/tspan>/gi)];
156
+ const lines = spans.length ? spans.map((span) => ({ attributes: svgAttributes(span[1]), text: decodeXmlText(span[2]) })) : [{ attributes: textAttributes, text: decodeXmlText(textMatch[2]) }];
157
+ const resolved = lines.map((line) => {
158
+ const attributes = { ...textAttributes, ...line.attributes };
159
+ const family = attributes["font-family"] ?? "sans-serif";
160
+ const bold = /bold|[6-9]00/i.test(attributes["font-weight"] ?? "");
161
+ const italic = /italic|oblique/i.test(attributes["font-style"] ?? "");
162
+ return { ...line, attributes, family, source: fontSource(files, family, bold, italic) };
163
+ });
164
+ if (resolved.some((line) => !line.source)) {
165
+ output += textMatch[0];
166
+ continue;
167
+ }
168
+ const paths = [];
169
+ for (const line of resolved) {
170
+ const source = line.source;
171
+ const opened = typeof source === "string" ? fontkit.openSync(source) : fontkit.create(Buffer.from(source.buffer, source.byteOffset, source.byteLength));
172
+ if (!("layout" in opened)) {
173
+ throw new Error(`Font collection requires a named face: ${line.family}`);
174
+ }
175
+ const size = Number.parseFloat(line.attributes["font-size"] ?? "16");
176
+ const scale = size / opened.unitsPerEm;
177
+ const style = line.attributes.style ?? "";
178
+ const run = opened.layout(line.text);
179
+ let penX = Number.parseFloat(line.attributes.x ?? "0");
180
+ const baseline = Number.parseFloat(line.attributes.y ?? "0");
181
+ run.glyphs.forEach((glyph, glyphIndex) => {
182
+ const position = run.positions[glyphIndex];
183
+ const x = penX + position.xOffset * scale;
184
+ const y = baseline - position.yOffset * scale;
185
+ paths.push(
186
+ `<path d="${glyph.path.toSVG()}" transform="translate(${x} ${y}) scale(${scale} ${-scale})" style="${escapeXmlAttribute(style)}"/>`
187
+ );
188
+ penX += position.xAdvance * scale;
189
+ });
190
+ outlined.add(line.family);
191
+ }
192
+ output += `<g data-outlined-font="${escapeXmlAttribute([...new Set(resolved.map((line) => line.family))].join(","))}">${paths.join("")}</g>`;
193
+ }
194
+ return output + svg.slice(cursor);
195
+ }
196
+ function rgbToCmyk([redByte, greenByte, blueByte]) {
197
+ const red = redByte / 255;
198
+ const green = greenByte / 255;
199
+ const blue = blueByte / 255;
200
+ const black = 1 - Math.max(red, green, blue);
201
+ if (black >= 1) return [0, 0, 0, 100];
202
+ return [
203
+ (1 - red - black) / (1 - black) * 100,
204
+ (1 - green - black) / (1 - black) * 100,
205
+ (1 - blue - black) / (1 - black) * 100,
206
+ black * 100
207
+ ];
208
+ }
209
+ async function imageSourceBytes(source, allow) {
210
+ if (source.startsWith("data:")) {
211
+ const response2 = await fetch(source);
212
+ if (!response2.ok) throw new Error("Failed to decode an embedded SVG image");
213
+ return new Uint8Array(await response2.arrayBuffer());
214
+ }
215
+ const permitted = allow ? allow(source) : (() => {
216
+ try {
217
+ return ["http:", "https:"].includes(new URL(source).protocol);
218
+ } catch {
219
+ return false;
220
+ }
221
+ })();
222
+ if (!permitted) throw new Error(`Blocked disallowed vector image URL: ${source}`);
223
+ const response = await fetch(source);
224
+ if (!response.ok) throw new Error(`Failed to load vector image: ${source}`);
225
+ return new Uint8Array(await response.arrayBuffer());
226
+ }
227
+ async function convertSvgImagesToCmyk(svg, sharp, profile, allow) {
228
+ const sources = /* @__PURE__ */ new Set();
229
+ for (const image of svg.matchAll(/<image\b[^>]*(?:xlink:href|href)="([^"]+)"[^>]*>/gi)) {
230
+ if (image[1]) sources.add(image[1]);
231
+ }
232
+ let converted = svg;
233
+ for (const source of sources) {
234
+ const bytes = await imageSourceBytes(source, allow);
235
+ const jpeg = await sharp(bytes).flatten({ background: "#ffffff" }).toColourspace("cmyk").withIccProfile(profile).jpeg({ quality: 100, chromaSubsampling: "4:4:4" }).toBuffer();
236
+ const dataUrl = `data:image/jpeg;base64,${jpeg.toString("base64")}`;
237
+ converted = converted.split(source).join(dataUrl);
238
+ }
239
+ return converted;
240
+ }
241
+ async function renderVectorOverlay(state, options, width, height, sharp, warnings, outlinedFonts, unoutlinedFonts) {
242
+ const [{ default: PDFKit }, { default: SVGtoPDF }, { renderEditorState: renderEditorState2 }] = await Promise.all([
243
+ import("pdfkit"),
244
+ import("svg-to-pdfkit"),
245
+ Promise.resolve().then(() => (init_node(), node_exports))
246
+ ]);
247
+ const rendered = await renderEditorState2(
248
+ { ...state, background: "#ffffff" },
249
+ {
250
+ format: "svg",
251
+ allowImageUrl: options.allowImageUrl
252
+ }
253
+ );
254
+ if (typeof rendered.data !== "string") throw new Error("Vector rendering returned raster data");
255
+ const fontFiles = options.fontFiles ?? {};
256
+ const outlinedSvg = options.outlineFonts === false ? rendered.data : await outlineSvgText(rendered.data, fontFiles, outlinedFonts);
257
+ const svg = await convertSvgImagesToCmyk(
258
+ outlinedSvg,
259
+ sharp,
260
+ options.iccProfile ?? "cmyk",
261
+ options.allowImageUrl
262
+ );
263
+ const remainingText = [...svg.matchAll(/<text\b([^>]*)>/gi)].map(
264
+ (match) => svgAttributes(match[1])
265
+ );
266
+ for (const attributes of remainingText) {
267
+ const family = attributes["font-family"] ?? "sans-serif";
268
+ unoutlinedFonts.add(family);
269
+ const bold = /bold|[6-9]00/i.test(attributes["font-weight"] ?? "");
270
+ const italic = /italic|oblique/i.test(attributes["font-style"] ?? "");
271
+ if (!fontSource(fontFiles, family, bold, italic)) {
272
+ warnings.push(
273
+ `Font "${family}" used a PDF standard fallback; supply fontFiles for exact embedding`
274
+ );
275
+ }
276
+ }
277
+ const document = new PDFKit({ autoFirstPage: false, compress: false, pdfVersion: "1.7" });
278
+ for (const [name, path] of Object.entries(fontFiles)) {
279
+ document.registerFont(name, typeof path === "string" ? path : Buffer.from(path));
280
+ }
281
+ document.addPage({ size: [width, height], margin: 0 });
282
+ SVGtoPDF(document, svg, 0, 0, {
283
+ width,
284
+ height,
285
+ preserveAspectRatio: "none",
286
+ colorCallback: (color) => {
287
+ const [rgb, opacity] = color;
288
+ return [rgbToCmyk(rgb), opacity];
289
+ },
290
+ warningCallback: (warning) => warnings.push(`Vector render: ${warning}`)
291
+ });
292
+ return new Promise((resolve, reject) => {
293
+ const chunks = [];
294
+ document.on("data", (chunk) => chunks.push(chunk));
295
+ document.on("error", reject);
296
+ document.on("end", () => resolve(new Uint8Array(Buffer.concat(chunks))));
297
+ document.end();
298
+ });
299
+ }
300
+ function positive(value, label) {
301
+ if (!Number.isFinite(value) || value <= 0) throw new Error(`${label} must be positive`);
302
+ return value;
303
+ }
304
+ function boundedDpi(value, label) {
305
+ const dpi = positive(value, label);
306
+ if (dpi > MAX_DPI) throw new Error(`${label} must not exceed ${MAX_DPI}`);
307
+ if (dpi < 1) throw new Error(`${label} must be at least 1`);
308
+ return dpi;
309
+ }
310
+ function nonNegative(value, label) {
311
+ if (!Number.isFinite(value) || value < 0) throw new Error(`${label} cannot be negative`);
312
+ return value;
313
+ }
314
+ function preflightSafeArea(state, safePixels) {
315
+ if (safePixels <= 0) return [];
316
+ const right = state.canvas.width - safePixels;
317
+ const bottom = state.canvas.height - safePixels;
318
+ const warnings = [];
319
+ for (const layer of state.layers) {
320
+ if (!layer.visible) continue;
321
+ const object = layer.fabricObject;
322
+ const left = Number(object.left ?? 0);
323
+ const top = Number(object.top ?? 0);
324
+ const width = Number(object.width ?? 0) * Math.abs(Number(object.scaleX ?? 1));
325
+ const height = Number(object.height ?? 0) * Math.abs(Number(object.scaleY ?? 1));
326
+ if (left < safePixels || top < safePixels || left + width > right || top + height > bottom) {
327
+ warnings.push(`Layer "${layer.name}" extends outside the configured safe area`);
328
+ }
329
+ }
330
+ return warnings;
331
+ }
332
+ async function renderPrintPdf(state, options = {}) {
333
+ const dpi = boundedDpi(options.dpi ?? state.canvas.dpi ?? 300, "Print DPI");
334
+ const documentDpi = boundedDpi(state.canvas.dpi ?? 72, "Document DPI");
335
+ const maxPixels = positive(options.maxPixels ?? DEFAULT_MAX_PIXELS, "Max pixels");
336
+ const bleedInches = nonNegative(options.bleed ?? 0.125, "Bleed");
337
+ const marksMarginInches = nonNegative(options.marksMargin ?? 0.25, "Marks margin");
338
+ const safeAreaInches = nonNegative(options.safeArea ?? 0, "Safe area");
339
+ const rendering = options.rendering ?? "vector";
340
+ const bleedPixels = Math.round(bleedInches * dpi);
341
+ const trimWidthPoints = state.canvas.width / documentDpi * 72;
342
+ const trimHeightPoints = state.canvas.height / documentDpi * 72;
343
+ const bleedPoints = bleedInches * 72;
344
+ const marksMarginPoints = marksMarginInches * 72;
345
+ const scale = dpi / documentDpi;
346
+ const outputPixels = Math.round(state.canvas.width * scale + bleedPixels * 2) * Math.round(state.canvas.height * scale + bleedPixels * 2);
347
+ if (!Number.isFinite(outputPixels) || outputPixels > maxPixels) {
348
+ throw new Error(
349
+ `Print raster of ${outputPixels} pixels exceeds the ${maxPixels} pixel budget; lower the DPI or raise maxPixels`
350
+ );
351
+ }
352
+ const [{ default: sharp }, pdfLib] = await Promise.all([import("sharp"), import("pdf-lib")]);
353
+ const { renderEditorState: renderEditorState2 } = await Promise.resolve().then(() => (init_node(), node_exports));
354
+ const rendered = await renderEditorState2(state, {
355
+ format: "png",
356
+ multiplier: scale,
357
+ allowImageUrl: options.allowImageUrl
358
+ });
359
+ if (typeof rendered.data === "string") throw new Error("Print rasterization returned SVG data");
360
+ let pipeline = sharp(rendered.data).flatten({ background: "#ffffff" });
361
+ if (bleedPixels > 0) {
362
+ pipeline = pipeline.extend({
363
+ top: bleedPixels,
364
+ right: bleedPixels,
365
+ bottom: bleedPixels,
366
+ left: bleedPixels,
367
+ extendWith: "copy"
368
+ });
369
+ }
370
+ const { data: cmykJpeg, info } = await pipeline.toColourspace("cmyk").withIccProfile(options.iccProfile ?? "cmyk").withDensity(dpi).jpeg({ quality: 100, chromaSubsampling: "4:4:4" }).toBuffer({ resolveWithObject: true });
371
+ const metadata = await sharp(cmykJpeg).metadata();
372
+ if (info.channels !== 4 || metadata.space !== "cmyk" || !metadata.icc) {
373
+ throw new Error("CMYK conversion did not produce a four-channel image with an ICC profile");
374
+ }
375
+ const { PDFDocument, PDFDict, PDFName, PDFString, cmyk } = pdfLib;
376
+ const document = await PDFDocument.create();
377
+ const title = options.title ?? "Overtone Canvas Editor print export";
378
+ document.setTitle(title);
379
+ document.setCreator("@overtone-art/canvas-editor-core");
380
+ document.setProducer("@overtone-art/canvas-editor-core");
381
+ const pageWidth = trimWidthPoints + bleedPoints * 2 + marksMarginPoints * 2;
382
+ const pageHeight = trimHeightPoints + bleedPoints * 2 + marksMarginPoints * 2;
383
+ const page = document.addPage([pageWidth, pageHeight]);
384
+ const image = await document.embedJpg(cmykJpeg);
385
+ page.drawImage(image, {
386
+ x: marksMarginPoints,
387
+ y: marksMarginPoints,
388
+ width: trimWidthPoints + bleedPoints * 2,
389
+ height: trimHeightPoints + bleedPoints * 2
390
+ });
391
+ const trimLeft = marksMarginPoints + bleedPoints;
392
+ const trimBottom = marksMarginPoints + bleedPoints;
393
+ const trimRight = trimLeft + trimWidthPoints;
394
+ const trimTop = trimBottom + trimHeightPoints;
395
+ const warnings = preflightSafeArea(state, safeAreaInches * documentDpi);
396
+ const outlinedFonts = /* @__PURE__ */ new Set();
397
+ const unoutlinedFonts = /* @__PURE__ */ new Set();
398
+ if (rendering === "vector") {
399
+ const vectorPdf = await renderVectorOverlay(
400
+ state,
401
+ options,
402
+ trimWidthPoints,
403
+ trimHeightPoints,
404
+ sharp,
405
+ warnings,
406
+ outlinedFonts,
407
+ unoutlinedFonts
408
+ );
409
+ const [vectorPage] = await document.embedPdf(vectorPdf);
410
+ page.drawPage(vectorPage, {
411
+ x: trimLeft,
412
+ y: trimBottom,
413
+ width: trimWidthPoints,
414
+ height: trimHeightPoints
415
+ });
416
+ }
417
+ const bleedLeft = marksMarginPoints;
418
+ const bleedBottom = marksMarginPoints;
419
+ const bleedRight = pageWidth - marksMarginPoints;
420
+ const bleedTop = pageHeight - marksMarginPoints;
421
+ const context = document.context;
422
+ page.node.set(PDFName.of("TrimBox"), context.obj([trimLeft, trimBottom, trimRight, trimTop]));
423
+ page.node.set(
424
+ PDFName.of("BleedBox"),
425
+ context.obj([bleedLeft, bleedBottom, bleedRight, bleedTop])
426
+ );
427
+ page.node.set(PDFName.of("ArtBox"), context.obj([trimLeft, trimBottom, trimRight, trimTop]));
428
+ const markColour = cmyk(0, 0, 0, 1);
429
+ if (options.trimMarks !== false) {
430
+ const offset = Math.max(3, bleedPoints / 2);
431
+ const length = Math.max(9, marksMarginPoints - 3);
432
+ for (const x of [trimLeft, trimRight]) {
433
+ page.drawLine({
434
+ start: { x, y: trimBottom - offset },
435
+ end: { x, y: trimBottom - offset - length },
436
+ thickness: 0.5,
437
+ color: markColour
438
+ });
439
+ page.drawLine({
440
+ start: { x, y: trimTop + offset },
441
+ end: { x, y: trimTop + offset + length },
442
+ thickness: 0.5,
443
+ color: markColour
444
+ });
445
+ }
446
+ for (const y of [trimBottom, trimTop]) {
447
+ page.drawLine({
448
+ start: { x: trimLeft - offset, y },
449
+ end: { x: trimLeft - offset - length, y },
450
+ thickness: 0.5,
451
+ color: markColour
452
+ });
453
+ page.drawLine({
454
+ start: { x: trimRight + offset, y },
455
+ end: { x: trimRight + offset + length, y },
456
+ thickness: 0.5,
457
+ color: markColour
458
+ });
459
+ }
460
+ }
461
+ if (options.registrationMarks !== false) {
462
+ for (const [x, y] of [
463
+ [pageWidth / 2, marksMarginPoints / 2],
464
+ [pageWidth / 2, pageHeight - marksMarginPoints / 2],
465
+ [marksMarginPoints / 2, pageHeight / 2],
466
+ [pageWidth - marksMarginPoints / 2, pageHeight / 2]
467
+ ]) {
468
+ page.drawCircle({ x, y, size: 4, borderWidth: 0.5, borderColor: markColour });
469
+ page.drawLine({
470
+ start: { x: x - 6, y },
471
+ end: { x: x + 6, y },
472
+ thickness: 0.5,
473
+ color: markColour
474
+ });
475
+ page.drawLine({
476
+ start: { x, y: y - 6 },
477
+ end: { x, y: y + 6 },
478
+ thickness: 0.5,
479
+ color: markColour
480
+ });
481
+ }
482
+ }
483
+ const profileStream = context.flateStream(metadata.icc, {
484
+ N: 4,
485
+ Alternate: PDFName.of("DeviceCMYK")
486
+ });
487
+ const profileRef = context.register(profileStream);
488
+ const outputIntent = context.obj({
489
+ Type: PDFName.of("OutputIntent"),
490
+ S: PDFName.of("GTS_PDFX"),
491
+ OutputConditionIdentifier: PDFString.of(options.outputConditionIdentifier ?? "CMYK"),
492
+ RegistryName: PDFString.of("https://www.color.org"),
493
+ Info: PDFString.of(options.outputConditionIdentifier ?? "CMYK print condition"),
494
+ DestOutputProfile: profileRef
495
+ });
496
+ document.catalog.set(PDFName.of("OutputIntents"), context.obj([context.register(outputIntent)]));
497
+ const now = (/* @__PURE__ */ new Date()).toISOString();
498
+ const xmp = `<?xpacket begin="\uFEFF" id="W5M0MpCehiHzreSzNTczkc9d"?>
499
+ <x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
500
+ <rdf:Description rdf:about="" xmlns:pdfxid="http://www.npes.org/pdfx/ns/id/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:xmp="http://ns.adobe.com/xap/1.0/" pdfxid:GTS_PDFXVersion="PDF/X-4" xmp:CreateDate="${now}"><dc:title><rdf:Alt><rdf:li xml:lang="x-default">${title.replace(/[<>&]/g, "")}</rdf:li></rdf:Alt></dc:title></rdf:Description>
501
+ </rdf:RDF></x:xmpmeta><?xpacket end="w"?>`;
502
+ const metadataStream = context.flateStream(new TextEncoder().encode(xmp), {
503
+ Type: PDFName.of("Metadata"),
504
+ Subtype: PDFName.of("XML")
505
+ });
506
+ document.catalog.set(PDFName.of("Metadata"), context.register(metadataStream));
507
+ const infoRef = context.trailerInfo.Info;
508
+ if (infoRef) {
509
+ const infoDict = context.lookup(infoRef, PDFDict);
510
+ infoDict.set(PDFName.of("GTS_PDFXVersion"), PDFString.of("PDF/X-4"));
511
+ infoDict.set(PDFName.of("Trapped"), PDFName.of("False"));
512
+ }
513
+ return {
514
+ format: "pdf",
515
+ mimeType: "application/pdf",
516
+ data: await document.save({ useObjectStreams: false }),
517
+ standard: "PDF/X-4",
518
+ colourSpace: "CMYK",
519
+ rendering,
520
+ outlinedFonts: [...outlinedFonts].sort(),
521
+ unoutlinedFonts: [...unoutlinedFonts].sort(),
522
+ dpi,
523
+ trimWidthPoints,
524
+ trimHeightPoints,
525
+ bleedPoints,
526
+ warnings
527
+ };
528
+ }
529
+ var MAX_DPI, DEFAULT_MAX_PIXELS;
530
+ var init_print = __esm({
531
+ "src/print.ts"() {
532
+ "use strict";
533
+ MAX_DPI = 2400;
534
+ DEFAULT_MAX_PIXELS = 25e7;
535
+ }
536
+ });
53
537
 
54
538
  // src/node.ts
55
- var ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "data:"]);
56
- var URL_KEYS = /* @__PURE__ */ new Set(["src", "image"]);
539
+ var node_exports = {};
540
+ __export(node_exports, {
541
+ renderEditorState: () => renderEditorState,
542
+ renderEditorStateBatch: () => renderEditorStateBatch,
543
+ renderMockupState: () => renderMockupState,
544
+ renderMockupStateBatch: () => renderMockupStateBatch,
545
+ renderPrintPdf: () => renderPrintPdf
546
+ });
547
+ module.exports = __toCommonJS(node_exports);
57
548
  function defaultAllowImageUrl(url) {
58
549
  const scheme = /^([a-z][a-z\d+.-]*):/i.exec(url.trim());
59
550
  return scheme !== null && ALLOWED_PROTOCOLS.has(`${scheme[1].toLowerCase()}:`);
@@ -173,6 +664,30 @@ async function coverImage(url, width, height) {
173
664
  });
174
665
  return image;
175
666
  }
667
+ async function displacedDesignUrl(designCanvas, displacement, width, height) {
668
+ const mapCanvas = new import_node.StaticCanvas(void 0, { width, height });
669
+ const warpedCanvas = new import_node.StaticCanvas(void 0, { width, height });
670
+ try {
671
+ mapCanvas.add(await coverImage(displacement.image, width, height));
672
+ mapCanvas.renderAll();
673
+ const pixels = displaceRgba(
674
+ designCanvas.getContext().getImageData(0, 0, width, height).data,
675
+ mapCanvas.getContext().getImageData(0, 0, width, height).data,
676
+ width,
677
+ height,
678
+ displacement
679
+ );
680
+ const imageData = warpedCanvas.getContext().createImageData(width, height);
681
+ imageData.data.set(pixels);
682
+ warpedCanvas.getContext().putImageData(imageData, 0, 0);
683
+ return warpedCanvas.toDataURL({ format: "png", multiplier: 1 });
684
+ } catch (error) {
685
+ throw new Error("Failed to apply mockup displacement map", { cause: error });
686
+ } finally {
687
+ mapCanvas.dispose();
688
+ warpedCanvas.dispose();
689
+ }
690
+ }
176
691
  async function renderEditorState(state, options = {}) {
177
692
  validateState(state);
178
693
  const resolved = validateOptions(options);
@@ -201,7 +716,12 @@ async function renderMockupState(state, options = {}) {
201
716
  });
202
717
  try {
203
718
  output.add(await coverImage(mockup.image, state.canvas.width, state.canvas.height));
204
- const designUrl = designCanvas.toDataURL({ format: "png", multiplier: 1 });
719
+ const designUrl = mockup.displacement ? await displacedDesignUrl(
720
+ designCanvas,
721
+ mockup.displacement,
722
+ state.canvas.width,
723
+ state.canvas.height
724
+ ) : designCanvas.toDataURL({ format: "png", multiplier: 1 });
205
725
  const design = await import_node.FabricImage.fromURL(designUrl);
206
726
  design.set({
207
727
  originX: "left",
@@ -271,11 +791,24 @@ async function renderEditorStateBatch(states, options = {}) {
271
791
  async function renderMockupStateBatch(states, options = {}) {
272
792
  return renderBatch(states, options, renderMockupState);
273
793
  }
794
+ var import_node, ALLOWED_PROTOCOLS, URL_KEYS;
795
+ var init_node = __esm({
796
+ "src/node.ts"() {
797
+ import_node = require("fabric/node");
798
+ init_export();
799
+ init_displacement();
800
+ init_print();
801
+ ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "data:"]);
802
+ URL_KEYS = /* @__PURE__ */ new Set(["src", "image"]);
803
+ }
804
+ });
805
+ init_node();
274
806
  // Annotate the CommonJS export names for ESM import in node:
275
807
  0 && (module.exports = {
276
808
  renderEditorState,
277
809
  renderEditorStateBatch,
278
810
  renderMockupState,
279
- renderMockupStateBatch
811
+ renderMockupStateBatch,
812
+ renderPrintPdf
280
813
  });
281
814
  //# sourceMappingURL=node.js.map