UNPKG

merchi_product_editor

Version:

A React component for editing product images using Fabric.js

517 lines (516 loc) 33.3 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.loadPsdOntoCanvas = exports.extractPsdBaseLayer = void 0; var fabric_1 = require("fabric"); var ag_psd_1 = require("ag-psd"); var job_1 = require("./job"); /** * Extracts layers from a PSD file and renders them to a PNG * * @param psdUrl URL to the PSD file * @returns Promise that resolves to a PNG data URL */ var extractPsdBaseLayer = function (psdUrl) { return __awaiter(void 0, void 0, void 0, function () { var response, arrayBuffer, firstBytes, signature, psd, canvas, ctx, designBounds, imageData, layers, designLayer, maskLayer, contentLayers, _i, layers_1, layer, renderLayer_1, processLayer_1, _a, contentLayers_1, layer, dataUrl, blob, objectUrl_1, img_1, error_1; return __generator(this, function (_b) { switch (_b.label) { case 0: _b.trys.push([0, 3, , 4]); return [4 /*yield*/, fetch(psdUrl, { mode: 'cors', credentials: 'same-origin', headers: { 'Accept': '*/*', } })]; case 1: response = _b.sent(); if (!response.ok) { throw new Error("Failed to fetch PSD file: ".concat(response.status, " ").concat(response.statusText)); } return [4 /*yield*/, response.arrayBuffer()]; case 2: arrayBuffer = _b.sent(); firstBytes = new Uint8Array(arrayBuffer, 0, 4); signature = String.fromCharCode(firstBytes[0]) + String.fromCharCode(firstBytes[1]) + String.fromCharCode(firstBytes[2]) + String.fromCharCode(firstBytes[3]); if (signature !== '8BPS') { // TODO handle different file types console.warn('Warning: File does not have PSD signature. Got:', signature); // Continue anyway - the server might be returning a different format } try { psd = (0, ag_psd_1.readPsd)(arrayBuffer, { skipCompositeImageData: false, skipLayerImageData: false, skipThumbnail: false }); canvas = document.createElement('canvas'); canvas.width = psd.width; canvas.height = psd.height; ctx = canvas.getContext('2d'); if (!ctx) { throw new Error('Could not get canvas context'); } designBounds = void 0; // If there are no layers or children, try to use the document itself if (!psd.children || psd.children.length === 0) { if (psd.imageData) { imageData = new ImageData(new Uint8ClampedArray(psd.imageData.data), psd.width, psd.height); ctx.putImageData(imageData, 0, 0); } else { throw new Error('No layers or composite image data found in PSD'); } } else { layers = __spreadArray([], psd.children, true); designLayer = null; maskLayer = null; contentLayers = []; // First pass - identify special layers and regular content layers for (_i = 0, layers_1 = layers; _i < layers_1.length; _i++) { layer = layers_1[_i]; if (layer.name === 'Design') { designLayer = layer; // Store design layer bounds for future reference if (typeof layer.left === 'number' && typeof layer.top === 'number' && typeof layer.right === 'number' && typeof layer.bottom === 'number') { designBounds = { left: layer.left, top: layer.top, right: layer.right, bottom: layer.bottom }; } } else if (layer.name === 'Mask') { maskLayer = layer; } else { contentLayers.push(layer); } } renderLayer_1 = function (layer, ctx) { // Check if the layer has a canvas if (layer.canvas && typeof layer.left === 'number' && typeof layer.top === 'number') { // Apply layer blending and opacity ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1; // Draw this layer onto the main canvas at its position ctx.drawImage(layer.canvas, layer.left, layer.top); // Reset alpha ctx.globalAlpha = 1; } else if (layer.imageData && typeof layer.left === 'number' && typeof layer.top === 'number') { // Create a temporary canvas for this layer var layerCanvas = document.createElement('canvas'); var width = layer.right - layer.left; var height = layer.bottom - layer.top; layerCanvas.width = width; layerCanvas.height = height; var layerCtx = layerCanvas.getContext('2d'); if (layerCtx && layer.imageData) { // Create an ImageData object from the PSD image data var imageData = new ImageData(new Uint8ClampedArray(layer.imageData.data), width, height); // Put the layer's image data on this canvas layerCtx.putImageData(imageData, 0, 0); // Apply layer blending and opacity ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1; // Draw this layer onto the main canvas at its position ctx.drawImage(layerCanvas, layer.left, layer.top); // Reset alpha ctx.globalAlpha = 1; } } else { console.warn("Layer \"".concat(layer.name, "\" has no canvas or imageData")); } }; processLayer_1 = function (layer, ctx) { // Skip hidden layers if (layer.hidden) { return; } // If this is a group layer with children, process its children if (layer.children && layer.children.length > 0) { // Process children var children = __spreadArray([], layer.children, true); children.forEach(function (child) { return processLayer_1(child, ctx); }); return; } // Render the actual layer renderLayer_1(layer, ctx); }; // Render in the correct order: // 1. First render the "Design" layer (margins/boundaries) if (designLayer) { processLayer_1(designLayer, ctx); } // 2. Then render all content layers for (_a = 0, contentLayers_1 = contentLayers; _a < contentLayers_1.length; _a++) { layer = contentLayers_1[_a]; processLayer_1(layer, ctx); } // 3. Finally render the "Mask" layer on top to hide overflow if (maskLayer) { processLayer_1(maskLayer, ctx); } } dataUrl = canvas.toDataURL('image/png'); // Return both the data URL and the design bounds if found return [2 /*return*/, { dataUrl: dataUrl, designBounds: designBounds }]; } catch (parseError) { blob = new Blob([arrayBuffer], { type: response.headers.get('content-type') || 'application/octet-stream' }); objectUrl_1 = URL.createObjectURL(blob); img_1 = new Image(); return [2 /*return*/, new Promise(function (resolve, reject) { img_1.onload = function () { var canvas = document.createElement('canvas'); canvas.width = img_1.width; canvas.height = img_1.height; var ctx = canvas.getContext('2d'); if (!ctx) { reject(new Error('Could not get canvas context')); return; } ctx.drawImage(img_1, 0, 0); var dataUrl = canvas.toDataURL('image/png'); URL.revokeObjectURL(objectUrl_1); resolve({ dataUrl: dataUrl }); }; img_1.onerror = function () { URL.revokeObjectURL(objectUrl_1); reject(new Error('Could not parse PSD or load as image')); }; img_1.src = objectUrl_1; })]; } return [3 /*break*/, 4]; case 3: error_1 = _b.sent(); throw error_1; case 4: return [2 /*return*/]; } }); }); }; exports.extractPsdBaseLayer = extractPsdBaseLayer; /** * Loads a PSD file onto a fabric.js canvas * * @param canvas fabric.js Canvas instance * @param psdUrl URL to the PSD file * @param width Canvas width * @param height Canvas height * @returns Promise that resolves when the image is added to the canvas */ var loadPsdOntoCanvas = function (canvas, psdUrl, variations, savedObjects, width, height) { return __awaiter(void 0, void 0, void 0, function () { var canvasRef_1, _a, dataUrl_1, designBounds_1, error_2; return __generator(this, function (_b) { switch (_b.label) { case 0: _b.trys.push([0, 2, , 3]); canvasRef_1 = canvas; // Store the current psdUrl on the canvas to help track which operation is active canvasRef_1.psdUrl = psdUrl; return [4 /*yield*/, (0, exports.extractPsdBaseLayer)(psdUrl)]; case 1: _a = _b.sent(), dataUrl_1 = _a.dataUrl, designBounds_1 = _a.designBounds; return [2 /*return*/, new Promise(function (resolve, reject) { // Check if the canvas is still the same one that requested this operation if (!canvasRef_1 || canvasRef_1.psdUrl !== psdUrl) { console.warn('Canvas changed or disposed during PSD processing, aborting.'); resolve(); return; } fabric_1.fabric.Image.fromURL(dataUrl_1, function (img) { // Check again if the canvas is still valid if (!canvasRef_1 || canvasRef_1.psdUrl !== psdUrl) { console.warn('Canvas changed or disposed during image loading, aborting.'); resolve(); return; } if (!img || !img.width || !img.height) { reject(new Error('Failed to create fabric image from PSD')); return; } // Scale image to fit canvas var scale = Math.min(width / img.width, height / img.height); // Center the image - important for design bounds calculation var imgLeft = (width - img.width * scale) / 2; var imgTop = (height - img.height * scale) / 2; // Set image scale and position img.scaleX = scale; img.scaleY = scale; img.left = imgLeft; img.top = imgTop; img.selectable = false; img.evented = false; // Add image to canvas - check canvas validity first if (!canvasRef_1 || canvasRef_1.psdUrl !== psdUrl) { console.warn('Canvas changed or disposed before adding image, aborting.'); resolve(); return; } canvasRef_1.add(img); // If we have design bounds, store them for reference if (designBounds_1) { // Calculate scaled design bounds var scaledDesignBounds_1 = { left: imgLeft + (designBounds_1.left * scale), top: imgTop + (designBounds_1.top * scale), width: (designBounds_1.right - designBounds_1.left) * scale, height: (designBounds_1.bottom - designBounds_1.top) * scale, right: imgLeft + (designBounds_1.right * scale), bottom: imgTop + (designBounds_1.bottom * scale) }; // Check canvas validity before continuing if (!canvasRef_1 || canvasRef_1.psdUrl !== psdUrl) { console.warn('Canvas changed or disposed before adding design bounds, aborting.'); resolve(); return; } // Store on the canvas for future reference canvasRef_1.designBounds = scaledDesignBounds_1; // Add a visual indicator of the design area var boundaryRect = new fabric_1.fabric.Rect({ left: scaledDesignBounds_1.left, top: scaledDesignBounds_1.top, width: scaledDesignBounds_1.width, height: scaledDesignBounds_1.height, fill: 'transparent', stroke: 'rgba(255, 0, 0, 0.5)', strokeWidth: 1, strokeDashArray: [5, 5], selectable: false, evented: false, id: 'design-boundary-rect' }); // Check canvas validity before adding rect if (!canvasRef_1 || canvasRef_1.psdUrl !== psdUrl) { console.warn('Canvas changed or disposed before adding boundary rect, aborting.'); resolve(); return; } canvasRef_1.add(boundaryRect); // Create a clip path for the text based on design bounds var clipPath_1 = new fabric_1.fabric.Rect({ left: scaledDesignBounds_1.left, top: scaledDesignBounds_1.top, width: scaledDesignBounds_1.width, height: scaledDesignBounds_1.height, absolutePositioned: true }); // Convert variations into canvas objects which obay the design bounds here if (variations && variations.length > 0) { // Process each variation to create appropriate canvas objects variations.forEach(function (variation) { if (!variation) { return; // Skip invalid variations } // Get common positioning within design bounds var objectLeft = scaledDesignBounds_1.left + scaledDesignBounds_1.width / 2; var objectTop = scaledDesignBounds_1.top + scaledDesignBounds_1.height / 2; // Extract the field type and value from the variation var canvasObjectType = variation.canvasObjectType, file = variation.file, fieldId = variation.fieldId, fontFamily = variation.fontFamily, fontSize = variation.fontSize; var value = variation.value; // Check if this variation exists in savedObjects var savedObject = savedObjects.find(function (obj) { return obj.fieldId === fieldId; }); // Create canvas var canvasObject; // Handle text variations if (canvasObjectType === 'text') { // Create the text object with either saved or defaults if (savedObject) { // Create text with default first canvasObject = new fabric_1.fabric.IText((value === null || value === void 0 ? void 0 : value.toString()) || 'Text', __assign(__assign({}, savedObject), { left: savedObject.left || objectLeft, top: savedObject.top || objectTop, fontFamily: savedObject.fontFamily || fontFamily, fontSize: savedObject.fontSize || fontSize, fill: savedObject.fill || '#000000', originX: savedObject.originX || 'center', originY: savedObject.originY || 'center', selectable: true, editable: true, text: value === null || value === void 0 ? void 0 : value.toString(), scaleX: savedObject.scaleX || 1, scaleY: savedObject.scaleY || 1, angle: savedObject.angle || 0 })); } else { // Create with default properties canvasObject = new fabric_1.fabric.IText((value === null || value === void 0 ? void 0 : value.toString()) || 'Text', { left: objectLeft, top: objectTop, fontFamily: fontFamily, fontSize: fontSize, fill: '#000000', originX: 'center', originY: 'center', selectable: true, editable: false, text: value === null || value === void 0 ? void 0 : value.toString(), }); } // Store fieldId as a custom property canvasObject.fieldId = fieldId; // Store uniqueId if it exists in the saved object canvasObject.uniqueId = (savedObject === null || savedObject === void 0 ? void 0 : savedObject.uniqueId) || (0, job_1.generateUniqueId)(); // Apply the clip path to the text // Omit clip path for now to restore movability canvasObject.clipPath = clipPath_1; canvasRef_1.add(canvasObject); } // Handle image variations else if (canvasObjectType === "image") { // Get the file ID for comparison with saved objects var fileId_1 = file === null || file === void 0 ? void 0 : file.id; // Find by both fieldId and fileId for images var savedImageObject_1 = fileId_1 ? savedObjects.find(function (obj) { return obj.fieldId === fieldId && obj.fileId === fileId_1; }) : savedObject; if (file && file.viewUrl) { fabric_1.fabric.Image.fromURL(file.viewUrl, function (img) { if (!img) return; // Default scaling to fit design bounds var maxWidth = scaledDesignBounds_1.width * 0.8; var maxHeight = scaledDesignBounds_1.height * 0.8; var defaultScale = Math.min(maxWidth / img.width, maxHeight / img.height, 1); if (savedImageObject_1) { // Apply saved properties img.set(__assign(__assign({}, savedImageObject_1), { left: savedImageObject_1.left || objectLeft, top: savedImageObject_1.top || objectTop, scaleX: savedImageObject_1.scaleX || defaultScale, scaleY: savedImageObject_1.scaleY || defaultScale, angle: savedImageObject_1.angle || 0, originX: savedImageObject_1.originX || 'center', originY: savedImageObject_1.originY || 'center', cornerSize: 8, borderColor: '#303DBF', cornerColor: '#303DBF', cornerStrokeColor: '#303DBF', transparentCorners: false, selectable: true, crossOrigin: 'anonymous' })); // Store uniqueId if it exists img.uniqueId = (savedImageObject_1 === null || savedImageObject_1 === void 0 ? void 0 : savedImageObject_1.uniqueId) || (0, job_1.generateUniqueId)(); } else { // Apply default properties img.scale(defaultScale); img.set({ left: objectLeft, top: objectTop, originX: 'center', originY: 'center', cornerSize: 8, borderColor: '#303DBF', cornerColor: '#303DBF', cornerStrokeColor: '#303DBF', transparentCorners: false, selectable: true, crossOrigin: 'anonymous', }); } // Store fieldId and fileId as custom properties img.fieldId = fieldId; if (fileId_1) { img.fileId = fileId_1; } img.uniqueId = (0, job_1.generateUniqueId)(); // Apply clip path var imageClipPath = new fabric_1.fabric.Rect({ left: scaledDesignBounds_1.left, top: scaledDesignBounds_1.top, width: scaledDesignBounds_1.width, height: scaledDesignBounds_1.height, absolutePositioned: true }); img.clipPath = imageClipPath; // Check canvas validity before adding if (!canvasRef_1 || canvasRef_1.psdUrl !== psdUrl) { return; } canvasRef_1.add(img); canvasRef_1.renderAll(); }, { crossOrigin: 'anonymous' }); } } }); } // Check canvas validity before adding text if (!canvasRef_1 || canvasRef_1.psdUrl !== psdUrl) { console.warn('Canvas changed or disposed before adding test text, aborting.'); resolve(); return; } } // Final check before renderAll if (!canvasRef_1 || canvasRef_1.psdUrl !== psdUrl) { console.warn('Canvas changed or disposed before final render, aborting.'); resolve(); return; } // Try to render the canvas, with error handling try { if (canvasRef_1 && canvasRef_1.renderAll) { canvasRef_1.renderAll(); } } catch (e) { console.error('Error rendering canvas:', e); } resolve(); }, { crossOrigin: 'anonymous' }); })]; case 2: error_2 = _b.sent(); console.error('Error loading PSD onto canvas:', error_2); throw error_2; case 3: return [2 /*return*/]; } }); }); }; exports.loadPsdOntoCanvas = loadPsdOntoCanvas;