@overtone-art/canvas-editor-core 0.8.0 → 0.8.2
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/{chunk-XNGPX7FG.mjs → chunk-TP527WVQ.mjs} +5 -3
- package/dist/{chunk-XNGPX7FG.mjs.map → chunk-TP527WVQ.mjs.map} +1 -1
- package/dist/index.global.js +4 -4
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3 -2
- package/dist/index.mjs.map +1 -1
- package/dist/node.js.map +1 -1
- package/dist/node.mjs +1 -1
- package/package.json +1 -1
package/dist/node.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/displacement.ts","../src/export.ts","../src/text-fit.ts","../src/text-wrap-split.ts","../src/masks/space.ts","../src/text-wrap.ts","../src/print.ts","../src/node.ts"],"sourcesContent":["import type { MockupDisplacement, MockupDisplacementChannel } from './types';\n\nconst CHANNEL_INDEX: Record<MockupDisplacementChannel, number> = {\n red: 0,\n green: 1,\n blue: 2,\n alpha: 3,\n};\n\nfunction finiteScale(value: number | undefined, fallback: number, label: string): number {\n const resolved = value ?? fallback;\n if (!Number.isFinite(resolved)) throw new Error(`${label} must be finite`);\n return resolved;\n}\n\nfunction sample(\n source: Uint8ClampedArray,\n width: number,\n height: number,\n x: number,\n y: number,\n channel: number,\n): number {\n const clampedX = Math.max(0, Math.min(width - 1, x));\n const clampedY = Math.max(0, Math.min(height - 1, y));\n const x0 = Math.floor(clampedX);\n const y0 = Math.floor(clampedY);\n const x1 = Math.min(width - 1, x0 + 1);\n const y1 = Math.min(height - 1, y0 + 1);\n const tx = clampedX - x0;\n const ty = clampedY - y0;\n const top =\n source[(y0 * width + x0) * 4 + channel] * (1 - tx) +\n source[(y0 * width + x1) * 4 + channel] * tx;\n const bottom =\n source[(y1 * width + x0) * 4 + channel] * (1 - tx) +\n source[(y1 * width + x1) * 4 + channel] * tx;\n return top * (1 - ty) + bottom * ty;\n}\n\n/**\n * Warp RGBA pixels with an equally sized channel map. A channel value of 128\n * is neutral; 0 and 255 move by the configured negative/positive maximum.\n */\nexport function displaceRgba(\n source: Uint8ClampedArray,\n map: Uint8ClampedArray,\n width: number,\n height: number,\n options: Omit<MockupDisplacement, 'image'>,\n): Uint8ClampedArray {\n if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {\n throw new Error('Displacement dimensions must be positive integers');\n }\n const expectedLength = width * height * 4;\n if (source.length !== expectedLength || map.length !== expectedLength) {\n throw new Error('Displacement source and map must match the requested dimensions');\n }\n\n const scaleX = finiteScale(options.scaleX, 10, 'Displacement scaleX');\n const scaleY = finiteScale(options.scaleY, 10, 'Displacement scaleY');\n const channelX = CHANNEL_INDEX[options.channelX ?? 'red'];\n const channelY = CHANNEL_INDEX[options.channelY ?? 'green'];\n const output = new Uint8ClampedArray(expectedLength);\n\n for (let y = 0; y < height; y += 1) {\n for (let x = 0; x < width; x += 1) {\n const offset = (y * width + x) * 4;\n const sourceX = x + ((map[offset + channelX] - 128) / 127) * scaleX;\n const sourceY = y + ((map[offset + channelY] - 128) / 127) * scaleY;\n for (let channel = 0; channel < 4; channel += 1) {\n output[offset + channel] = Math.round(\n sample(source, width, height, sourceX, sourceY, channel),\n );\n }\n }\n }\n return output;\n}\n","import { StaticCanvas } from 'fabric';\nimport type { Canvas, FabricObject, ImageFormat } from 'fabric';\nimport type { MockupConfig, MockupPrintArea } from './types';\nimport { displaceRgba } from './displacement';\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/** Render cloned objects without mutating the live editor canvas. */\nexport async function exportIsolatedPNG(\n source: Canvas,\n objects: FabricObject[],\n options: PngExportOptions & {\n width?: number;\n height?: number;\n backgroundColor?: string;\n backgroundImage?: FabricObject | null;\n cloneObjects?: boolean;\n } = {},\n): Promise<Blob> {\n const element = source.lowerCanvasEl.ownerDocument.createElement('canvas');\n const canvas = new StaticCanvas(element, {\n width: options.width ?? source.getWidth(),\n height: options.height ?? source.getHeight(),\n backgroundColor: options.backgroundColor || undefined,\n });\n try {\n const clones =\n options.cloneObjects === false\n ? objects\n : await Promise.all(objects.map((object) => object.clone()));\n if (clones.length) canvas.add(...clones);\n if (options.backgroundImage) canvas.backgroundImage = await options.backgroundImage.clone();\n canvas.requestRenderAll();\n // Awaited, not returned: `finally` would otherwise dispose the canvas while\n // the export is still reading from it.\n return await exportPNG(canvas as unknown as Canvas, options);\n } finally {\n canvas.dispose();\n }\n}\n\n/**\n * Render just the print-area rectangle, on transparency.\n *\n * This is the file a print provider receives: the design alone, cropped to the\n * printable rectangle, with no garment behind it and no canvas background baked\n * in — so it is rendered from cloned objects rather than off the live canvas.\n */\nexport async function exportPrintArea(\n source: Canvas,\n area: MockupPrintArea,\n options: PngExportOptions = {},\n): Promise<Blob> {\n const { multiplier = 1, format = 'png' as ImageFormat, quality = 1 } = options;\n const width = source.getWidth();\n const height = source.getHeight();\n const clip = computePrintAreaClip(\n area,\n multiplier,\n multiplier,\n width * multiplier,\n height * multiplier,\n );\n if (clip.width <= 0 || clip.height <= 0) {\n throw new Error('Print area does not overlap the canvas');\n }\n\n const element = source.lowerCanvasEl.ownerDocument.createElement('canvas');\n const canvas = new StaticCanvas(element, { width, height });\n try {\n const clones = await Promise.all(source.getObjects().map((object) => object.clone()));\n if (clones.length) canvas.add(...clones);\n canvas.requestRenderAll();\n const rendered = canvas.toCanvasElement(multiplier);\n const output = rendered.ownerDocument.createElement('canvas');\n output.width = Math.max(1, Math.round(clip.width));\n output.height = Math.max(1, Math.round(clip.height));\n const context = output.getContext('2d');\n if (!context) throw new Error('2D canvas context is unavailable');\n context.drawImage(rendered, -clip.left, -clip.top);\n // Awaited, not returned: `finally` would dispose the canvas mid-read.\n return await canvasElementToBlob(output, format, quality);\n } finally {\n canvas.dispose();\n }\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 = (\n image: HTMLImageElement,\n targetContext: CanvasRenderingContext2D = context,\n ) => {\n const placement = computeCoverPlacement(\n image.naturalWidth || image.width,\n image.naturalHeight || image.height,\n output.width,\n output.height,\n );\n targetContext.drawImage(\n image,\n placement.left,\n placement.top,\n placement.width,\n placement.height,\n );\n };\n\n // Full-size scratch buffers, released explicitly in the finally below: a\n // detached canvas element can hold its backing store well past its last\n // reference, and a 4K mockup allocates three of them per export.\n const scratch: HTMLCanvasElement[] = [design];\n try {\n drawCover(await loadImage(mockup.image));\n let compositedDesign: CanvasImageSource = design;\n if (mockup.displacement) {\n const sourceContext = design.getContext('2d');\n if (!sourceContext) throw new Error('2D design context is unavailable');\n const mapCanvas = design.ownerDocument.createElement('canvas');\n scratch.push(mapCanvas);\n mapCanvas.width = design.width;\n mapCanvas.height = design.height;\n const mapContext = mapCanvas.getContext('2d');\n if (!mapContext) throw new Error('2D displacement-map context is unavailable');\n drawCover(await loadImage(mockup.displacement.image), mapContext);\n\n const warped = design.ownerDocument.createElement('canvas');\n scratch.push(warped);\n warped.width = design.width;\n warped.height = design.height;\n const warpedContext = warped.getContext('2d');\n if (!warpedContext) throw new Error('2D displaced-design context is unavailable');\n let sourcePixels: Uint8ClampedArray;\n let mapPixels: Uint8ClampedArray;\n try {\n sourcePixels = sourceContext.getImageData(0, 0, design.width, design.height).data;\n mapPixels = mapContext.getImageData(0, 0, design.width, design.height).data;\n } catch (error) {\n throw new Error('Failed to apply mockup displacement map; verify image CORS access', {\n cause: error,\n });\n }\n const pixels = displaceRgba(sourcePixels, mapPixels, design.width, design.height, {\n ...mockup.displacement,\n scaleX: (mockup.displacement.scaleX ?? 10) * multiplier,\n scaleY: (mockup.displacement.scaleY ?? 10) * multiplier,\n });\n const imageData = warpedContext.createImageData(design.width, design.height);\n imageData.data.set(pixels);\n warpedContext.putImageData(imageData, 0, 0);\n compositedDesign = warped;\n }\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(compositedDesign, 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 await canvasElementToBlob(output, format, quality);\n } finally {\n for (const element of scratch) {\n element.width = 0;\n element.height = 0;\n }\n }\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","import { Point } from 'fabric';\nimport type { FabricObject } from 'fabric';\n\n/**\n * Fitting a text layer's box to the letters inside it.\n *\n * A `Textbox`'s `width` is a **wrap width** somebody authored, not the width of\n * the glyphs, and fabric is explicit that it stays that way: \"Unlike superclass's\n * version of this function, Textbox does not update its width.\" So the box only\n * ever ratchets *up*, and only when a word physically cannot fit — raise a 48px\n * font to 96px inside a 200px box and the selection frame does not move, and\n * dropping the size back leaves the box at whatever width it grew to.\n *\n * Two things need the same measurement: this, and the pattern engine (a tile\n * stepped by the box rather than the run leaves transparent padding between\n * every repeat).\n */\n\n/**\n * Slack left when a box is measured down to its run, so a float rounding error\n * cannot push the widest line into a new wrap on the next layout pass.\n */\nexport const TEXT_FIT_SLACK = 0.5;\n\n/** Box width used to measure a run that is not allowed to wrap. */\nconst MEASURE_WIDTH = 100_000;\n\n/** How far each attempt overshoots while bracketing a width that does not wrap. */\nconst GROWTH_FACTOR = 1.1;\n\n/**\n * Caps on the widening search. A run whose layout never settles has to exit and\n * leave the box a little wide, not spin — this runs on the typing path.\n */\nconst GROWTH_TRIES = 8;\nconst SEARCH_TRIES = 24;\n\n/** The text-shaped surface of an object whose box can be wider than its art. */\nexport interface TextSource extends FabricObject {\n text: string;\n width: number;\n textAlign?: string;\n calcTextWidth?: () => number;\n initDimensions?: () => void;\n /** Lines as laid out. More of them than the author typed means a soft wrap. */\n _textLines?: unknown[];\n /** Set while the run follows a curve, which owns the box instead. */\n path?: FabricObject | null;\n}\n\n/** Width the run wants on one line, measured against a box wide enough not to wrap. */\nfunction unwrappedWidth(text: TextSource): number {\n const authored = text.width;\n try {\n text.set({ width: MEASURE_WIDTH });\n text.initDimensions?.();\n const measured = text.calcTextWidth?.() ?? authored;\n return Number.isFinite(measured) && measured > 0 ? measured : authored;\n } finally {\n text.set({ width: authored });\n text.initDimensions?.();\n }\n}\n\n/** The object as text, or null. Only text carries a box wider than what it paints. */\nexport function asText(object: FabricObject | null | undefined): TextSource | null {\n if (!object) return null;\n const text = object as TextSource;\n return typeof text.text === 'string' && typeof text.calcTextWidth === 'function' ? text : null;\n}\n\n/**\n * What a text object paints inside its box, unscaled: the run's width, and how\n * far the run's centre sits from the box's.\n *\n * `calcTextWidth` reports the widest wrapped line — the run's real footprint —\n * and `textAlign` says where in the box that run sits.\n */\nexport function textInk(text: TextSource): { width: number; dx: number } {\n const boxWidth = Math.max(0, text.width ?? 0);\n const measured = text.calcTextWidth?.() ?? boxWidth;\n const width = Math.max(1, Math.min(boxWidth, Number.isFinite(measured) ? measured : boxWidth));\n const slack = (boxWidth - width) / 2;\n const align = text.textAlign ?? 'left';\n if (align.includes('center')) return { width, dx: 0 };\n const flip = text.flipX ? -1 : 1;\n // Right-aligned text hugs the right edge, so its centre is right of the box's;\n // left and justify both start at the left edge.\n return { width, dx: flip * (align.includes('right') ? slack : -slack) };\n}\n\n/** Lay the run out at `width` and report whether anything soft-wrapped. */\nfunction wrapsAt(text: TextSource, width: number): boolean {\n text.set({ width });\n text.initDimensions?.();\n // Newlines the author typed are lines fabric must produce; anything beyond\n // that count is the box breaking the run on its own.\n const authored = text.text.split('\\n').length;\n return (text._textLines?.length ?? 0) > authored;\n}\n\n/**\n * Narrowest width from `fitted` up that the run does not soft-wrap in, leaving\n * the box laid out at it.\n *\n * `calcTextWidth` reports what a line *renders* at, with its trailing space\n * trimmed, while fabric decides where to wrap with the infix spaces counted —\n * so a box fitted to the rendered width can be a hair too narrow and break the\n * run anyway. The gap grows with the number of spaces and with `charSpacing`,\n * so no constant slack covers it; the width fabric agrees to is searched for\n * instead of guessed.\n */\nfunction widenPastSoftWrap(text: TextSource, fitted: number): number {\n // The common case, and the only pass a single-word run or a box that already\n // holds its text ever pays — this runs on every keystroke.\n if (!wrapsAt(text, fitted)) return fitted;\n\n // Bracket a width that holds the run, then close in on the narrowest one.\n let low = fitted;\n let high = fitted;\n let bracketed = false;\n for (let tries = 0; tries < GROWTH_TRIES; tries += 1) {\n low = high;\n high = high * GROWTH_FACTOR + 1;\n if (!wrapsAt(text, high)) {\n bracketed = true;\n break;\n }\n }\n // Still wrapping at the widest width tried: take it rather than keep going.\n if (!bracketed) return high;\n\n for (let tries = 0; tries < SEARCH_TRIES && high - low > TEXT_FIT_SLACK; tries += 1) {\n const mid = (low + high) / 2;\n if (wrapsAt(text, mid)) low = mid;\n else high = mid;\n }\n // `high` is the bound known to hold the run; `low` is known to break it.\n if (text.width !== high) wrapsAt(text, high);\n return high;\n}\n\n/**\n * Put the box back at `width` after a probe was abandoned partway through.\n *\n * The restore's own failure is dropped rather than thrown: it runs while an\n * exception is already on its way out, and replacing that one with this one\n * would bury the fault that actually broke the layout.\n */\nfunction restoreWidth(text: TextSource, width: number): void {\n try {\n text.set({ width });\n text.initDimensions?.();\n } catch {\n // Nothing better to offer than the width itself, which is set either way.\n }\n}\n\n/**\n * Whether the box is already the narrowest width that holds its run, and so has\n * nothing to search for.\n *\n * Worth asking because {@link widenPastSoftWrap} settles a hair ABOVE the width\n * `calcTextWidth` reports, which is the width the cheap check below compares\n * against — so a settled multi-word box looks perpetually a hair off and would\n * re-run the whole search on every keystroke, deriving the width it already has.\n *\n * Both halves are load-bearing. Holding the run alone would call any box that\n * fits \"settled\", and a box left far too wide by a font-size drop would then\n * never shrink; breaking a slack narrower is what says it is not also too wide.\n *\n * Leaves the box laid out at `width` either way.\n */\nfunction isSettled(text: TextSource, width: number): boolean {\n // Already wrapping: not settled, and `wrapsAt` has left the box at `width`.\n if (wrapsAt(text, width)) return false;\n const tight = wrapsAt(text, width - TEXT_FIT_SLACK);\n wrapsAt(text, width);\n return tight;\n}\n\n/**\n * Fit a text layer's box to the run it holds, leaving the letters where they\n * were on screen.\n *\n * The box becomes **auto-width**: it follows the run in both directions, so\n * raising the font size or opening up the letter spacing widens the frame\n * instead of silently breaking the line, and lowering them again takes the width\n * back — which fabric never does on its own, since `Textbox` only ratchets its\n * width up and only when a word cannot fit at all.\n *\n * Line breaks stay under the author's control through the text itself: the run\n * is measured per hard newline, so `One\\nTwo` stays two lines. What goes away is\n * *soft* wrapping, which in an editor whose text tool opens at a fixed 200px box\n * was mostly accidental anyway.\n *\n * Curved text is skipped — `TextCurveManager` owns that box and sizes it to the\n * glyphs along the path, which this would fight.\n *\n * Returns whether anything changed, so a caller can skip a history checkpoint.\n */\nexport function fitTextWidth(object: FabricObject | null | undefined): boolean {\n const text = asText(object);\n if (!text || text.path) return false;\n\n const authored = text.width;\n try {\n const before = textInk(text);\n const fitted = unwrappedWidth(text) + TEXT_FIT_SLACK;\n // A hair either way is the slack itself, not an edit worth a history step.\n if (Math.abs(fitted - authored) < TEXT_FIT_SLACK) return false;\n\n // Only a box sitting just above the naive fit can be the settled one, since\n // the search never lands past its first bracket step. Screening on that costs\n // no layout at all and skips the probes for a box that is simply the wrong\n // size — including every single-word run, which can never soft-wrap and so\n // could never have come back settled.\n const nearFit = authored > fitted && authored <= fitted * GROWTH_FACTOR + 1;\n if (nearFit && isSettled(text, authored)) return false;\n\n // Keep the run put: shrinking the box moves its centre, and with it every\n // alignment except centre. The ink offset is what that move has to cancel.\n const centre = text.getCenterPoint();\n const settled = widenPastSoftWrap(text, fitted);\n const after = textInk(text);\n const shift = (before.dx - after.dx) * (text.scaleX ?? 1);\n const radians = ((text.angle ?? 0) * Math.PI) / 180;\n const moved = new Point(\n centre.x + shift * Math.cos(radians),\n centre.y + shift * Math.sin(radians),\n );\n text.setPositionByOrigin(moved, 'center', 'center');\n text.setCoords();\n text.dirty = true;\n // Measured against where the box actually settled, not where the first guess\n // put it: widening past a soft wrap can land back on the authored width, and\n // reporting that as a change would checkpoint an edit that never happened.\n return Math.abs(settled - authored) >= TEXT_FIT_SLACK;\n } catch (error) {\n // Every probe above leaves the box at a trial width and undoes it on the\n // next line; a throw in between strands it there — narrower than its run,\n // which is the soft wrap this file exists to keep out. Same discipline as\n // `unwrappedWidth`'s `finally`, applied to the searches it feeds.\n restoreWidth(text, authored);\n throw error;\n }\n}\n","/**\n * Word splitting for `wrap: 'pre-wrap'`.\n *\n * Fabric splits a line on `/[ \\t\\r]/` and rejoins the pieces with a single\n * `' '`, then suppresses that space at a soft break — which is why a wrapped\n * continuation line loses the indentation the author typed. Attaching each\n * run of whitespace to the word that FOLLOWS it moves that whitespace inside a\n * token, where nothing can drop it. One character is held back per token\n * because fabric re-inserts exactly one space between tokens.\n *\n * Pure and fabric-free on purpose: the API mirrors this function against\n * `fabric/node` for the print renderer, and the two are locked together by\n * matching fixture tables in both repos.\n */\nconst WHITESPACE = /[ \\t\\r]/;\n\nexport function preWrapWordSplit(value: string): string[] {\n const tokens: string[] = [];\n let index = 0;\n let first = true;\n\n while (index < value.length) {\n let space = '';\n while (index < value.length && WHITESPACE.test(value[index])) {\n space += value[index];\n index += 1;\n }\n let word = '';\n while (index < value.length && !WHITESPACE.test(value[index])) {\n word += value[index];\n index += 1;\n }\n // Nothing precedes the first token, so it keeps every space it was given.\n tokens.push((first ? space : space.slice(1)) + word);\n first = false;\n }\n\n return tokens.length > 0 ? tokens : [''];\n}\n","import { util } from 'fabric';\nimport type { FabricObject, Group, TMat2D } from 'fabric';\n\n/**\n * Coordinate-space plumbing for mask stacks.\n *\n * A fabric `clipPath` lives in one of two spaces, and the mask stack uses both:\n *\n * - **host space** (`absolutePositioned: false`) — the clip is drawn inside the\n * host object's own transform, so it follows every move, scale and rotation\n * of the layer for free. This is what a *linked* mask wants.\n * - **canvas space** (`absolutePositioned: true`) — the clip ignores the host's\n * transform, so the artwork slides underneath a mask that stays put. This is\n * what an *unlinked* mask wants.\n *\n * A stack composes into a single group, so one unlinked mask forces the whole\n * stack into canvas space; the linked entries are then re-fitted from the host's\n * transform. `toCanvasSpace` / `toHostSpace` are the conversions that migration\n * between the two regimes needs, and they are exact for scale, rotation and skew\n * because they compose matrices rather than copying left/top.\n */\n\nexport function matrixOf(object: FabricObject): TMat2D {\n return object.calcTransformMatrix();\n}\n\n/** Overwrite an object's transform with `matrix`, keeping its own dimensions. */\nexport function applyMatrix(object: FabricObject, matrix: TMat2D): void {\n const decomposed = util.qrDecompose(matrix);\n object.set({\n flipX: false,\n flipY: false,\n originX: 'center',\n originY: 'center',\n left: decomposed.translateX,\n top: decomposed.translateY,\n scaleX: decomposed.scaleX,\n scaleY: decomposed.scaleY,\n angle: decomposed.angle,\n skewX: decomposed.skewX,\n skewY: 0,\n });\n object.setCoords();\n}\n\n/** Host-space geometry → canvas-space, for the same on-screen result. */\nexport function toCanvasSpace(object: FabricObject, host: FabricObject): void {\n applyMatrix(object, util.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));\n}\n\n/** Canvas-space geometry → host-space, for the same on-screen result. */\nexport function toHostSpace(object: FabricObject, host: FabricObject): void {\n applyMatrix(\n object,\n util.multiplyTransformMatrices(util.invertTransform(matrixOf(host)), matrixOf(object)),\n );\n}\n\n/**\n * The mask's transform expressed relative to its host, so a linked mask can be\n * re-derived after the host moves. Stored on the entry, not recomputed from the\n * live objects: by the time the host has moved, the old relationship is gone.\n */\nexport function relativeMatrix(object: FabricObject, host: FabricObject): TMat2D {\n return util.multiplyTransformMatrices(util.invertTransform(matrixOf(host)), matrixOf(object));\n}\n\n/** Re-place a linked mask from the host's current transform and a stored `rel`. */\nexport function applyRelativeMatrix(object: FabricObject, host: FabricObject, rel: TMat2D): void {\n applyMatrix(object, util.multiplyTransformMatrices(matrixOf(host), rel));\n}\n\n/**\n * Fabric types `clipPath` with a looser prop set than the objects the editor\n * builds, so reading a clip back out needs a narrowing step. Every clip this\n * module reads is one it composed from real objects in the first place.\n */\nexport function asObject(clip: NonNullable<FabricObject['clipPath']>): FabricObject {\n return clip as FabricObject;\n}\n\n/** Narrow a persisted `rel` array back to a transform matrix. */\nexport function toMatrix(values: number[] | undefined): TMat2D | null {\n if (!values || values.length !== 6 || values.some((value) => !Number.isFinite(value)))\n return null;\n return [values[0], values[1], values[2], values[3], values[4], values[5]];\n}\n\n/** Scale an object so its unrotated box covers `box`, centred on it. */\nexport function fitToBox(\n object: FabricObject,\n box: { left: number; top: number; width: number; height: number },\n zoom = 1,\n): void {\n const width = Math.max(1, box.width) * zoom;\n const height = Math.max(1, box.height) * zoom;\n object.set({\n originX: 'center',\n originY: 'center',\n angle: 0,\n skewX: 0,\n skewY: 0,\n left: box.left + box.width / 2,\n top: box.top + box.height / 2,\n scaleX: width / Math.max(1, object.width ?? 1),\n scaleY: height / Math.max(1, object.height ?? 1),\n });\n object.setCoords();\n}\n\n/**\n * Pull a composed clip group back apart into standalone objects in the space the\n * group itself was in.\n *\n * `removeAll` already restores each child's own transform on the way out — the\n * group rebases children when it takes them in, and reverses that when it lets\n * them go. Folding the group's matrix back in on top (as one would with a v5-era\n * group) double-counts it, which stays invisible while a mask sits at the origin\n * and moves it twice as far as it should the moment one does not.\n *\n * The group passed in is emptied.\n */\nexport function unwrapGroup(group: Group): FabricObject[] {\n const children = group.removeAll();\n for (const child of children) child.setCoords();\n return children;\n}\n","import { Rect } from 'fabric';\nimport type { Canvas, FabricObject } from 'fabric';\nimport type { Layer, LayerManager } from './layer';\nimport type { HistoryManager } from './history';\nimport type { EventEmitter } from './events';\nimport type { EditorEvents, LayerMeta, TextOverflow, TextWrapMode } from './types';\nimport { asText, fitTextWidth, type TextSource } from './text-fit';\nimport { preWrapWordSplit } from './text-wrap-split';\nimport { asObject, toHostSpace } from './masks/space';\n\n/** Both properties as they apply to a layer, defaults filled in. */\nexport interface TextWrapState {\n wrap: TextWrapMode;\n overflow: TextOverflow;\n}\n\n/** A Textbox, plus the wrapping levers fabric does not put on FabricObject. */\ninterface WrappableText extends TextSource {\n splitByGrapheme?: boolean;\n wordSplit?: (value: string) => string[];\n}\n\n/**\n * Fabric names `wordSplit` as an override point, so pre-wrap is an own-property\n * shadow of the prototype method rather than a patched prototype: two layers on\n * one canvas can be in different modes.\n */\nfunction patchWordSplit(text: WrappableText): void {\n if (Object.prototype.hasOwnProperty.call(text, 'wordSplit')) return;\n Object.defineProperty(text, 'wordSplit', {\n value: preWrapWordSplit,\n configurable: true,\n writable: true,\n });\n}\n\nfunction unpatchWordSplit(text: WrappableText): void {\n if (Object.prototype.hasOwnProperty.call(text, 'wordSplit')) {\n delete text.wordSplit;\n }\n}\n\nexport function readTextWrap(meta: LayerMeta | undefined): TextWrapState {\n return { wrap: meta?.wrap ?? 'none', overflow: meta?.overflow ?? 'visible' };\n}\n\n/** Builds the clip rect. A parameter because a Node renderer's `Rect` (from\n * `fabric/node`) and the browser's `Rect` (from `fabric`) are different\n * classes — a clip built by one does not render on the other's canvas. */\ntype RectFactory = (options: Record<string, unknown>) => FabricObject;\n\n/**\n * The box, as a clip, in the layer's own frame.\n *\n * A top-level clipPath's coordinates are the object's own, centred on it and\n * unaffected by its scale, so this is sized to the unscaled dimensions — the\n * same rule `MaskPresetManager.buildClip` follows. That invariant holds only at\n * the top level: nested under another clip the frame is the parent's, and\n * `derive` converts into it. Height is the text height, which IS the content\n * height for a Textbox, so only the width ever clips anything.\n *\n * A fresh Rect per derive, and derive runs on the typing path — cheap, since\n * `objectCaching: false` means there is no backing canvas to retain. If it ever\n * shows up in a profile, the move is to resize the clip already installed\n * rather than to build a second one.\n */\nfunction boxClip(text: WrappableText, makeRect: RectFactory): FabricObject {\n return makeRect({\n width: Math.max(1, text.width ?? 1),\n height: Math.max(1, text.height ?? 1),\n originX: 'center',\n originY: 'center',\n left: 0,\n top: 0,\n objectCaching: false,\n });\n}\n\n/**\n * Does a mask already own this layer's clipPath? Read from meta, not the object.\n *\n * Deliberately blind to `PatternManager`, which strips a layer's clip for the\n * duration of a pattern: meta still says \"masked\" while the object carries no\n * clip at all, so the box clip becomes a no-op until the pattern is removed.\n * That is the safe way round — no clip beats taking a slot the pattern is using\n * — so do not \"fix\" this by asking the object what it currently holds.\n */\nfunction hasMask(meta: LayerMeta): boolean {\n return !!meta.maskPreset || (meta.maskStack?.length ?? 0) > 0;\n}\n\n/**\n * The renderer-agnostic half of {@link TextWrapManager.derive}: the fabric\n * state a wrap mode implies once the box's width is already settled.\n *\n * Deliberately does NOT touch width — re-fitting here would let a Node\n * renderer, which sees only the serialized state and never `meta.wrapWidth`,\n * shift a box the browser already fitted at authoring time. Width is the\n * manager's job (`fitTextWidth` for `'none'`, `meta.wrapWidth` restore for\n * everything else); this only sets the levers fabric does not serialize on\n * its own (`wordSplit`) or that depend on the box the manager just settled\n * (the clip). Call it AFTER any width decision, never before — the clip below\n * is sized to `text.width`/`text.height` as they stand when this runs.\n */\nexport function applyTextWrapToObject(\n object: FabricObject,\n meta: LayerMeta | undefined,\n makeRect: RectFactory = (options) => new Rect(options),\n): void {\n const text = asText(object) as WrappableText | null;\n if (!text) return;\n\n const state = readTextWrap(meta);\n\n // A curved run follows a path built for the box the curve manager sized;\n // re-deriving here would fight it. The mode stays stored and applies again\n // when the curve is cleared.\n if (!text.path) {\n text.set({ splitByGrapheme: state.wrap === 'break-all' });\n if (state.wrap === 'pre-wrap') patchWordSplit(text);\n else unpatchWordSplit(text);\n // `splitByGrapheme` is not one of fabric's `textLayoutProperties`, so the\n // `set` above did not re-lay the run out on its own, and neither does\n // patching `wordSplit` (an own-property override, not a tracked prop) —\n // a layer already at its current width, with no width change to trigger\n // fabric's own relayout, would otherwise keep lines laid out under the\n // PREVIOUS split function.\n text.initDimensions?.();\n }\n\n // Installed even on curved text: a curve owns the box, not the clip.\n const clip = state.overflow === 'hidden' ? boxClip(text, makeRect) : undefined;\n if (meta && hasMask(meta)) {\n const host = text.clipPath;\n if (host) {\n // A nested clip is drawn in its PARENT clip's frame, not the layer's:\n // fabric replays every entry in `parentClipPaths` before applying the\n // child's own transform (`Object.createClipPathLayer`). The mask group\n // lays itself out around its geometry, so that frame coincides with the\n // layer's only for a centred mask — which is why presets and default-fit\n // masks look right. Left unconverted, the box clip is displaced by the\n // whole of the mask's transform: an offset mask that covers the run\n // completely starts cutting it, and an unlinked one (composed in canvas\n // space, so the offset is the layer's full distance from the origin)\n // carries the clip clear of the text, which disappears outright.\n //\n // Re-expressing it relative to the mask is the same conversion a linked\n // mask needs when it joins a canvas-space stack, so it uses the same\n // helper rather than new matrix maths. `asObject` is the narrowing step\n // that module already carries for fabric's looser `clipPath` typing.\n if (clip) toHostSpace(clip, asObject(host));\n host.clipPath = clip;\n }\n } else {\n text.clipPath = clip;\n }\n}\n\n/**\n * How a text layer breaks its lines, and whether it paints past its box.\n *\n * `meta` is the authoring record; what actually renders — and what a print\n * pipeline that never reads `meta` sees — is the fabric state derived here:\n * `width`, `splitByGrapheme` and `clipPath`, all of which fabric serializes on\n * its own. That is the same division `TextCurveManager` draws between\n * `meta.curve` and the `path` it installs.\n */\nexport class TextWrapManager {\n /**\n * Typing changes the run the box was fitted to, so an auto-width layer has to\n * re-fit. No history entry: fabric records the edit when editing exits, and a\n * save per keystroke would bury every earlier step.\n */\n private readonly onTextChanged = (event: { target?: FabricObject }) => {\n const layer = event.target ? this.layers.findByObject(event.target) : undefined;\n if (!layer) return;\n this.refresh(layer.id, false);\n };\n\n /**\n * Both mask owners — the preset manager and every mask-stack mutation —\n * install their clip straight onto `clipPath`, dropping whatever was there,\n * and neither knows this layer had a box clip. Re-deriving on the one event\n * they both announce puts it back where it now belongs: nested under the new\n * mask, or at the top level when the last mask leaves. Waiting for the next\n * keystroke instead would leave a layer that should clip inside its box\n * serialized unclipped — which is what the print renderer reads.\n */\n private readonly onMasksChanged = ({ target }: { target: string }) => {\n // `refresh` ignores an id that is not a text layer, so the whole-design\n // mask target passes through it harmlessly.\n this.refresh(target);\n };\n\n constructor(\n private canvas: Canvas,\n private layers: LayerManager,\n private history: HistoryManager,\n private events: EventEmitter<EditorEvents>,\n ) {\n this.canvas.on('text:changed', this.onTextChanged);\n this.events.on('masks:changed', this.onMasksChanged);\n }\n\n dispose(): void {\n this.canvas.off('text:changed', this.onTextChanged);\n this.events.off('masks:changed', this.onMasksChanged);\n }\n\n /** Both properties for a text layer, or null when it is not text. */\n get(layerId: string): TextWrapState | null {\n const layer = this.layers.get(layerId);\n if (!layer || !asText(layer.fabricObject)) return null;\n return readTextWrap(layer.meta);\n }\n\n apply(layerId: string, wrap: TextWrapMode, save = true): boolean {\n const layer = this.layers.get(layerId);\n const text = layer ? asText(layer.fabricObject) : null;\n if (!layer || !text) return false;\n if (wrap === 'none') {\n // Parked before anything widens the box, so the authored width is the one\n // that comes back — and only on the way IN, or a second call would park\n // the fitted width over it.\n //\n // Never from a curved box: `TextCurveManager` widened that one to fit the\n // path and is holding the real authored width in `meta.curveWidth`. Park\n // here and uncurving later would restore the curve's width as if the\n // author had chosen it.\n const curved = !!text.path;\n if (layer.meta.wrapWidth === undefined && !curved) {\n layer.meta.wrapWidth = text.width ?? 0;\n }\n }\n layer.meta.wrap = wrap;\n return this.derive(layerId, save);\n }\n\n setOverflow(layerId: string, overflow: TextOverflow, save = true): boolean {\n const layer = this.layers.get(layerId);\n if (!layer || !asText(layer.fabricObject)) return false;\n layer.meta.overflow = overflow;\n return this.derive(layerId, save);\n }\n\n /** Back to the defaults: auto-width, unclipped. */\n clear(layerId: string, save = true): boolean {\n const layer = this.layers.get(layerId);\n if (!layer || !asText(layer.fabricObject)) return false;\n delete layer.meta.overflow;\n return this.apply(layerId, 'none', save);\n }\n\n /** Re-derive from the stored mode — after a text, font or size change. */\n refresh(layerId: string, save = false): boolean {\n const layer = this.layers.get(layerId);\n if (!layer || !asText(layer.fabricObject)) return false;\n return this.derive(layerId, save);\n }\n\n /**\n * Re-derive every text layer — used after a state restore.\n *\n * Recursive because a template inserts as a group of real child layers, and\n * text inside one would otherwise keep whatever box it was restored with.\n */\n refreshAll(): void {\n const visit = (layers: Layer[]): void => {\n for (const layer of layers) {\n if (asText(layer.fabricObject)) this.refresh(layer.id);\n if (layer.children.length > 0) visit(layer.children);\n }\n };\n visit(this.layers.getAll());\n }\n\n private derive(layerId: string, save: boolean): boolean {\n const layer = this.layers.get(layerId);\n const text = layer ? (asText(layer.fabricObject) as WrappableText | null) : null;\n if (!layer || !text) return false;\n\n const state = readTextWrap(layer.meta);\n\n // A curved run follows a path built for the box the curve manager sized;\n // re-deriving here would fight it. The mode stays stored and applies again\n // when the curve is cleared. This is the layer-dependent half of the mode:\n // `meta.wrapWidth` is a layer authoring record with no fabric equivalent,\n // and re-fitting is never something a renderer that only sees the already-\n // fitted serialized width should redo — both stay here rather than moving\n // into `applyTextWrapToObject`.\n if (!text.path) {\n if (state.wrap === 'none') {\n fitTextWidth(text);\n } else if (layer.meta.wrapWidth !== undefined) {\n text.set({ width: layer.meta.wrapWidth });\n delete layer.meta.wrapWidth;\n }\n }\n\n // The rest — split mode and the overflow clip — is object-only and shared\n // with the Node renderer, which derives it from the very same `meta` on a\n // freshly restored object with no editor around it. Called after the width\n // decision above so the clip is sized to the box that decision just chose.\n applyTextWrapToObject(text, layer.meta);\n\n text.dirty = true;\n text.setCoords();\n this.canvas.requestRenderAll();\n this.events.emit('layer:modified', { layerId });\n if (save) this.history.save();\n return true;\n }\n}\n","import type { EditorState } from './types';\nimport type { NodeRenderOptions } from './node';\n\nexport interface PrintPdfOptions {\n /** Rasterization density. Defaults to the document DPI, or 300. */\n dpi?: number;\n /** Bleed on every edge, in inches. Defaults to 0.125in. */\n bleed?: number;\n /** Space outside bleed reserved for printer marks, in inches. Defaults to 0.25in. */\n marksMargin?: number;\n trimMarks?: boolean;\n registrationMarks?: boolean;\n /** Optional safe-area inset in inches for preflight warnings. */\n safeArea?: number;\n /** Sharp built-in `cmyk` profile or an absolute path to a custom ICC profile. */\n iccProfile?: string;\n /** Ceiling on the rasterized bleed image, in pixels. Defaults to 250 megapixels. */\n maxPixels?: number;\n outputConditionIdentifier?: string;\n title?: string;\n allowImageUrl?: NodeRenderOptions['allowImageUrl'];\n /** Preserve SVG shapes/text over a CMYK bleed raster. Defaults to `vector`. */\n rendering?: 'vector' | 'raster';\n /** PDFKit font registrations keyed by the exact SVG font-family name. */\n fontFiles?: Record<string, string | Uint8Array>;\n /** Convert text backed by `fontFiles` into glyph paths. Defaults to true. */\n outlineFonts?: boolean;\n}\n\nexport interface PrintPdfResult {\n format: 'pdf';\n mimeType: 'application/pdf';\n data: Uint8Array;\n standard: 'PDF/X-4';\n colourSpace: 'CMYK';\n rendering: 'vector' | 'raster';\n outlinedFonts: string[];\n unoutlinedFonts: string[];\n dpi: number;\n trimWidthPoints: number;\n trimHeightPoints: number;\n bleedPoints: number;\n warnings: string[];\n}\n\nfunction svgAttributes(source: string): Record<string, string> {\n return Object.fromEntries(\n [...source.matchAll(/([\\w:-]+)=(?:\"([^\"]*)\"|'([^']*)')/g)].map((match) => [\n match[1],\n match[2] ?? match[3] ?? '',\n ]),\n );\n}\n\nfunction decodeXmlText(source: string): string {\n return source\n .replace(/<[^>]+>/g, '')\n .replace(/&#x([\\da-f]+);/gi, (_, value: string) =>\n String.fromCodePoint(Number.parseInt(value, 16)),\n )\n .replace(/&#(\\d+);/g, (_, value: string) => String.fromCodePoint(Number(value)))\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/&/g, '&');\n}\n\nfunction escapeXmlAttribute(value: string): string {\n return value.replace(/[<>&\"']/g, (character) => `&#${character.charCodeAt(0)};`);\n}\n\nfunction fontSource(\n files: Record<string, string | Uint8Array>,\n family: string,\n bold: boolean,\n italic: boolean,\n): string | Uint8Array | undefined {\n const suffix = bold && italic ? '-BoldItalic' : bold ? '-Bold' : italic ? '-Italic' : '';\n // `family` comes from the untrusted SVG, so plain indexing would resolve\n // `__proto__`/`constructor` to inherited members and hand a non-font to fontkit.\n const own = (key: string) =>\n Object.hasOwn(files, key) ? (files[key] as string | Uint8Array) : undefined;\n return own(`${family}${suffix}`) ?? own(family);\n}\n\nasync function outlineSvgText(\n svg: string,\n files: Record<string, string | Uint8Array>,\n outlined: Set<string>,\n): Promise<string> {\n const fontkit = await import('fontkit');\n let output = '';\n let cursor = 0;\n for (const textMatch of svg.matchAll(/<text\\b([^>]*)>([\\s\\S]*?)<\\/text>/gi)) {\n const index = textMatch.index ?? 0;\n output += svg.slice(cursor, index);\n cursor = index + textMatch[0].length;\n const textAttributes = svgAttributes(textMatch[1]);\n const spans = [...textMatch[2].matchAll(/<tspan\\b([^>]*)>([\\s\\S]*?)<\\/tspan>/gi)];\n const lines = spans.length\n ? spans.map((span) => ({ attributes: svgAttributes(span[1]), text: decodeXmlText(span[2]) }))\n : [{ attributes: textAttributes, text: decodeXmlText(textMatch[2]) }];\n const resolved = lines.map((line) => {\n const attributes = { ...textAttributes, ...line.attributes };\n const family = attributes['font-family'] ?? 'sans-serif';\n const bold = /bold|[6-9]00/i.test(attributes['font-weight'] ?? '');\n const italic = /italic|oblique/i.test(attributes['font-style'] ?? '');\n return { ...line, attributes, family, source: fontSource(files, family, bold, italic) };\n });\n if (resolved.some((line) => !line.source)) {\n output += textMatch[0];\n continue;\n }\n const paths: string[] = [];\n for (const line of resolved) {\n const source = line.source!;\n const opened =\n typeof source === 'string'\n ? fontkit.openSync(source)\n : fontkit.create(Buffer.from(source.buffer, source.byteOffset, source.byteLength));\n if (!('layout' in opened)) {\n throw new Error(`Font collection requires a named face: ${line.family}`);\n }\n const size = Number.parseFloat(line.attributes['font-size'] ?? '16');\n const scale = size / opened.unitsPerEm;\n const style = line.attributes.style ?? '';\n const run = opened.layout(line.text);\n let penX = Number.parseFloat(line.attributes.x ?? '0');\n const baseline = Number.parseFloat(line.attributes.y ?? '0');\n run.glyphs.forEach((glyph, glyphIndex) => {\n const position = run.positions[glyphIndex];\n const x = penX + position.xOffset * scale;\n const y = baseline - position.yOffset * scale;\n paths.push(\n `<path d=\"${glyph.path.toSVG()}\" transform=\"translate(${x} ${y}) scale(${scale} ${-scale})\" style=\"${escapeXmlAttribute(style)}\"/>`,\n );\n penX += position.xAdvance * scale;\n });\n outlined.add(line.family);\n }\n output += `<g data-outlined-font=\"${escapeXmlAttribute([...new Set(resolved.map((line) => line.family))].join(','))}\">${paths.join('')}</g>`;\n }\n return output + svg.slice(cursor);\n}\n\nfunction rgbToCmyk([redByte, greenByte, blueByte]: [number, number, number]): [\n number,\n number,\n number,\n number,\n] {\n const red = redByte / 255;\n const green = greenByte / 255;\n const blue = blueByte / 255;\n const black = 1 - Math.max(red, green, blue);\n if (black >= 1) return [0, 0, 0, 100];\n return [\n ((1 - red - black) / (1 - black)) * 100,\n ((1 - green - black) / (1 - black)) * 100,\n ((1 - blue - black) / (1 - black)) * 100,\n black * 100,\n ];\n}\n\nasync function imageSourceBytes(\n source: string,\n allow?: (url: string) => boolean,\n): Promise<Uint8Array> {\n if (source.startsWith('data:')) {\n const response = await fetch(source);\n if (!response.ok) throw new Error('Failed to decode an embedded SVG image');\n return new Uint8Array(await response.arrayBuffer());\n }\n const permitted = allow\n ? allow(source)\n : (() => {\n try {\n return ['http:', 'https:'].includes(new URL(source).protocol);\n } catch {\n return false;\n }\n })();\n if (!permitted) throw new Error(`Blocked disallowed vector image URL: ${source}`);\n const response = await fetch(source);\n if (!response.ok) throw new Error(`Failed to load vector image: ${source}`);\n return new Uint8Array(await response.arrayBuffer());\n}\n\nasync function convertSvgImagesToCmyk(\n svg: string,\n sharp: (typeof import('sharp'))['default'],\n profile: string,\n allow?: (url: string) => boolean,\n): Promise<string> {\n const sources = new Set<string>();\n for (const image of svg.matchAll(/<image\\b[^>]*(?:xlink:href|href)=\"([^\"]+)\"[^>]*>/gi)) {\n if (image[1]) sources.add(image[1]);\n }\n let converted = svg;\n for (const source of sources) {\n const bytes = await imageSourceBytes(source, allow);\n const jpeg = await sharp(bytes)\n .flatten({ background: '#ffffff' })\n .toColourspace('cmyk')\n .withIccProfile(profile)\n .jpeg({ quality: 100, chromaSubsampling: '4:4:4' })\n .toBuffer();\n const dataUrl = `data:image/jpeg;base64,${jpeg.toString('base64')}`;\n converted = converted.split(source).join(dataUrl);\n }\n return converted;\n}\n\nasync function renderVectorOverlay(\n state: EditorState,\n options: PrintPdfOptions,\n width: number,\n height: number,\n sharp: (typeof import('sharp'))['default'],\n warnings: string[],\n outlinedFonts: Set<string>,\n unoutlinedFonts: Set<string>,\n): Promise<Uint8Array> {\n const [{ default: PDFKit }, { default: SVGtoPDF }, { renderEditorState }] = await Promise.all([\n import('pdfkit'),\n import('svg-to-pdfkit'),\n import('./node'),\n ]);\n // The bleed image is already flattened onto white. Render the trim overlay\n // against the same opaque print substrate so semi-transparent artwork does\n // not blend a second time with the raster copy beneath it.\n const rendered = await renderEditorState(\n { ...state, background: '#ffffff' },\n {\n format: 'svg',\n allowImageUrl: options.allowImageUrl,\n },\n );\n if (typeof rendered.data !== 'string') throw new Error('Vector rendering returned raster data');\n const fontFiles = options.fontFiles ?? {};\n const outlinedSvg =\n options.outlineFonts === false\n ? rendered.data\n : await outlineSvgText(rendered.data, fontFiles, outlinedFonts);\n const svg = await convertSvgImagesToCmyk(\n outlinedSvg,\n sharp,\n options.iccProfile ?? 'cmyk',\n options.allowImageUrl,\n );\n const remainingText = [...svg.matchAll(/<text\\b([^>]*)>/gi)].map((match) =>\n svgAttributes(match[1]),\n );\n for (const attributes of remainingText) {\n const family = attributes['font-family'] ?? 'sans-serif';\n unoutlinedFonts.add(family);\n const bold = /bold|[6-9]00/i.test(attributes['font-weight'] ?? '');\n const italic = /italic|oblique/i.test(attributes['font-style'] ?? '');\n if (!fontSource(fontFiles, family, bold, italic)) {\n warnings.push(\n `Font \"${family}\" used a PDF standard fallback; supply fontFiles for exact embedding`,\n );\n }\n }\n\n const document = new PDFKit({ autoFirstPage: false, compress: false, pdfVersion: '1.7' });\n for (const [name, path] of Object.entries(fontFiles)) {\n document.registerFont(name, typeof path === 'string' ? path : Buffer.from(path));\n }\n document.addPage({ size: [width, height], margin: 0 });\n SVGtoPDF(document, svg, 0, 0, {\n width,\n height,\n preserveAspectRatio: 'none',\n colorCallback: (color) => {\n const [rgb, opacity] = color;\n return [rgbToCmyk(rgb), opacity] as unknown as typeof color;\n },\n warningCallback: (warning) => warnings.push(`Vector render: ${warning}`),\n });\n\n return new Promise<Uint8Array>((resolve, reject) => {\n const chunks: Uint8Array[] = [];\n document.on('data', (chunk: Uint8Array) => chunks.push(chunk));\n document.on('error', reject);\n document.on('end', () => resolve(new Uint8Array(Buffer.concat(chunks))));\n document.end();\n });\n}\n\n/** Beyond this no printer is served, and larger values only buy an OOM. */\nconst MAX_DPI = 2400;\n/** ~24×36in at 600dpi. Bounds the raster a hostile state can ask a server for. */\nconst DEFAULT_MAX_PIXELS = 250_000_000;\n\nfunction positive(value: number, label: string): number {\n if (!Number.isFinite(value) || value <= 0) throw new Error(`${label} must be positive`);\n return value;\n}\n\n/**\n * States are browser-authored and untrusted on a server. `documentDpi` divides\n * the render multiplier, so an unvalidated fractional value (0.001) inflates the\n * rasterization by six orders of magnitude before any size check runs.\n */\nfunction boundedDpi(value: number, label: string): number {\n const dpi = positive(value, label);\n if (dpi > MAX_DPI) throw new Error(`${label} must not exceed ${MAX_DPI}`);\n if (dpi < 1) throw new Error(`${label} must be at least 1`);\n return dpi;\n}\n\nfunction nonNegative(value: number, label: string): number {\n if (!Number.isFinite(value) || value < 0) throw new Error(`${label} cannot be negative`);\n return value;\n}\n\nfunction preflightSafeArea(state: EditorState, safePixels: number): string[] {\n if (safePixels <= 0) return [];\n const right = state.canvas.width - safePixels;\n const bottom = state.canvas.height - safePixels;\n const warnings: string[] = [];\n for (const layer of state.layers) {\n if (!layer.visible) continue;\n const object = layer.fabricObject;\n const left = Number(object.left ?? 0);\n const top = Number(object.top ?? 0);\n const width = Number(object.width ?? 0) * Math.abs(Number(object.scaleX ?? 1));\n const height = Number(object.height ?? 0) * Math.abs(Number(object.scaleY ?? 1));\n if (left < safePixels || top < safePixels || left + width > right || top + height > bottom) {\n warnings.push(`Layer \"${layer.name}\" extends outside the configured safe area`);\n }\n }\n return warnings;\n}\n\n/**\n * Render an EditorState into a print-production PDF/X-4 file.\n *\n * The design is rasterized at the requested density, converted through Sharp's\n * CMYK ICC pipeline, edge-extended into bleed, and embedded with output intent,\n * trim/bleed boxes, XMP identification, and optional printer marks.\n */\nexport async function renderPrintPdf(\n state: EditorState,\n options: PrintPdfOptions = {},\n): Promise<PrintPdfResult> {\n const dpi = boundedDpi(options.dpi ?? state.canvas.dpi ?? 300, 'Print DPI');\n const documentDpi = boundedDpi(state.canvas.dpi ?? 72, 'Document DPI');\n const maxPixels = positive(options.maxPixels ?? DEFAULT_MAX_PIXELS, 'Max pixels');\n const bleedInches = nonNegative(options.bleed ?? 0.125, 'Bleed');\n const marksMarginInches = nonNegative(options.marksMargin ?? 0.25, 'Marks margin');\n const safeAreaInches = nonNegative(options.safeArea ?? 0, 'Safe area');\n const rendering = options.rendering ?? 'vector';\n const bleedPixels = Math.round(bleedInches * dpi);\n const trimWidthPoints = (state.canvas.width / documentDpi) * 72;\n const trimHeightPoints = (state.canvas.height / documentDpi) * 72;\n const bleedPoints = bleedInches * 72;\n const marksMarginPoints = marksMarginInches * 72;\n\n const scale = dpi / documentDpi;\n const outputPixels =\n Math.round(state.canvas.width * scale + bleedPixels * 2) *\n Math.round(state.canvas.height * scale + bleedPixels * 2);\n if (!Number.isFinite(outputPixels) || outputPixels > maxPixels) {\n throw new Error(\n `Print raster of ${outputPixels} pixels exceeds the ${maxPixels} pixel budget; lower the DPI or raise maxPixels`,\n );\n }\n\n const [{ default: sharp }, pdfLib] = await Promise.all([import('sharp'), import('pdf-lib')]);\n // Keep the optional print entry free of a static cycle with `node.ts`, which\n // re-exports this function as part of the Node-only public surface.\n const { renderEditorState } = await import('./node');\n const rendered = await renderEditorState(state, {\n format: 'png',\n multiplier: scale,\n allowImageUrl: options.allowImageUrl,\n });\n if (typeof rendered.data === 'string') throw new Error('Print rasterization returned SVG data');\n\n let pipeline = sharp(rendered.data).flatten({ background: '#ffffff' });\n if (bleedPixels > 0) {\n pipeline = pipeline.extend({\n top: bleedPixels,\n right: bleedPixels,\n bottom: bleedPixels,\n left: bleedPixels,\n extendWith: 'copy',\n });\n }\n const { data: cmykJpeg, info } = await pipeline\n .toColourspace('cmyk')\n .withIccProfile(options.iccProfile ?? 'cmyk')\n .withDensity(dpi)\n .jpeg({ quality: 100, chromaSubsampling: '4:4:4' })\n .toBuffer({ resolveWithObject: true });\n const metadata = await sharp(cmykJpeg).metadata();\n if (info.channels !== 4 || metadata.space !== 'cmyk' || !metadata.icc) {\n throw new Error('CMYK conversion did not produce a four-channel image with an ICC profile');\n }\n\n const { PDFDocument, PDFDict, PDFName, PDFString, cmyk } = pdfLib;\n const document = await PDFDocument.create();\n const title = options.title ?? 'Overtone Canvas Editor print export';\n document.setTitle(title);\n document.setCreator('@overtone-art/canvas-editor-core');\n document.setProducer('@overtone-art/canvas-editor-core');\n const pageWidth = trimWidthPoints + bleedPoints * 2 + marksMarginPoints * 2;\n const pageHeight = trimHeightPoints + bleedPoints * 2 + marksMarginPoints * 2;\n const page = document.addPage([pageWidth, pageHeight]);\n const image = await document.embedJpg(cmykJpeg);\n page.drawImage(image, {\n x: marksMarginPoints,\n y: marksMarginPoints,\n width: trimWidthPoints + bleedPoints * 2,\n height: trimHeightPoints + bleedPoints * 2,\n });\n\n const trimLeft = marksMarginPoints + bleedPoints;\n const trimBottom = marksMarginPoints + bleedPoints;\n const trimRight = trimLeft + trimWidthPoints;\n const trimTop = trimBottom + trimHeightPoints;\n const warnings = preflightSafeArea(state, safeAreaInches * documentDpi);\n const outlinedFonts = new Set<string>();\n const unoutlinedFonts = new Set<string>();\n if (rendering === 'vector') {\n const vectorPdf = await renderVectorOverlay(\n state,\n options,\n trimWidthPoints,\n trimHeightPoints,\n sharp,\n warnings,\n outlinedFonts,\n unoutlinedFonts,\n );\n const [vectorPage] = await document.embedPdf(vectorPdf);\n page.drawPage(vectorPage, {\n x: trimLeft,\n y: trimBottom,\n width: trimWidthPoints,\n height: trimHeightPoints,\n });\n }\n const bleedLeft = marksMarginPoints;\n const bleedBottom = marksMarginPoints;\n const bleedRight = pageWidth - marksMarginPoints;\n const bleedTop = pageHeight - marksMarginPoints;\n const context = document.context;\n page.node.set(PDFName.of('TrimBox'), context.obj([trimLeft, trimBottom, trimRight, trimTop]));\n page.node.set(\n PDFName.of('BleedBox'),\n context.obj([bleedLeft, bleedBottom, bleedRight, bleedTop]),\n );\n page.node.set(PDFName.of('ArtBox'), context.obj([trimLeft, trimBottom, trimRight, trimTop]));\n\n const markColour = cmyk(0, 0, 0, 1);\n if (options.trimMarks !== false) {\n const offset = Math.max(3, bleedPoints / 2);\n const length = Math.max(9, marksMarginPoints - 3);\n for (const x of [trimLeft, trimRight]) {\n page.drawLine({\n start: { x, y: trimBottom - offset },\n end: { x, y: trimBottom - offset - length },\n thickness: 0.5,\n color: markColour,\n });\n page.drawLine({\n start: { x, y: trimTop + offset },\n end: { x, y: trimTop + offset + length },\n thickness: 0.5,\n color: markColour,\n });\n }\n for (const y of [trimBottom, trimTop]) {\n page.drawLine({\n start: { x: trimLeft - offset, y },\n end: { x: trimLeft - offset - length, y },\n thickness: 0.5,\n color: markColour,\n });\n page.drawLine({\n start: { x: trimRight + offset, y },\n end: { x: trimRight + offset + length, y },\n thickness: 0.5,\n color: markColour,\n });\n }\n }\n if (options.registrationMarks !== false) {\n for (const [x, y] of [\n [pageWidth / 2, marksMarginPoints / 2],\n [pageWidth / 2, pageHeight - marksMarginPoints / 2],\n [marksMarginPoints / 2, pageHeight / 2],\n [pageWidth - marksMarginPoints / 2, pageHeight / 2],\n ] as Array<[number, number]>) {\n page.drawCircle({ x, y, size: 4, borderWidth: 0.5, borderColor: markColour });\n page.drawLine({\n start: { x: x - 6, y },\n end: { x: x + 6, y },\n thickness: 0.5,\n color: markColour,\n });\n page.drawLine({\n start: { x, y: y - 6 },\n end: { x, y: y + 6 },\n thickness: 0.5,\n color: markColour,\n });\n }\n }\n\n const profileStream = context.flateStream(metadata.icc, {\n N: 4,\n Alternate: PDFName.of('DeviceCMYK'),\n });\n const profileRef = context.register(profileStream);\n const outputIntent = context.obj({\n Type: PDFName.of('OutputIntent'),\n S: PDFName.of('GTS_PDFX'),\n OutputConditionIdentifier: PDFString.of(options.outputConditionIdentifier ?? 'CMYK'),\n RegistryName: PDFString.of('https://www.color.org'),\n Info: PDFString.of(options.outputConditionIdentifier ?? 'CMYK print condition'),\n DestOutputProfile: profileRef,\n });\n document.catalog.set(PDFName.of('OutputIntents'), context.obj([context.register(outputIntent)]));\n\n const now = new Date().toISOString();\n const xmp = `<?xpacket begin=\"\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>\n<x:xmpmeta xmlns:x=\"adobe:ns:meta/\"><rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n<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>\n</rdf:RDF></x:xmpmeta><?xpacket end=\"w\"?>`;\n const metadataStream = context.flateStream(new TextEncoder().encode(xmp), {\n Type: PDFName.of('Metadata'),\n Subtype: PDFName.of('XML'),\n });\n document.catalog.set(PDFName.of('Metadata'), context.register(metadataStream));\n const infoRef = context.trailerInfo.Info;\n if (infoRef) {\n const infoDict = context.lookup(infoRef, PDFDict);\n infoDict.set(PDFName.of('GTS_PDFXVersion'), PDFString.of('PDF/X-4'));\n infoDict.set(PDFName.of('Trapped'), PDFName.of('False'));\n }\n\n return {\n format: 'pdf',\n mimeType: 'application/pdf',\n data: await document.save({ useObjectStreams: false }),\n standard: 'PDF/X-4',\n colourSpace: 'CMYK',\n rendering,\n outlinedFonts: [...outlinedFonts].sort(),\n unoutlinedFonts: [...unoutlinedFonts].sort(),\n dpi,\n trimWidthPoints,\n trimHeightPoints,\n bleedPoints,\n warnings,\n };\n}\n","import { FabricImage, Rect, StaticCanvas, util } from 'fabric/node';\nimport type { FabricObject, ImageFormat } from 'fabric/node';\nimport { computeCoverPlacement, computePrintAreaClip } from './export';\nimport { displaceRgba } from './displacement';\nimport { applyTextWrapToObject } from './text-wrap';\nimport type { EditorState, MockupBlendMode, MockupConfig, MockupDisplacement } from './types';\n\nexport { renderPrintPdf } from './print';\nexport type { PrintPdfOptions, PrintPdfResult } from './print';\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 // The browser's TextWrapManager derives `wordSplit` and the overflow\n // clip live as the author types; fabric serializes neither the function\n // override nor (when a mask is present) the clip's nested, space-\n // converted placement, so a freshly enlivened object needs the same\n // derivation run once here, or the engine's own print output would\n // differ from what the editor previewed.\n applyTextWrapToObject(object, layer.meta, (options) => new Rect(options));\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\nasync function displacedDesignUrl(\n designCanvas: StaticCanvas,\n displacement: MockupDisplacement,\n width: number,\n height: number,\n): Promise<string> {\n const mapCanvas = new StaticCanvas(undefined, { width, height });\n const warpedCanvas = new StaticCanvas(undefined, { width, height });\n try {\n mapCanvas.add(await coverImage(displacement.image, width, height));\n mapCanvas.renderAll();\n const pixels = displaceRgba(\n designCanvas.getContext().getImageData(0, 0, width, height).data,\n mapCanvas.getContext().getImageData(0, 0, width, height).data,\n width,\n height,\n displacement,\n );\n const imageData = warpedCanvas.getContext().createImageData(width, height);\n imageData.data.set(pixels);\n warpedCanvas.getContext().putImageData(imageData, 0, 0);\n return warpedCanvas.toDataURL({ format: 'png', multiplier: 1 });\n } catch (error) {\n throw new Error('Failed to apply mockup displacement map', { cause: error });\n } finally {\n mapCanvas.dispose();\n warpedCanvas.dispose();\n }\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 = mockup.displacement\n ? await displacedDesignUrl(\n designCanvas,\n mockup.displacement,\n state.canvas.width,\n state.canvas.height,\n )\n : 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":["import_fabric","import_fabric","import_fabric","response","renderEditorState"]}
|
|
1
|
+
{"version":3,"sources":["../src/displacement.ts","../src/export.ts","../src/text-fit.ts","../src/text-wrap-split.ts","../src/masks/space.ts","../src/text-wrap.ts","../src/print.ts","../src/node.ts"],"sourcesContent":["import type { MockupDisplacement, MockupDisplacementChannel } from './types';\n\nconst CHANNEL_INDEX: Record<MockupDisplacementChannel, number> = {\n red: 0,\n green: 1,\n blue: 2,\n alpha: 3,\n};\n\nfunction finiteScale(value: number | undefined, fallback: number, label: string): number {\n const resolved = value ?? fallback;\n if (!Number.isFinite(resolved)) throw new Error(`${label} must be finite`);\n return resolved;\n}\n\nfunction sample(\n source: Uint8ClampedArray,\n width: number,\n height: number,\n x: number,\n y: number,\n channel: number,\n): number {\n const clampedX = Math.max(0, Math.min(width - 1, x));\n const clampedY = Math.max(0, Math.min(height - 1, y));\n const x0 = Math.floor(clampedX);\n const y0 = Math.floor(clampedY);\n const x1 = Math.min(width - 1, x0 + 1);\n const y1 = Math.min(height - 1, y0 + 1);\n const tx = clampedX - x0;\n const ty = clampedY - y0;\n const top =\n source[(y0 * width + x0) * 4 + channel] * (1 - tx) +\n source[(y0 * width + x1) * 4 + channel] * tx;\n const bottom =\n source[(y1 * width + x0) * 4 + channel] * (1 - tx) +\n source[(y1 * width + x1) * 4 + channel] * tx;\n return top * (1 - ty) + bottom * ty;\n}\n\n/**\n * Warp RGBA pixels with an equally sized channel map. A channel value of 128\n * is neutral; 0 and 255 move by the configured negative/positive maximum.\n */\nexport function displaceRgba(\n source: Uint8ClampedArray,\n map: Uint8ClampedArray,\n width: number,\n height: number,\n options: Omit<MockupDisplacement, 'image'>,\n): Uint8ClampedArray {\n if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) {\n throw new Error('Displacement dimensions must be positive integers');\n }\n const expectedLength = width * height * 4;\n if (source.length !== expectedLength || map.length !== expectedLength) {\n throw new Error('Displacement source and map must match the requested dimensions');\n }\n\n const scaleX = finiteScale(options.scaleX, 10, 'Displacement scaleX');\n const scaleY = finiteScale(options.scaleY, 10, 'Displacement scaleY');\n const channelX = CHANNEL_INDEX[options.channelX ?? 'red'];\n const channelY = CHANNEL_INDEX[options.channelY ?? 'green'];\n const output = new Uint8ClampedArray(expectedLength);\n\n for (let y = 0; y < height; y += 1) {\n for (let x = 0; x < width; x += 1) {\n const offset = (y * width + x) * 4;\n const sourceX = x + ((map[offset + channelX] - 128) / 127) * scaleX;\n const sourceY = y + ((map[offset + channelY] - 128) / 127) * scaleY;\n for (let channel = 0; channel < 4; channel += 1) {\n output[offset + channel] = Math.round(\n sample(source, width, height, sourceX, sourceY, channel),\n );\n }\n }\n }\n return output;\n}\n","import { StaticCanvas } from 'fabric';\nimport type { Canvas, FabricObject, ImageFormat } from 'fabric';\nimport type { MockupConfig, MockupPrintArea } from './types';\nimport { displaceRgba } from './displacement';\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/** Render cloned objects without mutating the live editor canvas. */\nexport async function exportIsolatedPNG(\n source: Canvas,\n objects: FabricObject[],\n options: PngExportOptions & {\n width?: number;\n height?: number;\n backgroundColor?: string;\n backgroundImage?: FabricObject | null;\n cloneObjects?: boolean;\n } = {},\n): Promise<Blob> {\n const element = source.lowerCanvasEl.ownerDocument.createElement('canvas');\n const canvas = new StaticCanvas(element, {\n width: options.width ?? source.getWidth(),\n height: options.height ?? source.getHeight(),\n backgroundColor: options.backgroundColor || undefined,\n });\n try {\n const clones =\n options.cloneObjects === false\n ? objects\n : await Promise.all(objects.map((object) => object.clone()));\n if (clones.length) canvas.add(...clones);\n if (options.backgroundImage) canvas.backgroundImage = await options.backgroundImage.clone();\n canvas.requestRenderAll();\n // Awaited, not returned: `finally` would otherwise dispose the canvas while\n // the export is still reading from it.\n return await exportPNG(canvas as unknown as Canvas, options);\n } finally {\n canvas.dispose();\n }\n}\n\n/**\n * Render just the print-area rectangle, on transparency.\n *\n * This is the file a print provider receives: the design alone, cropped to the\n * printable rectangle, with no garment behind it and no canvas background baked\n * in — so it is rendered from cloned objects rather than off the live canvas.\n */\nexport async function exportPrintArea(\n source: Canvas,\n area: MockupPrintArea,\n options: PngExportOptions = {},\n): Promise<Blob> {\n const { multiplier = 1, format = 'png' as ImageFormat, quality = 1 } = options;\n const width = source.getWidth();\n const height = source.getHeight();\n const clip = computePrintAreaClip(\n area,\n multiplier,\n multiplier,\n width * multiplier,\n height * multiplier,\n );\n if (clip.width <= 0 || clip.height <= 0) {\n throw new Error('Print area does not overlap the canvas');\n }\n\n const element = source.lowerCanvasEl.ownerDocument.createElement('canvas');\n const canvas = new StaticCanvas(element, { width, height });\n try {\n const clones = await Promise.all(source.getObjects().map((object) => object.clone()));\n if (clones.length) canvas.add(...clones);\n canvas.requestRenderAll();\n const rendered = canvas.toCanvasElement(multiplier);\n const output = rendered.ownerDocument.createElement('canvas');\n output.width = Math.max(1, Math.round(clip.width));\n output.height = Math.max(1, Math.round(clip.height));\n const context = output.getContext('2d');\n if (!context) throw new Error('2D canvas context is unavailable');\n context.drawImage(rendered, -clip.left, -clip.top);\n // Awaited, not returned: `finally` would dispose the canvas mid-read.\n return await canvasElementToBlob(output, format, quality);\n } finally {\n canvas.dispose();\n }\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 = (\n image: HTMLImageElement,\n targetContext: CanvasRenderingContext2D = context,\n ) => {\n const placement = computeCoverPlacement(\n image.naturalWidth || image.width,\n image.naturalHeight || image.height,\n output.width,\n output.height,\n );\n targetContext.drawImage(\n image,\n placement.left,\n placement.top,\n placement.width,\n placement.height,\n );\n };\n\n // Full-size scratch buffers, released explicitly in the finally below: a\n // detached canvas element can hold its backing store well past its last\n // reference, and a 4K mockup allocates three of them per export.\n const scratch: HTMLCanvasElement[] = [design];\n try {\n drawCover(await loadImage(mockup.image));\n let compositedDesign: CanvasImageSource = design;\n if (mockup.displacement) {\n const sourceContext = design.getContext('2d');\n if (!sourceContext) throw new Error('2D design context is unavailable');\n const mapCanvas = design.ownerDocument.createElement('canvas');\n scratch.push(mapCanvas);\n mapCanvas.width = design.width;\n mapCanvas.height = design.height;\n const mapContext = mapCanvas.getContext('2d');\n if (!mapContext) throw new Error('2D displacement-map context is unavailable');\n drawCover(await loadImage(mockup.displacement.image), mapContext);\n\n const warped = design.ownerDocument.createElement('canvas');\n scratch.push(warped);\n warped.width = design.width;\n warped.height = design.height;\n const warpedContext = warped.getContext('2d');\n if (!warpedContext) throw new Error('2D displaced-design context is unavailable');\n let sourcePixels: Uint8ClampedArray;\n let mapPixels: Uint8ClampedArray;\n try {\n sourcePixels = sourceContext.getImageData(0, 0, design.width, design.height).data;\n mapPixels = mapContext.getImageData(0, 0, design.width, design.height).data;\n } catch (error) {\n throw new Error('Failed to apply mockup displacement map; verify image CORS access', {\n cause: error,\n });\n }\n const pixels = displaceRgba(sourcePixels, mapPixels, design.width, design.height, {\n ...mockup.displacement,\n scaleX: (mockup.displacement.scaleX ?? 10) * multiplier,\n scaleY: (mockup.displacement.scaleY ?? 10) * multiplier,\n });\n const imageData = warpedContext.createImageData(design.width, design.height);\n imageData.data.set(pixels);\n warpedContext.putImageData(imageData, 0, 0);\n compositedDesign = warped;\n }\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(compositedDesign, 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 await canvasElementToBlob(output, format, quality);\n } finally {\n for (const element of scratch) {\n element.width = 0;\n element.height = 0;\n }\n }\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","import { Point } from 'fabric';\nimport type { FabricObject } from 'fabric';\n\n/**\n * Fitting a text layer's box to the letters inside it.\n *\n * A `Textbox`'s `width` is a **wrap width** somebody authored, not the width of\n * the glyphs, and fabric is explicit that it stays that way: \"Unlike superclass's\n * version of this function, Textbox does not update its width.\" So the box only\n * ever ratchets *up*, and only when a word physically cannot fit — raise a 48px\n * font to 96px inside a 200px box and the selection frame does not move, and\n * dropping the size back leaves the box at whatever width it grew to.\n *\n * Two things need the same measurement: this, and the pattern engine (a tile\n * stepped by the box rather than the run leaves transparent padding between\n * every repeat).\n */\n\n/**\n * Slack left when a box is measured down to its run, so a float rounding error\n * cannot push the widest line into a new wrap on the next layout pass.\n */\nexport const TEXT_FIT_SLACK = 0.5;\n\n/** Box width used to measure a run that is not allowed to wrap. */\nconst MEASURE_WIDTH = 100_000;\n\n/** How far each attempt overshoots while bracketing a width that does not wrap. */\nconst GROWTH_FACTOR = 1.1;\n\n/**\n * Caps on the widening search. A run whose layout never settles has to exit and\n * leave the box a little wide, not spin — this runs on the typing path.\n */\nconst GROWTH_TRIES = 8;\nconst SEARCH_TRIES = 24;\n\n/** The text-shaped surface of an object whose box can be wider than its art. */\nexport interface TextSource extends FabricObject {\n text: string;\n width: number;\n textAlign?: string;\n calcTextWidth?: () => number;\n initDimensions?: () => void;\n /** Lines as laid out. More of them than the author typed means a soft wrap. */\n _textLines?: unknown[];\n /** Set while the run follows a curve, which owns the box instead. */\n path?: FabricObject | null;\n}\n\n/** Width the run wants on one line, measured against a box wide enough not to wrap. */\nfunction unwrappedWidth(text: TextSource): number {\n const authored = text.width;\n try {\n text.set({ width: MEASURE_WIDTH });\n text.initDimensions?.();\n const measured = text.calcTextWidth?.() ?? authored;\n return Number.isFinite(measured) && measured > 0 ? measured : authored;\n } finally {\n text.set({ width: authored });\n text.initDimensions?.();\n }\n}\n\n/** The object as text, or null. Only text carries a box wider than what it paints. */\nexport function asText(object: FabricObject | null | undefined): TextSource | null {\n if (!object) return null;\n const text = object as TextSource;\n return typeof text.text === 'string' && typeof text.calcTextWidth === 'function' ? text : null;\n}\n\n/**\n * What a text object paints inside its box, unscaled: the run's width, and how\n * far the run's centre sits from the box's.\n *\n * `calcTextWidth` reports the widest wrapped line — the run's real footprint —\n * and `textAlign` says where in the box that run sits.\n */\nexport function textInk(text: TextSource): { width: number; dx: number } {\n const boxWidth = Math.max(0, text.width ?? 0);\n const measured = text.calcTextWidth?.() ?? boxWidth;\n const width = Math.max(1, Math.min(boxWidth, Number.isFinite(measured) ? measured : boxWidth));\n const slack = (boxWidth - width) / 2;\n const align = text.textAlign ?? 'left';\n if (align.includes('center')) return { width, dx: 0 };\n const flip = text.flipX ? -1 : 1;\n // Right-aligned text hugs the right edge, so its centre is right of the box's;\n // left and justify both start at the left edge.\n return { width, dx: flip * (align.includes('right') ? slack : -slack) };\n}\n\n/** Lay the run out at `width` and report whether anything soft-wrapped. */\nfunction wrapsAt(text: TextSource, width: number): boolean {\n text.set({ width });\n text.initDimensions?.();\n // Newlines the author typed are lines fabric must produce; anything beyond\n // that count is the box breaking the run on its own.\n const authored = text.text.split('\\n').length;\n return (text._textLines?.length ?? 0) > authored;\n}\n\n/**\n * Narrowest width from `fitted` up that the run does not soft-wrap in, leaving\n * the box laid out at it.\n *\n * `calcTextWidth` reports what a line *renders* at, with its trailing space\n * trimmed, while fabric decides where to wrap with the infix spaces counted —\n * so a box fitted to the rendered width can be a hair too narrow and break the\n * run anyway. The gap grows with the number of spaces and with `charSpacing`,\n * so no constant slack covers it; the width fabric agrees to is searched for\n * instead of guessed.\n */\nfunction widenPastSoftWrap(text: TextSource, fitted: number): number {\n // The common case, and the only pass a single-word run or a box that already\n // holds its text ever pays — this runs on every keystroke.\n if (!wrapsAt(text, fitted)) return fitted;\n\n // Bracket a width that holds the run, then close in on the narrowest one.\n let low = fitted;\n let high = fitted;\n let bracketed = false;\n for (let tries = 0; tries < GROWTH_TRIES; tries += 1) {\n low = high;\n high = high * GROWTH_FACTOR + 1;\n if (!wrapsAt(text, high)) {\n bracketed = true;\n break;\n }\n }\n // Still wrapping at the widest width tried: take it rather than keep going.\n if (!bracketed) return high;\n\n for (let tries = 0; tries < SEARCH_TRIES && high - low > TEXT_FIT_SLACK; tries += 1) {\n const mid = (low + high) / 2;\n if (wrapsAt(text, mid)) low = mid;\n else high = mid;\n }\n // `high` is the bound known to hold the run; `low` is known to break it.\n if (text.width !== high) wrapsAt(text, high);\n return high;\n}\n\n/**\n * Put the box back at `width` after a probe was abandoned partway through.\n *\n * The restore's own failure is dropped rather than thrown: it runs while an\n * exception is already on its way out, and replacing that one with this one\n * would bury the fault that actually broke the layout.\n */\nfunction restoreWidth(text: TextSource, width: number): void {\n try {\n text.set({ width });\n text.initDimensions?.();\n } catch {\n // Nothing better to offer than the width itself, which is set either way.\n }\n}\n\n/**\n * Whether the box is already the narrowest width that holds its run, and so has\n * nothing to search for.\n *\n * Worth asking because {@link widenPastSoftWrap} settles a hair ABOVE the width\n * `calcTextWidth` reports, which is the width the cheap check below compares\n * against — so a settled multi-word box looks perpetually a hair off and would\n * re-run the whole search on every keystroke, deriving the width it already has.\n *\n * Both halves are load-bearing. Holding the run alone would call any box that\n * fits \"settled\", and a box left far too wide by a font-size drop would then\n * never shrink; breaking a slack narrower is what says it is not also too wide.\n *\n * Leaves the box laid out at `width` either way.\n */\nfunction isSettled(text: TextSource, width: number): boolean {\n // Already wrapping: not settled, and `wrapsAt` has left the box at `width`.\n if (wrapsAt(text, width)) return false;\n const tight = wrapsAt(text, width - TEXT_FIT_SLACK);\n wrapsAt(text, width);\n return tight;\n}\n\n/**\n * Fit a text layer's box to the run it holds, leaving the letters where they\n * were on screen.\n *\n * The box becomes **auto-width**: it follows the run in both directions, so\n * raising the font size or opening up the letter spacing widens the frame\n * instead of silently breaking the line, and lowering them again takes the width\n * back — which fabric never does on its own, since `Textbox` only ratchets its\n * width up and only when a word cannot fit at all.\n *\n * Line breaks stay under the author's control through the text itself: the run\n * is measured per hard newline, so `One\\nTwo` stays two lines. What goes away is\n * *soft* wrapping, which in an editor whose text tool opens at a fixed 200px box\n * was mostly accidental anyway.\n *\n * Curved text is skipped — `TextCurveManager` owns that box and sizes it to the\n * glyphs along the path, which this would fight.\n *\n * Returns whether anything changed, so a caller can skip a history checkpoint.\n */\nexport function fitTextWidth(object: FabricObject | null | undefined): boolean {\n const text = asText(object);\n if (!text || text.path) return false;\n\n const authored = text.width;\n try {\n const before = textInk(text);\n const fitted = unwrappedWidth(text) + TEXT_FIT_SLACK;\n // A hair either way is the slack itself, not an edit worth a history step.\n if (Math.abs(fitted - authored) < TEXT_FIT_SLACK) return false;\n\n // Only a box sitting just above the naive fit can be the settled one, since\n // the search never lands past its first bracket step. Screening on that costs\n // no layout at all and skips the probes for a box that is simply the wrong\n // size — including every single-word run, which can never soft-wrap and so\n // could never have come back settled.\n const nearFit = authored > fitted && authored <= fitted * GROWTH_FACTOR + 1;\n if (nearFit && isSettled(text, authored)) return false;\n\n // Keep the run put: shrinking the box moves its centre, and with it every\n // alignment except centre. The ink offset is what that move has to cancel.\n const centre = text.getCenterPoint();\n const settled = widenPastSoftWrap(text, fitted);\n const after = textInk(text);\n const shift = (before.dx - after.dx) * (text.scaleX ?? 1);\n const radians = ((text.angle ?? 0) * Math.PI) / 180;\n const moved = new Point(\n centre.x + shift * Math.cos(radians),\n centre.y + shift * Math.sin(radians),\n );\n text.setPositionByOrigin(moved, 'center', 'center');\n text.setCoords();\n text.dirty = true;\n // Measured against where the box actually settled, not where the first guess\n // put it: widening past a soft wrap can land back on the authored width, and\n // reporting that as a change would checkpoint an edit that never happened.\n return Math.abs(settled - authored) >= TEXT_FIT_SLACK;\n } catch (error) {\n // Every probe above leaves the box at a trial width and undoes it on the\n // next line; a throw in between strands it there — narrower than its run,\n // which is the soft wrap this file exists to keep out. Same discipline as\n // `unwrappedWidth`'s `finally`, applied to the searches it feeds.\n restoreWidth(text, authored);\n throw error;\n }\n}\n","/**\n * Word splitting for `wrap: 'pre-wrap'`.\n *\n * Fabric splits a line on `/[ \\t\\r]/` and rejoins the pieces with a single\n * `' '`, then suppresses that space at a soft break — which is why a wrapped\n * continuation line loses the indentation the author typed. Attaching each\n * run of whitespace to the word that FOLLOWS it moves that whitespace inside a\n * token, where nothing can drop it. One character is held back per token\n * because fabric re-inserts exactly one space between tokens.\n *\n * Pure and fabric-free on purpose: the API mirrors this function against\n * `fabric/node` for the print renderer, and the two are locked together by\n * matching fixture tables in both repos.\n */\nconst WHITESPACE = /[ \\t\\r]/;\n\nexport function preWrapWordSplit(value: string): string[] {\n const tokens: string[] = [];\n let index = 0;\n let first = true;\n\n while (index < value.length) {\n let space = '';\n while (index < value.length && WHITESPACE.test(value[index])) {\n space += value[index];\n index += 1;\n }\n let word = '';\n while (index < value.length && !WHITESPACE.test(value[index])) {\n word += value[index];\n index += 1;\n }\n // Nothing precedes the first token, so it keeps every space it was given.\n tokens.push((first ? space : space.slice(1)) + word);\n first = false;\n }\n\n return tokens.length > 0 ? tokens : [''];\n}\n","import { util } from 'fabric';\nimport type { FabricObject, Group, TMat2D } from 'fabric';\n\n/**\n * Coordinate-space plumbing for mask stacks.\n *\n * A fabric `clipPath` lives in one of two spaces, and the mask stack uses both:\n *\n * - **host space** (`absolutePositioned: false`) — the clip is drawn inside the\n * host object's own transform, so it follows every move, scale and rotation\n * of the layer for free. This is what a *linked* mask wants.\n * - **canvas space** (`absolutePositioned: true`) — the clip ignores the host's\n * transform, so the artwork slides underneath a mask that stays put. This is\n * what an *unlinked* mask wants.\n *\n * A stack composes into a single group, so one unlinked mask forces the whole\n * stack into canvas space; the linked entries are then re-fitted from the host's\n * transform. `toCanvasSpace` / `toHostSpace` are the conversions that migration\n * between the two regimes needs, and they are exact for scale, rotation and skew\n * because they compose matrices rather than copying left/top.\n */\n\nexport function matrixOf(object: FabricObject): TMat2D {\n return object.calcTransformMatrix();\n}\n\n/** Overwrite an object's transform with `matrix`, keeping its own dimensions. */\nexport function applyMatrix(object: FabricObject, matrix: TMat2D): void {\n const decomposed = util.qrDecompose(matrix);\n object.set({\n flipX: false,\n flipY: false,\n originX: 'center',\n originY: 'center',\n left: decomposed.translateX,\n top: decomposed.translateY,\n scaleX: decomposed.scaleX,\n scaleY: decomposed.scaleY,\n angle: decomposed.angle,\n skewX: decomposed.skewX,\n skewY: 0,\n });\n object.setCoords();\n}\n\n/** Host-space geometry → canvas-space, for the same on-screen result. */\nexport function toCanvasSpace(object: FabricObject, host: FabricObject): void {\n applyMatrix(object, util.multiplyTransformMatrices(matrixOf(host), matrixOf(object)));\n}\n\n/** Canvas-space geometry → host-space, for the same on-screen result. */\nexport function toHostSpace(object: FabricObject, host: FabricObject): void {\n applyMatrix(\n object,\n util.multiplyTransformMatrices(util.invertTransform(matrixOf(host)), matrixOf(object)),\n );\n}\n\n/**\n * The mask's transform expressed relative to its host, so a linked mask can be\n * re-derived after the host moves. Stored on the entry, not recomputed from the\n * live objects: by the time the host has moved, the old relationship is gone.\n */\nexport function relativeMatrix(object: FabricObject, host: FabricObject): TMat2D {\n return util.multiplyTransformMatrices(util.invertTransform(matrixOf(host)), matrixOf(object));\n}\n\n/** Re-place a linked mask from the host's current transform and a stored `rel`. */\nexport function applyRelativeMatrix(object: FabricObject, host: FabricObject, rel: TMat2D): void {\n applyMatrix(object, util.multiplyTransformMatrices(matrixOf(host), rel));\n}\n\n/**\n * Fabric types `clipPath` with a looser prop set than the objects the editor\n * builds, so reading a clip back out needs a narrowing step. Every clip this\n * module reads is one it composed from real objects in the first place.\n */\nexport function asObject(clip: NonNullable<FabricObject['clipPath']>): FabricObject {\n return clip as FabricObject;\n}\n\n/** Narrow a persisted `rel` array back to a transform matrix. */\nexport function toMatrix(values: number[] | undefined): TMat2D | null {\n if (!values || values.length !== 6 || values.some((value) => !Number.isFinite(value)))\n return null;\n return [values[0], values[1], values[2], values[3], values[4], values[5]];\n}\n\n/** Scale an object so its unrotated box covers `box`, centred on it. */\nexport function fitToBox(\n object: FabricObject,\n box: { left: number; top: number; width: number; height: number },\n zoom = 1,\n): void {\n const width = Math.max(1, box.width) * zoom;\n const height = Math.max(1, box.height) * zoom;\n object.set({\n originX: 'center',\n originY: 'center',\n angle: 0,\n skewX: 0,\n skewY: 0,\n left: box.left + box.width / 2,\n top: box.top + box.height / 2,\n scaleX: width / Math.max(1, object.width ?? 1),\n scaleY: height / Math.max(1, object.height ?? 1),\n });\n object.setCoords();\n}\n\n/**\n * Pull a composed clip group back apart into standalone objects in the space the\n * group itself was in.\n *\n * `removeAll` already restores each child's own transform on the way out — the\n * group rebases children when it takes them in, and reverses that when it lets\n * them go. Folding the group's matrix back in on top (as one would with a v5-era\n * group) double-counts it, which stays invisible while a mask sits at the origin\n * and moves it twice as far as it should the moment one does not.\n *\n * The group passed in is emptied.\n */\nexport function unwrapGroup(group: Group): FabricObject[] {\n const children = group.removeAll();\n for (const child of children) child.setCoords();\n return children;\n}\n","import { Rect } from 'fabric';\nimport type { Canvas, FabricObject } from 'fabric';\nimport type { Layer, LayerManager } from './layer';\nimport type { HistoryManager } from './history';\nimport type { EventEmitter } from './events';\nimport type { EditorEvents, LayerMeta, TextOverflow, TextWrapMode } from './types';\nimport { asText, fitTextWidth, type TextSource } from './text-fit';\nimport { preWrapWordSplit } from './text-wrap-split';\nimport { asObject, toHostSpace } from './masks/space';\n\n/** Both properties as they apply to a layer, defaults filled in. */\nexport interface TextWrapState {\n wrap: TextWrapMode;\n overflow: TextOverflow;\n}\n\n/** A Textbox, plus the wrapping levers fabric does not put on FabricObject. */\ninterface WrappableText extends TextSource {\n splitByGrapheme?: boolean;\n wordSplit?: (value: string) => string[];\n}\n\n/**\n * Fabric names `wordSplit` as an override point, so pre-wrap is an own-property\n * shadow of the prototype method rather than a patched prototype: two layers on\n * one canvas can be in different modes.\n */\nfunction patchWordSplit(text: WrappableText): void {\n if (Object.prototype.hasOwnProperty.call(text, 'wordSplit')) return;\n Object.defineProperty(text, 'wordSplit', {\n value: preWrapWordSplit,\n configurable: true,\n writable: true,\n });\n}\n\nfunction unpatchWordSplit(text: WrappableText): void {\n if (Object.prototype.hasOwnProperty.call(text, 'wordSplit')) {\n delete text.wordSplit;\n }\n}\n\nexport function readTextWrap(meta: LayerMeta | undefined): TextWrapState {\n return { wrap: meta?.wrap ?? 'none', overflow: meta?.overflow ?? 'visible' };\n}\n\n/** Builds the clip rect. A parameter because a Node renderer's `Rect` (from\n * `fabric/node`) and the browser's `Rect` (from `fabric`) are different\n * classes — a clip built by one does not render on the other's canvas. */\ntype RectFactory = (options: Record<string, unknown>) => FabricObject;\n\n/**\n * The box, as a clip, in the layer's own frame.\n *\n * A top-level clipPath's coordinates are the object's own, centred on it and\n * unaffected by its scale, so this is sized to the unscaled dimensions — the\n * same rule `MaskPresetManager.buildClip` follows. That invariant holds only at\n * the top level: nested under another clip the frame is the parent's, and\n * `derive` converts into it. Height is the text height, which IS the content\n * height for a Textbox, so only the width ever clips anything.\n *\n * A fresh Rect per derive, and derive runs on the typing path — cheap, since\n * `objectCaching: false` means there is no backing canvas to retain. If it ever\n * shows up in a profile, the move is to resize the clip already installed\n * rather than to build a second one.\n */\nfunction boxClip(text: WrappableText, makeRect: RectFactory): FabricObject {\n return makeRect({\n width: Math.max(1, text.width ?? 1),\n height: Math.max(1, text.height ?? 1),\n originX: 'center',\n originY: 'center',\n left: 0,\n top: 0,\n objectCaching: false,\n });\n}\n\n/**\n * Does a mask already own this layer's clipPath? Read from meta, not the object.\n *\n * Deliberately blind to `PatternManager`, which strips a layer's clip for the\n * duration of a pattern: meta still says \"masked\" while the object carries no\n * clip at all, so the box clip becomes a no-op until the pattern is removed.\n * That is the safe way round — no clip beats taking a slot the pattern is using\n * — so do not \"fix\" this by asking the object what it currently holds.\n */\nfunction hasMask(meta: LayerMeta): boolean {\n return !!meta.maskPreset || (meta.maskStack?.length ?? 0) > 0;\n}\n\n/**\n * The renderer-agnostic half of {@link TextWrapManager.derive}: the fabric\n * state a wrap mode implies once the box's width is already settled.\n *\n * Deliberately does NOT touch width — re-fitting here would let a Node\n * renderer, which sees only the serialized state and never `meta.wrapWidth`,\n * shift a box the browser already fitted at authoring time. Width is the\n * manager's job (`fitTextWidth` for `'none'`, `meta.wrapWidth` restore for\n * everything else); this only sets the levers fabric does not serialize on\n * its own (`wordSplit`) or that depend on the box the manager just settled\n * (the clip). Call it AFTER any width decision, never before — the clip below\n * is sized to `text.width`/`text.height` as they stand when this runs.\n */\nexport function applyTextWrapToObject(\n object: FabricObject,\n meta: LayerMeta | undefined,\n makeRect: RectFactory = (options) => new Rect(options),\n): void {\n const text = asText(object) as WrappableText | null;\n if (!text) return;\n\n const state = readTextWrap(meta);\n\n // A curved run follows a path built for the box the curve manager sized;\n // re-deriving here would fight it. The mode stays stored and applies again\n // when the curve is cleared.\n if (!text.path) {\n text.set({ splitByGrapheme: state.wrap === 'break-all' });\n if (state.wrap === 'pre-wrap') patchWordSplit(text);\n else unpatchWordSplit(text);\n // `splitByGrapheme` is not one of fabric's `textLayoutProperties`, so the\n // `set` above did not re-lay the run out on its own, and neither does\n // patching `wordSplit` (an own-property override, not a tracked prop) —\n // a layer already at its current width, with no width change to trigger\n // fabric's own relayout, would otherwise keep lines laid out under the\n // PREVIOUS split function.\n text.initDimensions?.();\n }\n\n // Installed even on curved text: a curve owns the box, not the clip.\n const clip = state.overflow === 'hidden' ? boxClip(text, makeRect) : undefined;\n if (meta && hasMask(meta)) {\n const host = text.clipPath;\n if (host) {\n // A nested clip is drawn in its PARENT clip's frame, not the layer's:\n // fabric replays every entry in `parentClipPaths` before applying the\n // child's own transform (`Object.createClipPathLayer`). The mask group\n // lays itself out around its geometry, so that frame coincides with the\n // layer's only for a centred mask — which is why presets and default-fit\n // masks look right. Left unconverted, the box clip is displaced by the\n // whole of the mask's transform: an offset mask that covers the run\n // completely starts cutting it, and an unlinked one (composed in canvas\n // space, so the offset is the layer's full distance from the origin)\n // carries the clip clear of the text, which disappears outright.\n //\n // Re-expressing it relative to the mask is the same conversion a linked\n // mask needs when it joins a canvas-space stack, so it uses the same\n // helper rather than new matrix maths. `asObject` is the narrowing step\n // that module already carries for fabric's looser `clipPath` typing.\n if (clip) toHostSpace(clip, asObject(host));\n host.clipPath = clip;\n }\n } else {\n text.clipPath = clip;\n }\n}\n\n/**\n * How a text layer breaks its lines, and whether it paints past its box.\n *\n * `meta` is the authoring record; what actually renders — and what a print\n * pipeline that never reads `meta` sees — is the fabric state derived here:\n * `width`, `splitByGrapheme` and `clipPath`, all of which fabric serializes on\n * its own. That is the same division `TextCurveManager` draws between\n * `meta.curve` and the `path` it installs.\n */\nexport class TextWrapManager {\n /**\n * Typing changes the run the box was fitted to, so an auto-width layer has to\n * re-fit. No history entry: fabric records the edit when editing exits, and a\n * save per keystroke would bury every earlier step.\n */\n private readonly onTextChanged = (event: { target?: FabricObject }) => {\n const layer = event.target ? this.layers.findByObject(event.target) : undefined;\n if (!layer) return;\n this.refresh(layer.id, false);\n };\n\n /**\n * Both mask owners — the preset manager and every mask-stack mutation —\n * install their clip straight onto `clipPath`, dropping whatever was there,\n * and neither knows this layer had a box clip. Re-deriving on the one event\n * they both announce puts it back where it now belongs: nested under the new\n * mask, or at the top level when the last mask leaves. Waiting for the next\n * keystroke instead would leave a layer that should clip inside its box\n * serialized unclipped — which is what the print renderer reads.\n */\n private readonly onMasksChanged = ({ target }: { target: string }) => {\n // `refresh` ignores an id that is not a text layer, so the whole-design\n // mask target passes through it harmlessly.\n this.refresh(target);\n };\n\n constructor(\n private canvas: Canvas,\n private layers: LayerManager,\n private history: HistoryManager,\n private events: EventEmitter<EditorEvents>,\n ) {\n this.canvas.on('text:changed', this.onTextChanged);\n this.events.on('masks:changed', this.onMasksChanged);\n }\n\n dispose(): void {\n this.canvas.off('text:changed', this.onTextChanged);\n this.events.off('masks:changed', this.onMasksChanged);\n }\n\n /** Both properties for a text layer, or null when it is not text. */\n get(layerId: string): TextWrapState | null {\n const layer = this.layers.get(layerId);\n if (!layer || !asText(layer.fabricObject)) return null;\n return readTextWrap(layer.meta);\n }\n\n apply(layerId: string, wrap: TextWrapMode, save = true): boolean {\n const layer = this.layers.get(layerId);\n const text = layer ? asText(layer.fabricObject) : null;\n if (!layer || !text) return false;\n if (wrap === 'none') {\n // Parked before anything widens the box, so the authored width is the one\n // that comes back — and only on the way IN, or a second call would park\n // the fitted width over it.\n //\n // Never from a curved box: `TextCurveManager` widened that one to fit the\n // path and is holding the real authored width in `meta.curveWidth`. Park\n // here and uncurving later would restore the curve's width as if the\n // author had chosen it.\n const curved = !!text.path;\n if (layer.meta.wrapWidth === undefined && !curved) {\n layer.meta.wrapWidth = text.width ?? 0;\n }\n }\n layer.meta.wrap = wrap;\n return this.derive(layerId, save);\n }\n\n setOverflow(layerId: string, overflow: TextOverflow, save = true): boolean {\n const layer = this.layers.get(layerId);\n if (!layer || !asText(layer.fabricObject)) return false;\n layer.meta.overflow = overflow;\n return this.derive(layerId, save);\n }\n\n /** Back to the defaults: auto-width, unclipped. */\n clear(layerId: string, save = true): boolean {\n const layer = this.layers.get(layerId);\n if (!layer || !asText(layer.fabricObject)) return false;\n delete layer.meta.overflow;\n return this.apply(layerId, 'none', save);\n }\n\n /** Re-derive from the stored mode — after a text, font or size change. */\n refresh(layerId: string, save = false): boolean {\n const layer = this.layers.get(layerId);\n if (!layer || !asText(layer.fabricObject)) return false;\n return this.derive(layerId, save);\n }\n\n /**\n * Re-derive every text layer — used after a state restore.\n *\n * Recursive because a template inserts as a group of real child layers, and\n * text inside one would otherwise keep whatever box it was restored with.\n */\n refreshAll(): void {\n const visit = (layers: Layer[]): void => {\n for (const layer of layers) {\n if (asText(layer.fabricObject)) this.refresh(layer.id);\n if (layer.children.length > 0) visit(layer.children);\n }\n };\n visit(this.layers.getAll());\n }\n\n private derive(layerId: string, save: boolean): boolean {\n const layer = this.layers.get(layerId);\n const text = layer ? (asText(layer.fabricObject) as WrappableText | null) : null;\n if (!layer || !text) return false;\n\n const state = readTextWrap(layer.meta);\n\n // A curved run follows a path built for the box the curve manager sized;\n // re-deriving here would fight it. The mode stays stored and applies again\n // when the curve is cleared. This is the layer-dependent half of the mode:\n // `meta.wrapWidth` is a layer authoring record with no fabric equivalent,\n // and re-fitting is never something a renderer that only sees the already-\n // fitted serialized width should redo — both stay here rather than moving\n // into `applyTextWrapToObject`.\n if (!text.path) {\n if (state.wrap === 'none') {\n fitTextWidth(text);\n } else if (layer.meta.wrapWidth !== undefined) {\n text.set({ width: layer.meta.wrapWidth });\n delete layer.meta.wrapWidth;\n }\n }\n\n // The rest — split mode and the overflow clip — is object-only and shared\n // with the Node renderer, which derives it from the very same `meta` on a\n // freshly restored object with no editor around it. Called after the width\n // decision above so the clip is sized to the box that decision just chose.\n applyTextWrapToObject(text, layer.meta);\n\n text.dirty = true;\n text.setCoords();\n this.canvas.requestRenderAll();\n // A refresh reconstructs derived Fabric-only state after restore or another\n // manager replaced the clip. It is not a user edit: restore already\n // notifies React through `layers:changed`, and the manager that replaced a\n // mask already announced its own modification. Emitting here turned every\n // restore into a phantom layer write.\n if (save) {\n this.events.emit('layer:modified', { layerId });\n this.history.save();\n }\n return true;\n }\n}\n","import type { EditorState } from './types';\nimport type { NodeRenderOptions } from './node';\n\nexport interface PrintPdfOptions {\n /** Rasterization density. Defaults to the document DPI, or 300. */\n dpi?: number;\n /** Bleed on every edge, in inches. Defaults to 0.125in. */\n bleed?: number;\n /** Space outside bleed reserved for printer marks, in inches. Defaults to 0.25in. */\n marksMargin?: number;\n trimMarks?: boolean;\n registrationMarks?: boolean;\n /** Optional safe-area inset in inches for preflight warnings. */\n safeArea?: number;\n /** Sharp built-in `cmyk` profile or an absolute path to a custom ICC profile. */\n iccProfile?: string;\n /** Ceiling on the rasterized bleed image, in pixels. Defaults to 250 megapixels. */\n maxPixels?: number;\n outputConditionIdentifier?: string;\n title?: string;\n allowImageUrl?: NodeRenderOptions['allowImageUrl'];\n /** Preserve SVG shapes/text over a CMYK bleed raster. Defaults to `vector`. */\n rendering?: 'vector' | 'raster';\n /** PDFKit font registrations keyed by the exact SVG font-family name. */\n fontFiles?: Record<string, string | Uint8Array>;\n /** Convert text backed by `fontFiles` into glyph paths. Defaults to true. */\n outlineFonts?: boolean;\n}\n\nexport interface PrintPdfResult {\n format: 'pdf';\n mimeType: 'application/pdf';\n data: Uint8Array;\n standard: 'PDF/X-4';\n colourSpace: 'CMYK';\n rendering: 'vector' | 'raster';\n outlinedFonts: string[];\n unoutlinedFonts: string[];\n dpi: number;\n trimWidthPoints: number;\n trimHeightPoints: number;\n bleedPoints: number;\n warnings: string[];\n}\n\nfunction svgAttributes(source: string): Record<string, string> {\n return Object.fromEntries(\n [...source.matchAll(/([\\w:-]+)=(?:\"([^\"]*)\"|'([^']*)')/g)].map((match) => [\n match[1],\n match[2] ?? match[3] ?? '',\n ]),\n );\n}\n\nfunction decodeXmlText(source: string): string {\n return source\n .replace(/<[^>]+>/g, '')\n .replace(/&#x([\\da-f]+);/gi, (_, value: string) =>\n String.fromCodePoint(Number.parseInt(value, 16)),\n )\n .replace(/&#(\\d+);/g, (_, value: string) => String.fromCodePoint(Number(value)))\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/&/g, '&');\n}\n\nfunction escapeXmlAttribute(value: string): string {\n return value.replace(/[<>&\"']/g, (character) => `&#${character.charCodeAt(0)};`);\n}\n\nfunction fontSource(\n files: Record<string, string | Uint8Array>,\n family: string,\n bold: boolean,\n italic: boolean,\n): string | Uint8Array | undefined {\n const suffix = bold && italic ? '-BoldItalic' : bold ? '-Bold' : italic ? '-Italic' : '';\n // `family` comes from the untrusted SVG, so plain indexing would resolve\n // `__proto__`/`constructor` to inherited members and hand a non-font to fontkit.\n const own = (key: string) =>\n Object.hasOwn(files, key) ? (files[key] as string | Uint8Array) : undefined;\n return own(`${family}${suffix}`) ?? own(family);\n}\n\nasync function outlineSvgText(\n svg: string,\n files: Record<string, string | Uint8Array>,\n outlined: Set<string>,\n): Promise<string> {\n const fontkit = await import('fontkit');\n let output = '';\n let cursor = 0;\n for (const textMatch of svg.matchAll(/<text\\b([^>]*)>([\\s\\S]*?)<\\/text>/gi)) {\n const index = textMatch.index ?? 0;\n output += svg.slice(cursor, index);\n cursor = index + textMatch[0].length;\n const textAttributes = svgAttributes(textMatch[1]);\n const spans = [...textMatch[2].matchAll(/<tspan\\b([^>]*)>([\\s\\S]*?)<\\/tspan>/gi)];\n const lines = spans.length\n ? spans.map((span) => ({ attributes: svgAttributes(span[1]), text: decodeXmlText(span[2]) }))\n : [{ attributes: textAttributes, text: decodeXmlText(textMatch[2]) }];\n const resolved = lines.map((line) => {\n const attributes = { ...textAttributes, ...line.attributes };\n const family = attributes['font-family'] ?? 'sans-serif';\n const bold = /bold|[6-9]00/i.test(attributes['font-weight'] ?? '');\n const italic = /italic|oblique/i.test(attributes['font-style'] ?? '');\n return { ...line, attributes, family, source: fontSource(files, family, bold, italic) };\n });\n if (resolved.some((line) => !line.source)) {\n output += textMatch[0];\n continue;\n }\n const paths: string[] = [];\n for (const line of resolved) {\n const source = line.source!;\n const opened =\n typeof source === 'string'\n ? fontkit.openSync(source)\n : fontkit.create(Buffer.from(source.buffer, source.byteOffset, source.byteLength));\n if (!('layout' in opened)) {\n throw new Error(`Font collection requires a named face: ${line.family}`);\n }\n const size = Number.parseFloat(line.attributes['font-size'] ?? '16');\n const scale = size / opened.unitsPerEm;\n const style = line.attributes.style ?? '';\n const run = opened.layout(line.text);\n let penX = Number.parseFloat(line.attributes.x ?? '0');\n const baseline = Number.parseFloat(line.attributes.y ?? '0');\n run.glyphs.forEach((glyph, glyphIndex) => {\n const position = run.positions[glyphIndex];\n const x = penX + position.xOffset * scale;\n const y = baseline - position.yOffset * scale;\n paths.push(\n `<path d=\"${glyph.path.toSVG()}\" transform=\"translate(${x} ${y}) scale(${scale} ${-scale})\" style=\"${escapeXmlAttribute(style)}\"/>`,\n );\n penX += position.xAdvance * scale;\n });\n outlined.add(line.family);\n }\n output += `<g data-outlined-font=\"${escapeXmlAttribute([...new Set(resolved.map((line) => line.family))].join(','))}\">${paths.join('')}</g>`;\n }\n return output + svg.slice(cursor);\n}\n\nfunction rgbToCmyk([redByte, greenByte, blueByte]: [number, number, number]): [\n number,\n number,\n number,\n number,\n] {\n const red = redByte / 255;\n const green = greenByte / 255;\n const blue = blueByte / 255;\n const black = 1 - Math.max(red, green, blue);\n if (black >= 1) return [0, 0, 0, 100];\n return [\n ((1 - red - black) / (1 - black)) * 100,\n ((1 - green - black) / (1 - black)) * 100,\n ((1 - blue - black) / (1 - black)) * 100,\n black * 100,\n ];\n}\n\nasync function imageSourceBytes(\n source: string,\n allow?: (url: string) => boolean,\n): Promise<Uint8Array> {\n if (source.startsWith('data:')) {\n const response = await fetch(source);\n if (!response.ok) throw new Error('Failed to decode an embedded SVG image');\n return new Uint8Array(await response.arrayBuffer());\n }\n const permitted = allow\n ? allow(source)\n : (() => {\n try {\n return ['http:', 'https:'].includes(new URL(source).protocol);\n } catch {\n return false;\n }\n })();\n if (!permitted) throw new Error(`Blocked disallowed vector image URL: ${source}`);\n const response = await fetch(source);\n if (!response.ok) throw new Error(`Failed to load vector image: ${source}`);\n return new Uint8Array(await response.arrayBuffer());\n}\n\nasync function convertSvgImagesToCmyk(\n svg: string,\n sharp: (typeof import('sharp'))['default'],\n profile: string,\n allow?: (url: string) => boolean,\n): Promise<string> {\n const sources = new Set<string>();\n for (const image of svg.matchAll(/<image\\b[^>]*(?:xlink:href|href)=\"([^\"]+)\"[^>]*>/gi)) {\n if (image[1]) sources.add(image[1]);\n }\n let converted = svg;\n for (const source of sources) {\n const bytes = await imageSourceBytes(source, allow);\n const jpeg = await sharp(bytes)\n .flatten({ background: '#ffffff' })\n .toColourspace('cmyk')\n .withIccProfile(profile)\n .jpeg({ quality: 100, chromaSubsampling: '4:4:4' })\n .toBuffer();\n const dataUrl = `data:image/jpeg;base64,${jpeg.toString('base64')}`;\n converted = converted.split(source).join(dataUrl);\n }\n return converted;\n}\n\nasync function renderVectorOverlay(\n state: EditorState,\n options: PrintPdfOptions,\n width: number,\n height: number,\n sharp: (typeof import('sharp'))['default'],\n warnings: string[],\n outlinedFonts: Set<string>,\n unoutlinedFonts: Set<string>,\n): Promise<Uint8Array> {\n const [{ default: PDFKit }, { default: SVGtoPDF }, { renderEditorState }] = await Promise.all([\n import('pdfkit'),\n import('svg-to-pdfkit'),\n import('./node'),\n ]);\n // The bleed image is already flattened onto white. Render the trim overlay\n // against the same opaque print substrate so semi-transparent artwork does\n // not blend a second time with the raster copy beneath it.\n const rendered = await renderEditorState(\n { ...state, background: '#ffffff' },\n {\n format: 'svg',\n allowImageUrl: options.allowImageUrl,\n },\n );\n if (typeof rendered.data !== 'string') throw new Error('Vector rendering returned raster data');\n const fontFiles = options.fontFiles ?? {};\n const outlinedSvg =\n options.outlineFonts === false\n ? rendered.data\n : await outlineSvgText(rendered.data, fontFiles, outlinedFonts);\n const svg = await convertSvgImagesToCmyk(\n outlinedSvg,\n sharp,\n options.iccProfile ?? 'cmyk',\n options.allowImageUrl,\n );\n const remainingText = [...svg.matchAll(/<text\\b([^>]*)>/gi)].map((match) =>\n svgAttributes(match[1]),\n );\n for (const attributes of remainingText) {\n const family = attributes['font-family'] ?? 'sans-serif';\n unoutlinedFonts.add(family);\n const bold = /bold|[6-9]00/i.test(attributes['font-weight'] ?? '');\n const italic = /italic|oblique/i.test(attributes['font-style'] ?? '');\n if (!fontSource(fontFiles, family, bold, italic)) {\n warnings.push(\n `Font \"${family}\" used a PDF standard fallback; supply fontFiles for exact embedding`,\n );\n }\n }\n\n const document = new PDFKit({ autoFirstPage: false, compress: false, pdfVersion: '1.7' });\n for (const [name, path] of Object.entries(fontFiles)) {\n document.registerFont(name, typeof path === 'string' ? path : Buffer.from(path));\n }\n document.addPage({ size: [width, height], margin: 0 });\n SVGtoPDF(document, svg, 0, 0, {\n width,\n height,\n preserveAspectRatio: 'none',\n colorCallback: (color) => {\n const [rgb, opacity] = color;\n return [rgbToCmyk(rgb), opacity] as unknown as typeof color;\n },\n warningCallback: (warning) => warnings.push(`Vector render: ${warning}`),\n });\n\n return new Promise<Uint8Array>((resolve, reject) => {\n const chunks: Uint8Array[] = [];\n document.on('data', (chunk: Uint8Array) => chunks.push(chunk));\n document.on('error', reject);\n document.on('end', () => resolve(new Uint8Array(Buffer.concat(chunks))));\n document.end();\n });\n}\n\n/** Beyond this no printer is served, and larger values only buy an OOM. */\nconst MAX_DPI = 2400;\n/** ~24×36in at 600dpi. Bounds the raster a hostile state can ask a server for. */\nconst DEFAULT_MAX_PIXELS = 250_000_000;\n\nfunction positive(value: number, label: string): number {\n if (!Number.isFinite(value) || value <= 0) throw new Error(`${label} must be positive`);\n return value;\n}\n\n/**\n * States are browser-authored and untrusted on a server. `documentDpi` divides\n * the render multiplier, so an unvalidated fractional value (0.001) inflates the\n * rasterization by six orders of magnitude before any size check runs.\n */\nfunction boundedDpi(value: number, label: string): number {\n const dpi = positive(value, label);\n if (dpi > MAX_DPI) throw new Error(`${label} must not exceed ${MAX_DPI}`);\n if (dpi < 1) throw new Error(`${label} must be at least 1`);\n return dpi;\n}\n\nfunction nonNegative(value: number, label: string): number {\n if (!Number.isFinite(value) || value < 0) throw new Error(`${label} cannot be negative`);\n return value;\n}\n\nfunction preflightSafeArea(state: EditorState, safePixels: number): string[] {\n if (safePixels <= 0) return [];\n const right = state.canvas.width - safePixels;\n const bottom = state.canvas.height - safePixels;\n const warnings: string[] = [];\n for (const layer of state.layers) {\n if (!layer.visible) continue;\n const object = layer.fabricObject;\n const left = Number(object.left ?? 0);\n const top = Number(object.top ?? 0);\n const width = Number(object.width ?? 0) * Math.abs(Number(object.scaleX ?? 1));\n const height = Number(object.height ?? 0) * Math.abs(Number(object.scaleY ?? 1));\n if (left < safePixels || top < safePixels || left + width > right || top + height > bottom) {\n warnings.push(`Layer \"${layer.name}\" extends outside the configured safe area`);\n }\n }\n return warnings;\n}\n\n/**\n * Render an EditorState into a print-production PDF/X-4 file.\n *\n * The design is rasterized at the requested density, converted through Sharp's\n * CMYK ICC pipeline, edge-extended into bleed, and embedded with output intent,\n * trim/bleed boxes, XMP identification, and optional printer marks.\n */\nexport async function renderPrintPdf(\n state: EditorState,\n options: PrintPdfOptions = {},\n): Promise<PrintPdfResult> {\n const dpi = boundedDpi(options.dpi ?? state.canvas.dpi ?? 300, 'Print DPI');\n const documentDpi = boundedDpi(state.canvas.dpi ?? 72, 'Document DPI');\n const maxPixels = positive(options.maxPixels ?? DEFAULT_MAX_PIXELS, 'Max pixels');\n const bleedInches = nonNegative(options.bleed ?? 0.125, 'Bleed');\n const marksMarginInches = nonNegative(options.marksMargin ?? 0.25, 'Marks margin');\n const safeAreaInches = nonNegative(options.safeArea ?? 0, 'Safe area');\n const rendering = options.rendering ?? 'vector';\n const bleedPixels = Math.round(bleedInches * dpi);\n const trimWidthPoints = (state.canvas.width / documentDpi) * 72;\n const trimHeightPoints = (state.canvas.height / documentDpi) * 72;\n const bleedPoints = bleedInches * 72;\n const marksMarginPoints = marksMarginInches * 72;\n\n const scale = dpi / documentDpi;\n const outputPixels =\n Math.round(state.canvas.width * scale + bleedPixels * 2) *\n Math.round(state.canvas.height * scale + bleedPixels * 2);\n if (!Number.isFinite(outputPixels) || outputPixels > maxPixels) {\n throw new Error(\n `Print raster of ${outputPixels} pixels exceeds the ${maxPixels} pixel budget; lower the DPI or raise maxPixels`,\n );\n }\n\n const [{ default: sharp }, pdfLib] = await Promise.all([import('sharp'), import('pdf-lib')]);\n // Keep the optional print entry free of a static cycle with `node.ts`, which\n // re-exports this function as part of the Node-only public surface.\n const { renderEditorState } = await import('./node');\n const rendered = await renderEditorState(state, {\n format: 'png',\n multiplier: scale,\n allowImageUrl: options.allowImageUrl,\n });\n if (typeof rendered.data === 'string') throw new Error('Print rasterization returned SVG data');\n\n let pipeline = sharp(rendered.data).flatten({ background: '#ffffff' });\n if (bleedPixels > 0) {\n pipeline = pipeline.extend({\n top: bleedPixels,\n right: bleedPixels,\n bottom: bleedPixels,\n left: bleedPixels,\n extendWith: 'copy',\n });\n }\n const { data: cmykJpeg, info } = await pipeline\n .toColourspace('cmyk')\n .withIccProfile(options.iccProfile ?? 'cmyk')\n .withDensity(dpi)\n .jpeg({ quality: 100, chromaSubsampling: '4:4:4' })\n .toBuffer({ resolveWithObject: true });\n const metadata = await sharp(cmykJpeg).metadata();\n if (info.channels !== 4 || metadata.space !== 'cmyk' || !metadata.icc) {\n throw new Error('CMYK conversion did not produce a four-channel image with an ICC profile');\n }\n\n const { PDFDocument, PDFDict, PDFName, PDFString, cmyk } = pdfLib;\n const document = await PDFDocument.create();\n const title = options.title ?? 'Overtone Canvas Editor print export';\n document.setTitle(title);\n document.setCreator('@overtone-art/canvas-editor-core');\n document.setProducer('@overtone-art/canvas-editor-core');\n const pageWidth = trimWidthPoints + bleedPoints * 2 + marksMarginPoints * 2;\n const pageHeight = trimHeightPoints + bleedPoints * 2 + marksMarginPoints * 2;\n const page = document.addPage([pageWidth, pageHeight]);\n const image = await document.embedJpg(cmykJpeg);\n page.drawImage(image, {\n x: marksMarginPoints,\n y: marksMarginPoints,\n width: trimWidthPoints + bleedPoints * 2,\n height: trimHeightPoints + bleedPoints * 2,\n });\n\n const trimLeft = marksMarginPoints + bleedPoints;\n const trimBottom = marksMarginPoints + bleedPoints;\n const trimRight = trimLeft + trimWidthPoints;\n const trimTop = trimBottom + trimHeightPoints;\n const warnings = preflightSafeArea(state, safeAreaInches * documentDpi);\n const outlinedFonts = new Set<string>();\n const unoutlinedFonts = new Set<string>();\n if (rendering === 'vector') {\n const vectorPdf = await renderVectorOverlay(\n state,\n options,\n trimWidthPoints,\n trimHeightPoints,\n sharp,\n warnings,\n outlinedFonts,\n unoutlinedFonts,\n );\n const [vectorPage] = await document.embedPdf(vectorPdf);\n page.drawPage(vectorPage, {\n x: trimLeft,\n y: trimBottom,\n width: trimWidthPoints,\n height: trimHeightPoints,\n });\n }\n const bleedLeft = marksMarginPoints;\n const bleedBottom = marksMarginPoints;\n const bleedRight = pageWidth - marksMarginPoints;\n const bleedTop = pageHeight - marksMarginPoints;\n const context = document.context;\n page.node.set(PDFName.of('TrimBox'), context.obj([trimLeft, trimBottom, trimRight, trimTop]));\n page.node.set(\n PDFName.of('BleedBox'),\n context.obj([bleedLeft, bleedBottom, bleedRight, bleedTop]),\n );\n page.node.set(PDFName.of('ArtBox'), context.obj([trimLeft, trimBottom, trimRight, trimTop]));\n\n const markColour = cmyk(0, 0, 0, 1);\n if (options.trimMarks !== false) {\n const offset = Math.max(3, bleedPoints / 2);\n const length = Math.max(9, marksMarginPoints - 3);\n for (const x of [trimLeft, trimRight]) {\n page.drawLine({\n start: { x, y: trimBottom - offset },\n end: { x, y: trimBottom - offset - length },\n thickness: 0.5,\n color: markColour,\n });\n page.drawLine({\n start: { x, y: trimTop + offset },\n end: { x, y: trimTop + offset + length },\n thickness: 0.5,\n color: markColour,\n });\n }\n for (const y of [trimBottom, trimTop]) {\n page.drawLine({\n start: { x: trimLeft - offset, y },\n end: { x: trimLeft - offset - length, y },\n thickness: 0.5,\n color: markColour,\n });\n page.drawLine({\n start: { x: trimRight + offset, y },\n end: { x: trimRight + offset + length, y },\n thickness: 0.5,\n color: markColour,\n });\n }\n }\n if (options.registrationMarks !== false) {\n for (const [x, y] of [\n [pageWidth / 2, marksMarginPoints / 2],\n [pageWidth / 2, pageHeight - marksMarginPoints / 2],\n [marksMarginPoints / 2, pageHeight / 2],\n [pageWidth - marksMarginPoints / 2, pageHeight / 2],\n ] as Array<[number, number]>) {\n page.drawCircle({ x, y, size: 4, borderWidth: 0.5, borderColor: markColour });\n page.drawLine({\n start: { x: x - 6, y },\n end: { x: x + 6, y },\n thickness: 0.5,\n color: markColour,\n });\n page.drawLine({\n start: { x, y: y - 6 },\n end: { x, y: y + 6 },\n thickness: 0.5,\n color: markColour,\n });\n }\n }\n\n const profileStream = context.flateStream(metadata.icc, {\n N: 4,\n Alternate: PDFName.of('DeviceCMYK'),\n });\n const profileRef = context.register(profileStream);\n const outputIntent = context.obj({\n Type: PDFName.of('OutputIntent'),\n S: PDFName.of('GTS_PDFX'),\n OutputConditionIdentifier: PDFString.of(options.outputConditionIdentifier ?? 'CMYK'),\n RegistryName: PDFString.of('https://www.color.org'),\n Info: PDFString.of(options.outputConditionIdentifier ?? 'CMYK print condition'),\n DestOutputProfile: profileRef,\n });\n document.catalog.set(PDFName.of('OutputIntents'), context.obj([context.register(outputIntent)]));\n\n const now = new Date().toISOString();\n const xmp = `<?xpacket begin=\"\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>\n<x:xmpmeta xmlns:x=\"adobe:ns:meta/\"><rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n<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>\n</rdf:RDF></x:xmpmeta><?xpacket end=\"w\"?>`;\n const metadataStream = context.flateStream(new TextEncoder().encode(xmp), {\n Type: PDFName.of('Metadata'),\n Subtype: PDFName.of('XML'),\n });\n document.catalog.set(PDFName.of('Metadata'), context.register(metadataStream));\n const infoRef = context.trailerInfo.Info;\n if (infoRef) {\n const infoDict = context.lookup(infoRef, PDFDict);\n infoDict.set(PDFName.of('GTS_PDFXVersion'), PDFString.of('PDF/X-4'));\n infoDict.set(PDFName.of('Trapped'), PDFName.of('False'));\n }\n\n return {\n format: 'pdf',\n mimeType: 'application/pdf',\n data: await document.save({ useObjectStreams: false }),\n standard: 'PDF/X-4',\n colourSpace: 'CMYK',\n rendering,\n outlinedFonts: [...outlinedFonts].sort(),\n unoutlinedFonts: [...unoutlinedFonts].sort(),\n dpi,\n trimWidthPoints,\n trimHeightPoints,\n bleedPoints,\n warnings,\n };\n}\n","import { FabricImage, Rect, StaticCanvas, util } from 'fabric/node';\nimport type { FabricObject, ImageFormat } from 'fabric/node';\nimport { computeCoverPlacement, computePrintAreaClip } from './export';\nimport { displaceRgba } from './displacement';\nimport { applyTextWrapToObject } from './text-wrap';\nimport type { EditorState, MockupBlendMode, MockupConfig, MockupDisplacement } from './types';\n\nexport { renderPrintPdf } from './print';\nexport type { PrintPdfOptions, PrintPdfResult } from './print';\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 // The browser's TextWrapManager derives `wordSplit` and the overflow\n // clip live as the author types; fabric serializes neither the function\n // override nor (when a mask is present) the clip's nested, space-\n // converted placement, so a freshly enlivened object needs the same\n // derivation run once here, or the engine's own print output would\n // differ from what the editor previewed.\n applyTextWrapToObject(object, layer.meta, (options) => new Rect(options));\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\nasync function displacedDesignUrl(\n designCanvas: StaticCanvas,\n displacement: MockupDisplacement,\n width: number,\n height: number,\n): Promise<string> {\n const mapCanvas = new StaticCanvas(undefined, { width, height });\n const warpedCanvas = new StaticCanvas(undefined, { width, height });\n try {\n mapCanvas.add(await coverImage(displacement.image, width, height));\n mapCanvas.renderAll();\n const pixels = displaceRgba(\n designCanvas.getContext().getImageData(0, 0, width, height).data,\n mapCanvas.getContext().getImageData(0, 0, width, height).data,\n width,\n height,\n displacement,\n );\n const imageData = warpedCanvas.getContext().createImageData(width, height);\n imageData.data.set(pixels);\n warpedCanvas.getContext().putImageData(imageData, 0, 0);\n return warpedCanvas.toDataURL({ format: 'png', multiplier: 1 });\n } catch (error) {\n throw new Error('Failed to apply mockup displacement map', { cause: error });\n } finally {\n mapCanvas.dispose();\n warpedCanvas.dispose();\n }\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 = mockup.displacement\n ? await displacedDesignUrl(\n designCanvas,\n mockup.displacement,\n state.canvas.width,\n state.canvas.height,\n )\n : 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":["import_fabric","import_fabric","import_fabric","response","renderEditorState"]}
|
package/dist/node.mjs
CHANGED