merchi_product_editor
Version:
A React component for editing product images using Fabric.js
517 lines (516 loc) • 33.3 kB
JavaScript
;
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;