merchi_product_editor
Version:
A React component for editing product images using Fabric.js
466 lines (465 loc) • 22.6 kB
JavaScript
;
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 };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.renderDraftPreviewsWithLayers = void 0;
var ag_psd_1 = require("ag-psd");
/**
* Find a layer by name within a PSD layer structure
*
* @param layers The array of PSD layers to search through
* @param name The name of the layer to find
* @returns The found layer or null if not found
*/
function findLayerByName(layers, name) {
if (!layers)
return null;
for (var _i = 0, layers_1 = layers; _i < layers_1.length; _i++) {
var layer = layers_1[_i];
if (layer.name === name) {
return layer;
}
if (layer.children) {
var found = findLayerByName(layer.children, name);
if (found)
return found;
}
}
return null;
}
/**
* Load an image from a URL
*
* @param url URL of the image to load
* @returns Promise that resolves to an HTMLImageElement
*/
function loadImage(url) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
return [2 /*return*/, new Promise(function (resolve, reject) {
var img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function () { return resolve(img); };
img.onerror = function (e) { return reject(new Error("Failed to load image from URL: ".concat(url, ", error: ").concat(e))); };
img.src = url;
})];
});
});
}
/**
* Validates if a data URL represents a valid image
*
* @param dataUrl The data URL to validate
* @returns Promise that resolves to true if valid, false otherwise
*/
function isValidImageDataUrl(dataUrl) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
return [2 /*return*/, new Promise(function (resolve) {
if (!dataUrl || !dataUrl.startsWith('data:image/')) {
console.warn('Invalid data URL format');
resolve(false);
return;
}
var img = new Image();
img.onload = function () {
// Check if the image has actual dimensions
if (img.width > 0 && img.height > 0) {
resolve(true);
}
else {
console.warn('Image loaded but has zero dimensions');
resolve(false);
}
};
img.onerror = function () {
console.warn('Failed to load image from data URL');
resolve(false);
};
img.src = dataUrl;
})];
});
});
}
/**
* Creates a fallback preview image with debugging information
*
* @param width Width of the image
* @param height Height of the image
* @param draftPreviewId ID of the draft preview
* @param error Optional error message
* @returns PNG data URL
*/
function createFallbackImage(width, height, draftPreviewId, error) {
var canvas = document.createElement('canvas');
canvas.width = Math.max(width || 300, 300);
canvas.height = Math.max(height || 200, 200);
var ctx = canvas.getContext('2d');
if (!ctx) {
// Even more basic fallback if context fails
var basicCanvas = document.createElement('canvas');
basicCanvas.width = 300;
basicCanvas.height = 200;
var basicCtx = basicCanvas.getContext('2d');
if (basicCtx) {
basicCtx.fillStyle = '#ffcccc';
basicCtx.fillRect(0, 0, 300, 200);
basicCtx.fillStyle = '#990000';
basicCtx.font = '16px Arial';
basicCtx.textAlign = 'center';
basicCtx.fillText('Preview failed', 150, 100);
return basicCanvas.toDataURL('image/png');
}
return '';
}
// Background
ctx.fillStyle = '#f8f8f8';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw a grid pattern
ctx.strokeStyle = '#e0e0e0';
ctx.lineWidth = 1;
var gridSize = 20;
for (var x = 0; x < canvas.width; x += gridSize) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
}
for (var y = 0; y < canvas.height; y += gridSize) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
}
// Border
ctx.strokeStyle = '#cccccc';
ctx.lineWidth = 2;
ctx.strokeRect(0, 0, canvas.width, canvas.height);
// Preview text
ctx.fillStyle = '#666666';
ctx.font = 'bold 24px Arial';
ctx.textAlign = 'center';
ctx.fillText('Preview', canvas.width / 2, canvas.height / 2 - 15);
// Preview ID
if (draftPreviewId !== undefined) {
ctx.font = '16px Arial';
ctx.fillText("ID: ".concat(draftPreviewId), canvas.width / 2, canvas.height / 2 + 15);
}
// Error message if provided
if (error) {
ctx.fillStyle = '#cc0000';
ctx.font = '14px Arial';
ctx.fillText(error, canvas.width / 2, canvas.height / 2 + 40);
}
return canvas.toDataURL('image/png');
}
/**
* Converts a data URL to a canvas element
*
* @param dataUrl The data URL to convert
* @returns Promise that resolves to a Canvas element
*/
function dataUrlToCanvas(dataUrl) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, loadImage(dataUrl)];
case 1:
// First load the data URL as an image
return [2 /*return*/, _a.sent()];
}
});
});
}
/**
* Process mapped previews, replacing layers in PSD files with rendered layers
* and converting the results to PNG images
*
* @param mappedPreviews Array of mapped previews with draftPreview and draftPreviewLayers
* @returns Promise that resolves to an array of processed previews with PNG data URLs
*/
function renderDraftPreviewsWithLayers(mappedPreviews) {
var _a;
return __awaiter(this, void 0, void 0, function () {
var results, _i, mappedPreviews_1, mappedPreview, draftPreview, draftPreviewLayers, hasRenderedLayers, simplePngDataUrl, response, arrayBuffer, firstBytes, signature, psd, canvas, ctx, imageData_1, _loop_1, _b, _c, layer, _d, draftPreviewLayers_1, draftPreviewLayer, psdLayer, layerCanvas, left, top_1, width, height, aspectRatio, targetAspectRatio, drawWidth, drawHeight, offsetX, offsetY, error_1, imageData, hasVisiblePixels, i, pngDataUrl, isValid, fallbackPng, error_2, errorMessage, fallbackPng;
return __generator(this, function (_e) {
switch (_e.label) {
case 0:
results = [];
_i = 0, mappedPreviews_1 = mappedPreviews;
_e.label = 1;
case 1:
if (!(_i < mappedPreviews_1.length)) return [3 /*break*/, 14];
mappedPreview = mappedPreviews_1[_i];
draftPreview = mappedPreview.draftPreview, draftPreviewLayers = mappedPreview.draftPreviewLayers;
// Skip if there's no file to process
if (!((_a = draftPreview === null || draftPreview === void 0 ? void 0 : draftPreview.file) === null || _a === void 0 ? void 0 : _a.viewUrl)) {
console.warn('Skipping preview without a file URL', draftPreview === null || draftPreview === void 0 ? void 0 : draftPreview.id);
return [3 /*break*/, 13];
}
hasRenderedLayers = draftPreviewLayers.some(function (layer) { return layer.renderedLayer !== null; });
simplePngDataUrl = null;
_e.label = 2;
case 2:
_e.trys.push([2, 12, , 13]);
return [4 /*yield*/, fetch(draftPreview.file.viewUrl, {
mode: 'cors',
credentials: 'same-origin',
headers: {
'Accept': '*/*',
}
})];
case 3:
response = _e.sent();
if (!response.ok) {
throw new Error("Failed to fetch PSD file: ".concat(response.status, " ").concat(response.statusText));
}
return [4 /*yield*/, response.arrayBuffer()];
case 4:
arrayBuffer = _e.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') {
// Use the simple approach result if we have it
if (simplePngDataUrl) {
results.push({
draftPreviewId: draftPreview.id,
pngDataUrl: simplePngDataUrl
});
return [3 /*break*/, 13];
}
throw new Error('Not a PSD file and simple approach failed');
}
psd = (0, ag_psd_1.readPsd)(arrayBuffer, {
skipCompositeImageData: false,
skipLayerImageData: false,
skipThumbnail: false
});
canvas = document.createElement('canvas');
canvas.width = psd.width || 500; // Default size if missing
canvas.height = psd.height || 400;
ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Failed to get canvas 2d context');
}
// Draw a background to ensure we have something visible
ctx.fillStyle = 'transparent';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// First draw the composite image as a base
if (psd.imageData) {
imageData_1 = new ImageData(new Uint8ClampedArray(psd.imageData.data), psd.width, psd.height);
ctx.putImageData(imageData_1, 0, 0);
}
// Now draw base layers that aren't being replaced
if (psd.children && psd.children.length > 0) {
_loop_1 = function (layer) {
// Skip if this layer is hidden or matches one of our replaceable layers
var shouldReplace = draftPreviewLayers.some(function (dpl) {
return dpl.layerName === layer.name && dpl.renderedLayer !== null;
});
if (!shouldReplace && !layer.hidden && layer.canvas) {
// Draw the original layer
ctx.drawImage(layer.canvas, layer.left || 0, layer.top || 0);
ctx.globalAlpha = 1;
}
};
for (_b = 0, _c = psd.children; _b < _c.length; _b++) {
layer = _c[_b];
_loop_1(layer);
}
}
if (!hasRenderedLayers) return [3 /*break*/, 10];
_d = 0, draftPreviewLayers_1 = draftPreviewLayers;
_e.label = 5;
case 5:
if (!(_d < draftPreviewLayers_1.length)) return [3 /*break*/, 10];
draftPreviewLayer = draftPreviewLayers_1[_d];
// Skip if no layer name or no rendered layer
if (!draftPreviewLayer.layerName || !draftPreviewLayer.renderedLayer) {
return [3 /*break*/, 9];
}
psdLayer = findLayerByName(psd.children, draftPreviewLayer.layerName);
if (!psdLayer) {
console.warn("Layer \"".concat(draftPreviewLayer.layerName, "\" not found in PSD"));
return [3 /*break*/, 9];
}
_e.label = 6;
case 6:
_e.trys.push([6, 8, , 9]);
// Validate the rendered layer image URL
if (!draftPreviewLayer.renderedLayer.draft ||
!draftPreviewLayer.renderedLayer.draft.startsWith('data:image/')) {
throw new Error('Invalid image URL format');
}
return [4 /*yield*/, dataUrlToCanvas(draftPreviewLayer.renderedLayer.draft)];
case 7:
layerCanvas = _e.sent();
// Validate the canvas dimensions
if (layerCanvas.width <= 0 || layerCanvas.height <= 0) {
console.error('Canvas has invalid dimensions');
throw new Error('Invalid canvas dimensions');
}
left = psdLayer.left || 0;
top_1 = psdLayer.top || 0;
width = (psdLayer.right || 0) - (psdLayer.left || 0);
height = (psdLayer.bottom || 0) - (psdLayer.top || 0);
if (width <= 0 || height <= 0) {
console.warn("Invalid layer dimensions: ".concat(width, "x").concat(height, ", skipping"));
return [3 /*break*/, 9];
}
aspectRatio = layerCanvas.width / layerCanvas.height;
targetAspectRatio = width / height;
drawWidth = void 0, drawHeight = void 0, offsetX = 0, offsetY = 0;
if (aspectRatio > targetAspectRatio) {
// Canvas is wider than target area
drawWidth = width;
drawHeight = width / aspectRatio;
offsetY = (height - drawHeight) / 2;
}
else {
// Canvas is taller than target area
drawHeight = height;
drawWidth = height * aspectRatio;
offsetX = (width - drawWidth) / 2;
}
// Draw the rendered layer canvas
// ctx.globalAlpha = psdLayer.opacity !== undefined ? psdLayer.opacity / 255 : 1;
ctx.restore();
ctx.drawImage(layerCanvas, left + offsetX, top_1 + offsetY, drawWidth, drawHeight);
ctx.globalAlpha = 1;
ctx.save();
return [3 /*break*/, 9];
case 8:
error_1 = _e.sent();
console.error("Failed to load rendered layer image for \"".concat(draftPreviewLayer.layerName, "\""), error_1);
// If we fail to load the replacement, draw the original layer as fallback
if (psdLayer.canvas) {
ctx.globalAlpha = psdLayer.opacity !== undefined ? psdLayer.opacity / 255 : 1;
ctx.drawImage(psdLayer.canvas, psdLayer.left || 0, psdLayer.top || 0);
ctx.globalAlpha = 1;
}
return [3 /*break*/, 9];
case 9:
_d++;
return [3 /*break*/, 5];
case 10:
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
hasVisiblePixels = false;
for (i = 3; i < imageData.data.length; i += 4) {
if (imageData.data[i] > 0) {
hasVisiblePixels = true;
break;
}
}
if (!hasVisiblePixels) {
console.warn('WARNING: Canvas appears to be completely transparent!');
// Use the simple approach result if available
if (simplePngDataUrl) {
results.push({
draftPreviewId: draftPreview.id,
pngDataUrl: simplePngDataUrl
});
return [3 /*break*/, 13];
}
// As a last resort, draw a visible placeholder
ctx.fillStyle = 'rgba(200, 200, 200, 0.5)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'rgba(150, 150, 150, 1)';
ctx.font = '20px Arial';
ctx.textAlign = 'center';
ctx.fillText('Preview', canvas.width / 2, canvas.height / 2);
}
pngDataUrl = canvas.toDataURL('image/png');
return [4 /*yield*/, isValidImageDataUrl(pngDataUrl)];
case 11:
isValid = _e.sent();
if (!isValid) {
console.warn('Generated PNG data URL is not valid!');
// Use the simple approach as fallback if available
if (simplePngDataUrl) {
results.push({
draftPreviewId: draftPreview.id,
pngDataUrl: simplePngDataUrl
});
}
else {
fallbackPng = createFallbackImage(500, 400, draftPreview.id, 'Invalid PNG data');
results.push({
draftPreviewId: draftPreview.id,
pngDataUrl: fallbackPng
});
}
}
else {
// Add the result to our array
results.push({
draftPreviewId: draftPreview.id,
pngDataUrl: pngDataUrl
});
}
return [3 /*break*/, 13];
case 12:
error_2 = _e.sent();
console.error('Failed to process PSD file for draft preview:', error_2);
// Use the simple approach as fallback if available
if (simplePngDataUrl) {
results.push({
draftPreviewId: draftPreview.id,
pngDataUrl: simplePngDataUrl
});
}
else {
errorMessage = error_2 instanceof Error ? error_2.message : 'Unknown error';
fallbackPng = createFallbackImage(500, 400, draftPreview.id, errorMessage);
results.push({
draftPreviewId: draftPreview.id,
pngDataUrl: fallbackPng
});
}
return [3 /*break*/, 13];
case 13:
_i++;
return [3 /*break*/, 1];
case 14: return [2 /*return*/, results];
}
});
});
}
exports.renderDraftPreviewsWithLayers = renderDraftPreviewsWithLayers;