@overtone-art/canvas-editor-core 0.2.6 → 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/LICENSE +21 -0
- package/dist/chunk-ORZZ6MGQ.mjs +228 -0
- package/dist/chunk-ORZZ6MGQ.mjs.map +1 -0
- package/dist/index.d.mts +297 -225
- package/dist/index.d.ts +297 -225
- package/dist/index.global.js +506 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +1719 -99
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1501 -94
- package/dist/index.mjs.map +1 -1
- package/dist/node.d.mts +86 -0
- package/dist/node.d.ts +86 -0
- package/dist/node.js +814 -0
- package/dist/node.js.map +1 -0
- package/dist/node.mjs +675 -0
- package/dist/node.mjs.map +1 -0
- package/dist/types-D60CfxL9.d.mts +461 -0
- package/dist/types-D60CfxL9.d.ts +461 -0
- package/package.json +49 -2
package/dist/node.mjs
ADDED
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computeCoverPlacement,
|
|
3
|
+
computePrintAreaClip,
|
|
4
|
+
displaceRgba
|
|
5
|
+
} from "./chunk-ORZZ6MGQ.mjs";
|
|
6
|
+
|
|
7
|
+
// src/node.ts
|
|
8
|
+
import { FabricImage, Rect, StaticCanvas, util } from "fabric/node";
|
|
9
|
+
|
|
10
|
+
// src/print.ts
|
|
11
|
+
function svgAttributes(source) {
|
|
12
|
+
return Object.fromEntries(
|
|
13
|
+
[...source.matchAll(/([\w:-]+)=(?:"([^"]*)"|'([^']*)')/g)].map((match) => [
|
|
14
|
+
match[1],
|
|
15
|
+
match[2] ?? match[3] ?? ""
|
|
16
|
+
])
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
function decodeXmlText(source) {
|
|
20
|
+
return source.replace(/<[^>]+>/g, "").replace(
|
|
21
|
+
/&#x([\da-f]+);/gi,
|
|
22
|
+
(_, value) => String.fromCodePoint(Number.parseInt(value, 16))
|
|
23
|
+
).replace(/&#(\d+);/g, (_, value) => String.fromCodePoint(Number(value))).replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&");
|
|
24
|
+
}
|
|
25
|
+
function escapeXmlAttribute(value) {
|
|
26
|
+
return value.replace(/[<>&"']/g, (character) => `&#${character.charCodeAt(0)};`);
|
|
27
|
+
}
|
|
28
|
+
function fontSource(files, family, bold, italic) {
|
|
29
|
+
const suffix = bold && italic ? "-BoldItalic" : bold ? "-Bold" : italic ? "-Italic" : "";
|
|
30
|
+
const own = (key) => Object.hasOwn(files, key) ? files[key] : void 0;
|
|
31
|
+
return own(`${family}${suffix}`) ?? own(family);
|
|
32
|
+
}
|
|
33
|
+
async function outlineSvgText(svg, files, outlined) {
|
|
34
|
+
const fontkit = await import("fontkit");
|
|
35
|
+
let output = "";
|
|
36
|
+
let cursor = 0;
|
|
37
|
+
for (const textMatch of svg.matchAll(/<text\b([^>]*)>([\s\S]*?)<\/text>/gi)) {
|
|
38
|
+
const index = textMatch.index ?? 0;
|
|
39
|
+
output += svg.slice(cursor, index);
|
|
40
|
+
cursor = index + textMatch[0].length;
|
|
41
|
+
const textAttributes = svgAttributes(textMatch[1]);
|
|
42
|
+
const spans = [...textMatch[2].matchAll(/<tspan\b([^>]*)>([\s\S]*?)<\/tspan>/gi)];
|
|
43
|
+
const lines = spans.length ? spans.map((span) => ({ attributes: svgAttributes(span[1]), text: decodeXmlText(span[2]) })) : [{ attributes: textAttributes, text: decodeXmlText(textMatch[2]) }];
|
|
44
|
+
const resolved = lines.map((line) => {
|
|
45
|
+
const attributes = { ...textAttributes, ...line.attributes };
|
|
46
|
+
const family = attributes["font-family"] ?? "sans-serif";
|
|
47
|
+
const bold = /bold|[6-9]00/i.test(attributes["font-weight"] ?? "");
|
|
48
|
+
const italic = /italic|oblique/i.test(attributes["font-style"] ?? "");
|
|
49
|
+
return { ...line, attributes, family, source: fontSource(files, family, bold, italic) };
|
|
50
|
+
});
|
|
51
|
+
if (resolved.some((line) => !line.source)) {
|
|
52
|
+
output += textMatch[0];
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const paths = [];
|
|
56
|
+
for (const line of resolved) {
|
|
57
|
+
const source = line.source;
|
|
58
|
+
const opened = typeof source === "string" ? fontkit.openSync(source) : fontkit.create(Buffer.from(source.buffer, source.byteOffset, source.byteLength));
|
|
59
|
+
if (!("layout" in opened)) {
|
|
60
|
+
throw new Error(`Font collection requires a named face: ${line.family}`);
|
|
61
|
+
}
|
|
62
|
+
const size = Number.parseFloat(line.attributes["font-size"] ?? "16");
|
|
63
|
+
const scale = size / opened.unitsPerEm;
|
|
64
|
+
const style = line.attributes.style ?? "";
|
|
65
|
+
const run = opened.layout(line.text);
|
|
66
|
+
let penX = Number.parseFloat(line.attributes.x ?? "0");
|
|
67
|
+
const baseline = Number.parseFloat(line.attributes.y ?? "0");
|
|
68
|
+
run.glyphs.forEach((glyph, glyphIndex) => {
|
|
69
|
+
const position = run.positions[glyphIndex];
|
|
70
|
+
const x = penX + position.xOffset * scale;
|
|
71
|
+
const y = baseline - position.yOffset * scale;
|
|
72
|
+
paths.push(
|
|
73
|
+
`<path d="${glyph.path.toSVG()}" transform="translate(${x} ${y}) scale(${scale} ${-scale})" style="${escapeXmlAttribute(style)}"/>`
|
|
74
|
+
);
|
|
75
|
+
penX += position.xAdvance * scale;
|
|
76
|
+
});
|
|
77
|
+
outlined.add(line.family);
|
|
78
|
+
}
|
|
79
|
+
output += `<g data-outlined-font="${escapeXmlAttribute([...new Set(resolved.map((line) => line.family))].join(","))}">${paths.join("")}</g>`;
|
|
80
|
+
}
|
|
81
|
+
return output + svg.slice(cursor);
|
|
82
|
+
}
|
|
83
|
+
function rgbToCmyk([redByte, greenByte, blueByte]) {
|
|
84
|
+
const red = redByte / 255;
|
|
85
|
+
const green = greenByte / 255;
|
|
86
|
+
const blue = blueByte / 255;
|
|
87
|
+
const black = 1 - Math.max(red, green, blue);
|
|
88
|
+
if (black >= 1) return [0, 0, 0, 100];
|
|
89
|
+
return [
|
|
90
|
+
(1 - red - black) / (1 - black) * 100,
|
|
91
|
+
(1 - green - black) / (1 - black) * 100,
|
|
92
|
+
(1 - blue - black) / (1 - black) * 100,
|
|
93
|
+
black * 100
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
async function imageSourceBytes(source, allow) {
|
|
97
|
+
if (source.startsWith("data:")) {
|
|
98
|
+
const response2 = await fetch(source);
|
|
99
|
+
if (!response2.ok) throw new Error("Failed to decode an embedded SVG image");
|
|
100
|
+
return new Uint8Array(await response2.arrayBuffer());
|
|
101
|
+
}
|
|
102
|
+
const permitted = allow ? allow(source) : (() => {
|
|
103
|
+
try {
|
|
104
|
+
return ["http:", "https:"].includes(new URL(source).protocol);
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
})();
|
|
109
|
+
if (!permitted) throw new Error(`Blocked disallowed vector image URL: ${source}`);
|
|
110
|
+
const response = await fetch(source);
|
|
111
|
+
if (!response.ok) throw new Error(`Failed to load vector image: ${source}`);
|
|
112
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
113
|
+
}
|
|
114
|
+
async function convertSvgImagesToCmyk(svg, sharp, profile, allow) {
|
|
115
|
+
const sources = /* @__PURE__ */ new Set();
|
|
116
|
+
for (const image of svg.matchAll(/<image\b[^>]*(?:xlink:href|href)="([^"]+)"[^>]*>/gi)) {
|
|
117
|
+
if (image[1]) sources.add(image[1]);
|
|
118
|
+
}
|
|
119
|
+
let converted = svg;
|
|
120
|
+
for (const source of sources) {
|
|
121
|
+
const bytes = await imageSourceBytes(source, allow);
|
|
122
|
+
const jpeg = await sharp(bytes).flatten({ background: "#ffffff" }).toColourspace("cmyk").withIccProfile(profile).jpeg({ quality: 100, chromaSubsampling: "4:4:4" }).toBuffer();
|
|
123
|
+
const dataUrl = `data:image/jpeg;base64,${jpeg.toString("base64")}`;
|
|
124
|
+
converted = converted.split(source).join(dataUrl);
|
|
125
|
+
}
|
|
126
|
+
return converted;
|
|
127
|
+
}
|
|
128
|
+
async function renderVectorOverlay(state, options, width, height, sharp, warnings, outlinedFonts, unoutlinedFonts) {
|
|
129
|
+
const [{ default: PDFKit }, { default: SVGtoPDF }, { renderEditorState: renderEditorState2 }] = await Promise.all([
|
|
130
|
+
import("pdfkit"),
|
|
131
|
+
import("svg-to-pdfkit"),
|
|
132
|
+
import("./node.mjs")
|
|
133
|
+
]);
|
|
134
|
+
const rendered = await renderEditorState2(
|
|
135
|
+
{ ...state, background: "#ffffff" },
|
|
136
|
+
{
|
|
137
|
+
format: "svg",
|
|
138
|
+
allowImageUrl: options.allowImageUrl
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
if (typeof rendered.data !== "string") throw new Error("Vector rendering returned raster data");
|
|
142
|
+
const fontFiles = options.fontFiles ?? {};
|
|
143
|
+
const outlinedSvg = options.outlineFonts === false ? rendered.data : await outlineSvgText(rendered.data, fontFiles, outlinedFonts);
|
|
144
|
+
const svg = await convertSvgImagesToCmyk(
|
|
145
|
+
outlinedSvg,
|
|
146
|
+
sharp,
|
|
147
|
+
options.iccProfile ?? "cmyk",
|
|
148
|
+
options.allowImageUrl
|
|
149
|
+
);
|
|
150
|
+
const remainingText = [...svg.matchAll(/<text\b([^>]*)>/gi)].map(
|
|
151
|
+
(match) => svgAttributes(match[1])
|
|
152
|
+
);
|
|
153
|
+
for (const attributes of remainingText) {
|
|
154
|
+
const family = attributes["font-family"] ?? "sans-serif";
|
|
155
|
+
unoutlinedFonts.add(family);
|
|
156
|
+
const bold = /bold|[6-9]00/i.test(attributes["font-weight"] ?? "");
|
|
157
|
+
const italic = /italic|oblique/i.test(attributes["font-style"] ?? "");
|
|
158
|
+
if (!fontSource(fontFiles, family, bold, italic)) {
|
|
159
|
+
warnings.push(
|
|
160
|
+
`Font "${family}" used a PDF standard fallback; supply fontFiles for exact embedding`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const document = new PDFKit({ autoFirstPage: false, compress: false, pdfVersion: "1.7" });
|
|
165
|
+
for (const [name, path] of Object.entries(fontFiles)) {
|
|
166
|
+
document.registerFont(name, typeof path === "string" ? path : Buffer.from(path));
|
|
167
|
+
}
|
|
168
|
+
document.addPage({ size: [width, height], margin: 0 });
|
|
169
|
+
SVGtoPDF(document, svg, 0, 0, {
|
|
170
|
+
width,
|
|
171
|
+
height,
|
|
172
|
+
preserveAspectRatio: "none",
|
|
173
|
+
colorCallback: (color) => {
|
|
174
|
+
const [rgb, opacity] = color;
|
|
175
|
+
return [rgbToCmyk(rgb), opacity];
|
|
176
|
+
},
|
|
177
|
+
warningCallback: (warning) => warnings.push(`Vector render: ${warning}`)
|
|
178
|
+
});
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
const chunks = [];
|
|
181
|
+
document.on("data", (chunk) => chunks.push(chunk));
|
|
182
|
+
document.on("error", reject);
|
|
183
|
+
document.on("end", () => resolve(new Uint8Array(Buffer.concat(chunks))));
|
|
184
|
+
document.end();
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
var MAX_DPI = 2400;
|
|
188
|
+
var DEFAULT_MAX_PIXELS = 25e7;
|
|
189
|
+
function positive(value, label) {
|
|
190
|
+
if (!Number.isFinite(value) || value <= 0) throw new Error(`${label} must be positive`);
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
function boundedDpi(value, label) {
|
|
194
|
+
const dpi = positive(value, label);
|
|
195
|
+
if (dpi > MAX_DPI) throw new Error(`${label} must not exceed ${MAX_DPI}`);
|
|
196
|
+
if (dpi < 1) throw new Error(`${label} must be at least 1`);
|
|
197
|
+
return dpi;
|
|
198
|
+
}
|
|
199
|
+
function nonNegative(value, label) {
|
|
200
|
+
if (!Number.isFinite(value) || value < 0) throw new Error(`${label} cannot be negative`);
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
203
|
+
function preflightSafeArea(state, safePixels) {
|
|
204
|
+
if (safePixels <= 0) return [];
|
|
205
|
+
const right = state.canvas.width - safePixels;
|
|
206
|
+
const bottom = state.canvas.height - safePixels;
|
|
207
|
+
const warnings = [];
|
|
208
|
+
for (const layer of state.layers) {
|
|
209
|
+
if (!layer.visible) continue;
|
|
210
|
+
const object = layer.fabricObject;
|
|
211
|
+
const left = Number(object.left ?? 0);
|
|
212
|
+
const top = Number(object.top ?? 0);
|
|
213
|
+
const width = Number(object.width ?? 0) * Math.abs(Number(object.scaleX ?? 1));
|
|
214
|
+
const height = Number(object.height ?? 0) * Math.abs(Number(object.scaleY ?? 1));
|
|
215
|
+
if (left < safePixels || top < safePixels || left + width > right || top + height > bottom) {
|
|
216
|
+
warnings.push(`Layer "${layer.name}" extends outside the configured safe area`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return warnings;
|
|
220
|
+
}
|
|
221
|
+
async function renderPrintPdf(state, options = {}) {
|
|
222
|
+
const dpi = boundedDpi(options.dpi ?? state.canvas.dpi ?? 300, "Print DPI");
|
|
223
|
+
const documentDpi = boundedDpi(state.canvas.dpi ?? 72, "Document DPI");
|
|
224
|
+
const maxPixels = positive(options.maxPixels ?? DEFAULT_MAX_PIXELS, "Max pixels");
|
|
225
|
+
const bleedInches = nonNegative(options.bleed ?? 0.125, "Bleed");
|
|
226
|
+
const marksMarginInches = nonNegative(options.marksMargin ?? 0.25, "Marks margin");
|
|
227
|
+
const safeAreaInches = nonNegative(options.safeArea ?? 0, "Safe area");
|
|
228
|
+
const rendering = options.rendering ?? "vector";
|
|
229
|
+
const bleedPixels = Math.round(bleedInches * dpi);
|
|
230
|
+
const trimWidthPoints = state.canvas.width / documentDpi * 72;
|
|
231
|
+
const trimHeightPoints = state.canvas.height / documentDpi * 72;
|
|
232
|
+
const bleedPoints = bleedInches * 72;
|
|
233
|
+
const marksMarginPoints = marksMarginInches * 72;
|
|
234
|
+
const scale = dpi / documentDpi;
|
|
235
|
+
const outputPixels = Math.round(state.canvas.width * scale + bleedPixels * 2) * Math.round(state.canvas.height * scale + bleedPixels * 2);
|
|
236
|
+
if (!Number.isFinite(outputPixels) || outputPixels > maxPixels) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`Print raster of ${outputPixels} pixels exceeds the ${maxPixels} pixel budget; lower the DPI or raise maxPixels`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
const [{ default: sharp }, pdfLib] = await Promise.all([import("sharp"), import("pdf-lib")]);
|
|
242
|
+
const { renderEditorState: renderEditorState2 } = await import("./node.mjs");
|
|
243
|
+
const rendered = await renderEditorState2(state, {
|
|
244
|
+
format: "png",
|
|
245
|
+
multiplier: scale,
|
|
246
|
+
allowImageUrl: options.allowImageUrl
|
|
247
|
+
});
|
|
248
|
+
if (typeof rendered.data === "string") throw new Error("Print rasterization returned SVG data");
|
|
249
|
+
let pipeline = sharp(rendered.data).flatten({ background: "#ffffff" });
|
|
250
|
+
if (bleedPixels > 0) {
|
|
251
|
+
pipeline = pipeline.extend({
|
|
252
|
+
top: bleedPixels,
|
|
253
|
+
right: bleedPixels,
|
|
254
|
+
bottom: bleedPixels,
|
|
255
|
+
left: bleedPixels,
|
|
256
|
+
extendWith: "copy"
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
const { data: cmykJpeg, info } = await pipeline.toColourspace("cmyk").withIccProfile(options.iccProfile ?? "cmyk").withDensity(dpi).jpeg({ quality: 100, chromaSubsampling: "4:4:4" }).toBuffer({ resolveWithObject: true });
|
|
260
|
+
const metadata = await sharp(cmykJpeg).metadata();
|
|
261
|
+
if (info.channels !== 4 || metadata.space !== "cmyk" || !metadata.icc) {
|
|
262
|
+
throw new Error("CMYK conversion did not produce a four-channel image with an ICC profile");
|
|
263
|
+
}
|
|
264
|
+
const { PDFDocument, PDFDict, PDFName, PDFString, cmyk } = pdfLib;
|
|
265
|
+
const document = await PDFDocument.create();
|
|
266
|
+
const title = options.title ?? "Overtone Canvas Editor print export";
|
|
267
|
+
document.setTitle(title);
|
|
268
|
+
document.setCreator("@overtone-art/canvas-editor-core");
|
|
269
|
+
document.setProducer("@overtone-art/canvas-editor-core");
|
|
270
|
+
const pageWidth = trimWidthPoints + bleedPoints * 2 + marksMarginPoints * 2;
|
|
271
|
+
const pageHeight = trimHeightPoints + bleedPoints * 2 + marksMarginPoints * 2;
|
|
272
|
+
const page = document.addPage([pageWidth, pageHeight]);
|
|
273
|
+
const image = await document.embedJpg(cmykJpeg);
|
|
274
|
+
page.drawImage(image, {
|
|
275
|
+
x: marksMarginPoints,
|
|
276
|
+
y: marksMarginPoints,
|
|
277
|
+
width: trimWidthPoints + bleedPoints * 2,
|
|
278
|
+
height: trimHeightPoints + bleedPoints * 2
|
|
279
|
+
});
|
|
280
|
+
const trimLeft = marksMarginPoints + bleedPoints;
|
|
281
|
+
const trimBottom = marksMarginPoints + bleedPoints;
|
|
282
|
+
const trimRight = trimLeft + trimWidthPoints;
|
|
283
|
+
const trimTop = trimBottom + trimHeightPoints;
|
|
284
|
+
const warnings = preflightSafeArea(state, safeAreaInches * documentDpi);
|
|
285
|
+
const outlinedFonts = /* @__PURE__ */ new Set();
|
|
286
|
+
const unoutlinedFonts = /* @__PURE__ */ new Set();
|
|
287
|
+
if (rendering === "vector") {
|
|
288
|
+
const vectorPdf = await renderVectorOverlay(
|
|
289
|
+
state,
|
|
290
|
+
options,
|
|
291
|
+
trimWidthPoints,
|
|
292
|
+
trimHeightPoints,
|
|
293
|
+
sharp,
|
|
294
|
+
warnings,
|
|
295
|
+
outlinedFonts,
|
|
296
|
+
unoutlinedFonts
|
|
297
|
+
);
|
|
298
|
+
const [vectorPage] = await document.embedPdf(vectorPdf);
|
|
299
|
+
page.drawPage(vectorPage, {
|
|
300
|
+
x: trimLeft,
|
|
301
|
+
y: trimBottom,
|
|
302
|
+
width: trimWidthPoints,
|
|
303
|
+
height: trimHeightPoints
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
const bleedLeft = marksMarginPoints;
|
|
307
|
+
const bleedBottom = marksMarginPoints;
|
|
308
|
+
const bleedRight = pageWidth - marksMarginPoints;
|
|
309
|
+
const bleedTop = pageHeight - marksMarginPoints;
|
|
310
|
+
const context = document.context;
|
|
311
|
+
page.node.set(PDFName.of("TrimBox"), context.obj([trimLeft, trimBottom, trimRight, trimTop]));
|
|
312
|
+
page.node.set(
|
|
313
|
+
PDFName.of("BleedBox"),
|
|
314
|
+
context.obj([bleedLeft, bleedBottom, bleedRight, bleedTop])
|
|
315
|
+
);
|
|
316
|
+
page.node.set(PDFName.of("ArtBox"), context.obj([trimLeft, trimBottom, trimRight, trimTop]));
|
|
317
|
+
const markColour = cmyk(0, 0, 0, 1);
|
|
318
|
+
if (options.trimMarks !== false) {
|
|
319
|
+
const offset = Math.max(3, bleedPoints / 2);
|
|
320
|
+
const length = Math.max(9, marksMarginPoints - 3);
|
|
321
|
+
for (const x of [trimLeft, trimRight]) {
|
|
322
|
+
page.drawLine({
|
|
323
|
+
start: { x, y: trimBottom - offset },
|
|
324
|
+
end: { x, y: trimBottom - offset - length },
|
|
325
|
+
thickness: 0.5,
|
|
326
|
+
color: markColour
|
|
327
|
+
});
|
|
328
|
+
page.drawLine({
|
|
329
|
+
start: { x, y: trimTop + offset },
|
|
330
|
+
end: { x, y: trimTop + offset + length },
|
|
331
|
+
thickness: 0.5,
|
|
332
|
+
color: markColour
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
for (const y of [trimBottom, trimTop]) {
|
|
336
|
+
page.drawLine({
|
|
337
|
+
start: { x: trimLeft - offset, y },
|
|
338
|
+
end: { x: trimLeft - offset - length, y },
|
|
339
|
+
thickness: 0.5,
|
|
340
|
+
color: markColour
|
|
341
|
+
});
|
|
342
|
+
page.drawLine({
|
|
343
|
+
start: { x: trimRight + offset, y },
|
|
344
|
+
end: { x: trimRight + offset + length, y },
|
|
345
|
+
thickness: 0.5,
|
|
346
|
+
color: markColour
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (options.registrationMarks !== false) {
|
|
351
|
+
for (const [x, y] of [
|
|
352
|
+
[pageWidth / 2, marksMarginPoints / 2],
|
|
353
|
+
[pageWidth / 2, pageHeight - marksMarginPoints / 2],
|
|
354
|
+
[marksMarginPoints / 2, pageHeight / 2],
|
|
355
|
+
[pageWidth - marksMarginPoints / 2, pageHeight / 2]
|
|
356
|
+
]) {
|
|
357
|
+
page.drawCircle({ x, y, size: 4, borderWidth: 0.5, borderColor: markColour });
|
|
358
|
+
page.drawLine({
|
|
359
|
+
start: { x: x - 6, y },
|
|
360
|
+
end: { x: x + 6, y },
|
|
361
|
+
thickness: 0.5,
|
|
362
|
+
color: markColour
|
|
363
|
+
});
|
|
364
|
+
page.drawLine({
|
|
365
|
+
start: { x, y: y - 6 },
|
|
366
|
+
end: { x, y: y + 6 },
|
|
367
|
+
thickness: 0.5,
|
|
368
|
+
color: markColour
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const profileStream = context.flateStream(metadata.icc, {
|
|
373
|
+
N: 4,
|
|
374
|
+
Alternate: PDFName.of("DeviceCMYK")
|
|
375
|
+
});
|
|
376
|
+
const profileRef = context.register(profileStream);
|
|
377
|
+
const outputIntent = context.obj({
|
|
378
|
+
Type: PDFName.of("OutputIntent"),
|
|
379
|
+
S: PDFName.of("GTS_PDFX"),
|
|
380
|
+
OutputConditionIdentifier: PDFString.of(options.outputConditionIdentifier ?? "CMYK"),
|
|
381
|
+
RegistryName: PDFString.of("https://www.color.org"),
|
|
382
|
+
Info: PDFString.of(options.outputConditionIdentifier ?? "CMYK print condition"),
|
|
383
|
+
DestOutputProfile: profileRef
|
|
384
|
+
});
|
|
385
|
+
document.catalog.set(PDFName.of("OutputIntents"), context.obj([context.register(outputIntent)]));
|
|
386
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
387
|
+
const xmp = `<?xpacket begin="\uFEFF" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
|
388
|
+
<x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
|
389
|
+
<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>
|
|
390
|
+
</rdf:RDF></x:xmpmeta><?xpacket end="w"?>`;
|
|
391
|
+
const metadataStream = context.flateStream(new TextEncoder().encode(xmp), {
|
|
392
|
+
Type: PDFName.of("Metadata"),
|
|
393
|
+
Subtype: PDFName.of("XML")
|
|
394
|
+
});
|
|
395
|
+
document.catalog.set(PDFName.of("Metadata"), context.register(metadataStream));
|
|
396
|
+
const infoRef = context.trailerInfo.Info;
|
|
397
|
+
if (infoRef) {
|
|
398
|
+
const infoDict = context.lookup(infoRef, PDFDict);
|
|
399
|
+
infoDict.set(PDFName.of("GTS_PDFXVersion"), PDFString.of("PDF/X-4"));
|
|
400
|
+
infoDict.set(PDFName.of("Trapped"), PDFName.of("False"));
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
format: "pdf",
|
|
404
|
+
mimeType: "application/pdf",
|
|
405
|
+
data: await document.save({ useObjectStreams: false }),
|
|
406
|
+
standard: "PDF/X-4",
|
|
407
|
+
colourSpace: "CMYK",
|
|
408
|
+
rendering,
|
|
409
|
+
outlinedFonts: [...outlinedFonts].sort(),
|
|
410
|
+
unoutlinedFonts: [...unoutlinedFonts].sort(),
|
|
411
|
+
dpi,
|
|
412
|
+
trimWidthPoints,
|
|
413
|
+
trimHeightPoints,
|
|
414
|
+
bleedPoints,
|
|
415
|
+
warnings
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/node.ts
|
|
420
|
+
var ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "data:"]);
|
|
421
|
+
var URL_KEYS = /* @__PURE__ */ new Set(["src", "image"]);
|
|
422
|
+
function defaultAllowImageUrl(url) {
|
|
423
|
+
const scheme = /^([a-z][a-z\d+.-]*):/i.exec(url.trim());
|
|
424
|
+
return scheme !== null && ALLOWED_PROTOCOLS.has(`${scheme[1].toLowerCase()}:`);
|
|
425
|
+
}
|
|
426
|
+
function assertImageUrlsAllowed(value, allow) {
|
|
427
|
+
if (Array.isArray(value)) {
|
|
428
|
+
for (const item of value) assertImageUrlsAllowed(item, allow);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (!value || typeof value !== "object") return;
|
|
432
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
433
|
+
if (typeof entry === "string") {
|
|
434
|
+
if (URL_KEYS.has(key) && !allow(entry)) {
|
|
435
|
+
throw new Error(`Blocked disallowed image URL: ${entry.slice(0, 120)}`);
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
assertImageUrlsAllowed(entry, allow);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function validateState(state) {
|
|
443
|
+
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) {
|
|
444
|
+
throw new Error("Invalid editor state");
|
|
445
|
+
}
|
|
446
|
+
const major = Number.parseInt(state.version?.split(".")[0] ?? "1", 10);
|
|
447
|
+
if (!Number.isFinite(major) || major > 2) {
|
|
448
|
+
throw new Error(`Unsupported editor state version: ${state.version}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function dataUrlBytes(dataUrl) {
|
|
452
|
+
const encoded = dataUrl.slice(dataUrl.indexOf(",") + 1);
|
|
453
|
+
const binary = atob(encoded);
|
|
454
|
+
const bytes = new Uint8Array(binary.length);
|
|
455
|
+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
456
|
+
return bytes;
|
|
457
|
+
}
|
|
458
|
+
function validateOptions(options) {
|
|
459
|
+
const format = options.format ?? "png";
|
|
460
|
+
const multiplier = options.multiplier ?? 1;
|
|
461
|
+
const quality = options.quality ?? 1;
|
|
462
|
+
const allowImageUrl = options.allowImageUrl ?? defaultAllowImageUrl;
|
|
463
|
+
if (!Number.isFinite(multiplier) || multiplier <= 0) {
|
|
464
|
+
throw new Error("Render multiplier must be positive");
|
|
465
|
+
}
|
|
466
|
+
if (!Number.isFinite(quality) || quality < 0 || quality > 1) {
|
|
467
|
+
throw new Error("Render quality must be between 0 and 1");
|
|
468
|
+
}
|
|
469
|
+
return { format, multiplier, quality, allowImageUrl };
|
|
470
|
+
}
|
|
471
|
+
async function createStateCanvas(state, transparent = false) {
|
|
472
|
+
const canvas = new StaticCanvas(void 0, {
|
|
473
|
+
width: state.canvas.width,
|
|
474
|
+
height: state.canvas.height,
|
|
475
|
+
backgroundColor: transparent ? "" : state.background ?? "",
|
|
476
|
+
preserveObjectStacking: true
|
|
477
|
+
});
|
|
478
|
+
try {
|
|
479
|
+
if (!transparent && state.backgroundImage) {
|
|
480
|
+
canvas.backgroundImage = (await util.enlivenObjects([state.backgroundImage]))[0];
|
|
481
|
+
}
|
|
482
|
+
const objects = await util.enlivenObjects(
|
|
483
|
+
state.layers.map((layer) => layer.fabricObject)
|
|
484
|
+
);
|
|
485
|
+
objects.forEach((object, index) => {
|
|
486
|
+
const layer = state.layers[index];
|
|
487
|
+
object.set({ visible: layer.visible, opacity: layer.opacity });
|
|
488
|
+
canvas.add(object);
|
|
489
|
+
});
|
|
490
|
+
canvas.renderAll();
|
|
491
|
+
return canvas;
|
|
492
|
+
} catch (error) {
|
|
493
|
+
canvas.dispose();
|
|
494
|
+
throw error;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function encodeCanvas(canvas, options) {
|
|
498
|
+
const { format, multiplier, quality } = options;
|
|
499
|
+
const width = Math.round(canvas.getWidth() * multiplier);
|
|
500
|
+
const height = Math.round(canvas.getHeight() * multiplier);
|
|
501
|
+
if (format === "svg") {
|
|
502
|
+
return { format, mimeType: "image/svg+xml", data: canvas.toSVG(), width, height };
|
|
503
|
+
}
|
|
504
|
+
const dataUrl = canvas.toDataURL({ format, multiplier, quality });
|
|
505
|
+
return {
|
|
506
|
+
format,
|
|
507
|
+
mimeType: format === "jpeg" ? "image/jpeg" : `image/${format}`,
|
|
508
|
+
data: dataUrlBytes(dataUrl),
|
|
509
|
+
width,
|
|
510
|
+
height
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
function compositeOperation(mode) {
|
|
514
|
+
return !mode || mode === "normal" ? "source-over" : mode;
|
|
515
|
+
}
|
|
516
|
+
function clampOpacity(value = 1) {
|
|
517
|
+
return Math.max(0, Math.min(1, value));
|
|
518
|
+
}
|
|
519
|
+
async function coverImage(url, width, height) {
|
|
520
|
+
let image;
|
|
521
|
+
try {
|
|
522
|
+
image = await FabricImage.fromURL(url);
|
|
523
|
+
} catch (error) {
|
|
524
|
+
throw new Error(`Failed to load mockup image: ${url}`, { cause: error });
|
|
525
|
+
}
|
|
526
|
+
const sourceWidth = image.width;
|
|
527
|
+
const sourceHeight = image.height;
|
|
528
|
+
const placement = computeCoverPlacement(sourceWidth, sourceHeight, width, height);
|
|
529
|
+
image.set({
|
|
530
|
+
originX: "left",
|
|
531
|
+
originY: "top",
|
|
532
|
+
left: placement.left,
|
|
533
|
+
top: placement.top,
|
|
534
|
+
scaleX: placement.width / sourceWidth,
|
|
535
|
+
scaleY: placement.height / sourceHeight,
|
|
536
|
+
selectable: false,
|
|
537
|
+
evented: false
|
|
538
|
+
});
|
|
539
|
+
return image;
|
|
540
|
+
}
|
|
541
|
+
async function displacedDesignUrl(designCanvas, displacement, width, height) {
|
|
542
|
+
const mapCanvas = new StaticCanvas(void 0, { width, height });
|
|
543
|
+
const warpedCanvas = new StaticCanvas(void 0, { width, height });
|
|
544
|
+
try {
|
|
545
|
+
mapCanvas.add(await coverImage(displacement.image, width, height));
|
|
546
|
+
mapCanvas.renderAll();
|
|
547
|
+
const pixels = displaceRgba(
|
|
548
|
+
designCanvas.getContext().getImageData(0, 0, width, height).data,
|
|
549
|
+
mapCanvas.getContext().getImageData(0, 0, width, height).data,
|
|
550
|
+
width,
|
|
551
|
+
height,
|
|
552
|
+
displacement
|
|
553
|
+
);
|
|
554
|
+
const imageData = warpedCanvas.getContext().createImageData(width, height);
|
|
555
|
+
imageData.data.set(pixels);
|
|
556
|
+
warpedCanvas.getContext().putImageData(imageData, 0, 0);
|
|
557
|
+
return warpedCanvas.toDataURL({ format: "png", multiplier: 1 });
|
|
558
|
+
} catch (error) {
|
|
559
|
+
throw new Error("Failed to apply mockup displacement map", { cause: error });
|
|
560
|
+
} finally {
|
|
561
|
+
mapCanvas.dispose();
|
|
562
|
+
warpedCanvas.dispose();
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
async function renderEditorState(state, options = {}) {
|
|
566
|
+
validateState(state);
|
|
567
|
+
const resolved = validateOptions(options);
|
|
568
|
+
assertImageUrlsAllowed(state, resolved.allowImageUrl);
|
|
569
|
+
const canvas = await createStateCanvas(state);
|
|
570
|
+
try {
|
|
571
|
+
return encodeCanvas(canvas, resolved);
|
|
572
|
+
} finally {
|
|
573
|
+
canvas.dispose();
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
async function renderMockupState(state, options = {}) {
|
|
577
|
+
validateState(state);
|
|
578
|
+
const resolved = validateOptions(options);
|
|
579
|
+
assertImageUrlsAllowed(state, resolved.allowImageUrl);
|
|
580
|
+
if (resolved.format === "svg") {
|
|
581
|
+
throw new Error("Mockup rendering supports PNG, JPEG, and WebP output");
|
|
582
|
+
}
|
|
583
|
+
const mockup = state.mockup;
|
|
584
|
+
if (!mockup?.image) throw new Error("No mockup is configured");
|
|
585
|
+
const designCanvas = await createStateCanvas(state, true);
|
|
586
|
+
const output = new StaticCanvas(void 0, {
|
|
587
|
+
width: state.canvas.width,
|
|
588
|
+
height: state.canvas.height,
|
|
589
|
+
preserveObjectStacking: true
|
|
590
|
+
});
|
|
591
|
+
try {
|
|
592
|
+
output.add(await coverImage(mockup.image, state.canvas.width, state.canvas.height));
|
|
593
|
+
const designUrl = mockup.displacement ? await displacedDesignUrl(
|
|
594
|
+
designCanvas,
|
|
595
|
+
mockup.displacement,
|
|
596
|
+
state.canvas.width,
|
|
597
|
+
state.canvas.height
|
|
598
|
+
) : designCanvas.toDataURL({ format: "png", multiplier: 1 });
|
|
599
|
+
const design = await FabricImage.fromURL(designUrl);
|
|
600
|
+
design.set({
|
|
601
|
+
originX: "left",
|
|
602
|
+
originY: "top",
|
|
603
|
+
left: 0,
|
|
604
|
+
top: 0,
|
|
605
|
+
opacity: clampOpacity(mockup.designOpacity),
|
|
606
|
+
globalCompositeOperation: compositeOperation(mockup.designBlendMode),
|
|
607
|
+
selectable: false,
|
|
608
|
+
evented: false
|
|
609
|
+
});
|
|
610
|
+
if (mockup.printArea && mockup.clipToPrintArea !== false) {
|
|
611
|
+
const clip = computePrintAreaClip(
|
|
612
|
+
mockup.printArea,
|
|
613
|
+
1,
|
|
614
|
+
1,
|
|
615
|
+
state.canvas.width,
|
|
616
|
+
state.canvas.height
|
|
617
|
+
);
|
|
618
|
+
design.clipPath = new Rect({
|
|
619
|
+
originX: "left",
|
|
620
|
+
originY: "top",
|
|
621
|
+
left: clip.left,
|
|
622
|
+
top: clip.top,
|
|
623
|
+
width: clip.width,
|
|
624
|
+
height: clip.height,
|
|
625
|
+
absolutePositioned: true
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
output.add(design);
|
|
629
|
+
if (mockup.overlay) {
|
|
630
|
+
const overlay = await coverImage(
|
|
631
|
+
mockup.overlay.image,
|
|
632
|
+
state.canvas.width,
|
|
633
|
+
state.canvas.height
|
|
634
|
+
);
|
|
635
|
+
overlay.set({
|
|
636
|
+
opacity: clampOpacity(mockup.overlay.opacity),
|
|
637
|
+
globalCompositeOperation: compositeOperation(mockup.overlay.blendMode ?? "multiply")
|
|
638
|
+
});
|
|
639
|
+
output.add(overlay);
|
|
640
|
+
}
|
|
641
|
+
output.renderAll();
|
|
642
|
+
return encodeCanvas(output, resolved);
|
|
643
|
+
} finally {
|
|
644
|
+
designCanvas.dispose();
|
|
645
|
+
output.dispose();
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
async function renderBatch(states, options, renderer) {
|
|
649
|
+
const concurrency = Math.max(1, Math.floor(options.concurrency ?? 2));
|
|
650
|
+
const results = new Array(states.length);
|
|
651
|
+
let nextIndex = 0;
|
|
652
|
+
await Promise.all(
|
|
653
|
+
Array.from({ length: Math.min(concurrency, states.length) }, async () => {
|
|
654
|
+
while (nextIndex < states.length) {
|
|
655
|
+
const index = nextIndex++;
|
|
656
|
+
results[index] = await renderer(states[index], options);
|
|
657
|
+
}
|
|
658
|
+
})
|
|
659
|
+
);
|
|
660
|
+
return results;
|
|
661
|
+
}
|
|
662
|
+
async function renderEditorStateBatch(states, options = {}) {
|
|
663
|
+
return renderBatch(states, options, renderEditorState);
|
|
664
|
+
}
|
|
665
|
+
async function renderMockupStateBatch(states, options = {}) {
|
|
666
|
+
return renderBatch(states, options, renderMockupState);
|
|
667
|
+
}
|
|
668
|
+
export {
|
|
669
|
+
renderEditorState,
|
|
670
|
+
renderEditorStateBatch,
|
|
671
|
+
renderMockupState,
|
|
672
|
+
renderMockupStateBatch,
|
|
673
|
+
renderPrintPdf
|
|
674
|
+
};
|
|
675
|
+
//# sourceMappingURL=node.mjs.map
|