@merchi/product-editor 0.1.9
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/.turbo/turbo-build.log +1 -0
- package/README.md +174 -0
- package/dist/components/AlignMenu.d.ts +3 -0
- package/dist/components/AlignMenu.js +61 -0
- package/dist/components/FloatingToolbar.d.ts +4 -0
- package/dist/components/FloatingToolbar.js +97 -0
- package/dist/components/FormatButtons.d.ts +2 -0
- package/dist/components/FormatButtons.js +22 -0
- package/dist/components/ImageNavButton.d.ts +7 -0
- package/dist/components/ImageNavButton.js +14 -0
- package/dist/components/ImageZoomModal.d.ts +12 -0
- package/dist/components/ImageZoomModal.js +129 -0
- package/dist/components/LayerPanel.d.ts +4 -0
- package/dist/components/LayerPanel.js +223 -0
- package/dist/components/LoadingOverlay.d.ts +9 -0
- package/dist/components/LoadingOverlay.js +17 -0
- package/dist/components/ProductEditor.d.ts +4 -0
- package/dist/components/ProductEditor.js +67 -0
- package/dist/components/ProductImageGallery.d.ts +6 -0
- package/dist/components/ProductImageGallery.js +148 -0
- package/dist/components/ProductPreviews.d.ts +7 -0
- package/dist/components/ProductPreviews.js +281 -0
- package/dist/components/TextToolbar.d.ts +4 -0
- package/dist/components/TextToolbar.js +117 -0
- package/dist/components/Toolbar.d.ts +3 -0
- package/dist/components/Toolbar.js +47 -0
- package/dist/config/colorConfig.d.ts +1 -0
- package/dist/config/colorConfig.js +10 -0
- package/dist/config/fontConfig.d.ts +5 -0
- package/dist/config/fontConfig.js +21 -0
- package/dist/context/ProductEditorContext.d.ts +60 -0
- package/dist/context/ProductEditorContext.js +743 -0
- package/dist/hooks/useSwipeNavigate.d.ts +11 -0
- package/dist/hooks/useSwipeNavigate.js +35 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +30 -0
- package/dist/styles/BottomPreviewDisplay.css +65 -0
- package/dist/styles/FloatingToolbar.css +41 -0
- package/dist/styles/ImageGallery.css +147 -0
- package/dist/styles/ImageNavButton.css +35 -0
- package/dist/styles/ImageZoomModal.css +101 -0
- package/dist/styles/LayerPanel.css +179 -0
- package/dist/styles/LoadingOverlay.css +48 -0
- package/dist/styles/ProductEditor.css +192 -0
- package/dist/styles/TextToolbar.css +327 -0
- package/dist/styles/Toolbar.css +125 -0
- package/dist/styles/index.css +5 -0
- package/dist/styles/textToolbarSelectStyles.d.ts +3 -0
- package/dist/styles/textToolbarSelectStyles.js +30 -0
- package/dist/types.d.ts +228 -0
- package/dist/types.js +17 -0
- package/dist/utils/canvasAddText.d.ts +11 -0
- package/dist/utils/canvasAddText.js +66 -0
- package/dist/utils/canvasUtils.d.ts +37 -0
- package/dist/utils/canvasUtils.js +342 -0
- package/dist/utils/deviceUtils.d.ts +5 -0
- package/dist/utils/deviceUtils.js +33 -0
- package/dist/utils/draftTemplateUtils.d.ts +23 -0
- package/dist/utils/draftTemplateUtils.js +66 -0
- package/dist/utils/grid.d.ts +32 -0
- package/dist/utils/grid.js +151 -0
- package/dist/utils/imageUtils.d.ts +12 -0
- package/dist/utils/imageUtils.js +178 -0
- package/dist/utils/job.d.ts +40 -0
- package/dist/utils/job.js +115 -0
- package/dist/utils/keyboard.d.ts +1 -0
- package/dist/utils/keyboard.js +26 -0
- package/dist/utils/previewUtils.d.ts +9 -0
- package/dist/utils/previewUtils.js +39 -0
- package/dist/utils/psdConverter.d.ts +40 -0
- package/dist/utils/psdConverter.js +516 -0
- package/dist/utils/psdRenderUtils.d.ts +12 -0
- package/dist/utils/psdRenderUtils.js +465 -0
- package/dist/utils/renderUtils.d.ts +2 -0
- package/dist/utils/renderUtils.js +85 -0
- package/package.json +66 -0
- package/src/components/AlignMenu.tsx +49 -0
- package/src/components/FloatingToolbar.tsx +83 -0
- package/src/components/FormatButtons.tsx +31 -0
- package/src/components/ImageNavButton.tsx +23 -0
- package/src/components/ImageZoomModal.tsx +163 -0
- package/src/components/LayerPanel.tsx +256 -0
- package/src/components/LoadingOverlay.tsx +27 -0
- package/src/components/ProductEditor.tsx +104 -0
- package/src/components/ProductImageGallery.tsx +181 -0
- package/src/components/ProductPreviews.tsx +243 -0
- package/src/components/TextToolbar.tsx +164 -0
- package/src/components/Toolbar.tsx +86 -0
- package/src/config/colorConfig.ts +7 -0
- package/src/config/fontConfig.ts +23 -0
- package/src/context/ProductEditorContext.tsx +829 -0
- package/src/hooks/useSwipeNavigate.ts +40 -0
- package/src/index.ts +6 -0
- package/src/styles/BottomPreviewDisplay.css +65 -0
- package/src/styles/FloatingToolbar.css +41 -0
- package/src/styles/ImageGallery.css +147 -0
- package/src/styles/ImageNavButton.css +35 -0
- package/src/styles/ImageZoomModal.css +101 -0
- package/src/styles/LayerPanel.css +179 -0
- package/src/styles/LoadingOverlay.css +48 -0
- package/src/styles/ProductEditor.css +192 -0
- package/src/styles/TextToolbar.css +327 -0
- package/src/styles/Toolbar.css +125 -0
- package/src/styles/index.css +5 -0
- package/src/styles/textToolbarSelectStyles.ts +60 -0
- package/src/types/ag-psd/index.d.ts +1 -0
- package/src/types/fabric.d.ts +14 -0
- package/src/types/lodash/index.d.ts +1 -0
- package/src/types/psd.d.ts +53 -0
- package/src/types.ts +247 -0
- package/src/utils/canvasAddText.ts +72 -0
- package/src/utils/canvasUtils.ts +406 -0
- package/src/utils/deviceUtils.ts +31 -0
- package/src/utils/draftTemplateUtils.ts +70 -0
- package/src/utils/grid.ts +154 -0
- package/src/utils/imageUtils.ts +218 -0
- package/src/utils/job.ts +128 -0
- package/src/utils/keyboard.ts +28 -0
- package/src/utils/previewUtils.ts +43 -0
- package/src/utils/psdConverter.ts +595 -0
- package/src/utils/psdRenderUtils.ts +453 -0
- package/src/utils/renderUtils.ts +44 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import { readPsd } from 'ag-psd';
|
|
2
|
+
import { MappedPreview } from '../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Find a layer by name within a PSD layer structure
|
|
6
|
+
*
|
|
7
|
+
* @param layers The array of PSD layers to search through
|
|
8
|
+
* @param name The name of the layer to find
|
|
9
|
+
* @returns The found layer or null if not found
|
|
10
|
+
*/
|
|
11
|
+
function findLayerByName(layers: any[] | undefined, name: string): any | null {
|
|
12
|
+
if (!layers) return null;
|
|
13
|
+
|
|
14
|
+
for (const layer of layers) {
|
|
15
|
+
if (layer.name === name) {
|
|
16
|
+
return layer;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (layer.children) {
|
|
20
|
+
const found = findLayerByName(layer.children, name);
|
|
21
|
+
if (found) return found;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Load an image from a URL
|
|
30
|
+
*
|
|
31
|
+
* @param url URL of the image to load
|
|
32
|
+
* @returns Promise that resolves to an HTMLImageElement
|
|
33
|
+
*/
|
|
34
|
+
async function loadImage(url: string): Promise<HTMLImageElement> {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const img = new Image();
|
|
37
|
+
img.crossOrigin = 'anonymous';
|
|
38
|
+
|
|
39
|
+
img.onload = () => resolve(img);
|
|
40
|
+
img.onerror = (e) => reject(new Error(`Failed to load image from URL: ${url}, error: ${e}`));
|
|
41
|
+
|
|
42
|
+
img.src = url;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validates if a data URL represents a valid image
|
|
48
|
+
*
|
|
49
|
+
* @param dataUrl The data URL to validate
|
|
50
|
+
* @returns Promise that resolves to true if valid, false otherwise
|
|
51
|
+
*/
|
|
52
|
+
async function isValidImageDataUrl(dataUrl: string): Promise<boolean> {
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
if (!dataUrl || !dataUrl.startsWith('data:image/')) {
|
|
55
|
+
console.warn('Invalid data URL format');
|
|
56
|
+
resolve(false);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const img = new Image();
|
|
61
|
+
img.onload = () => {
|
|
62
|
+
// Check if the image has actual dimensions
|
|
63
|
+
if (img.width > 0 && img.height > 0) {
|
|
64
|
+
resolve(true);
|
|
65
|
+
} else {
|
|
66
|
+
console.warn('Image loaded but has zero dimensions');
|
|
67
|
+
resolve(false);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
img.onerror = () => {
|
|
71
|
+
console.warn('Failed to load image from data URL');
|
|
72
|
+
resolve(false);
|
|
73
|
+
};
|
|
74
|
+
img.src = dataUrl;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Creates a fallback preview image with debugging information
|
|
80
|
+
*
|
|
81
|
+
* @param width Width of the image
|
|
82
|
+
* @param height Height of the image
|
|
83
|
+
* @param draftPreviewId ID of the draft preview
|
|
84
|
+
* @param error Optional error message
|
|
85
|
+
* @returns PNG data URL
|
|
86
|
+
*/
|
|
87
|
+
function createFallbackImage(width: number, height: number, draftPreviewId: number | undefined, error?: string): string {
|
|
88
|
+
const canvas = document.createElement('canvas');
|
|
89
|
+
canvas.width = Math.max(width || 300, 300);
|
|
90
|
+
canvas.height = Math.max(height || 200, 200);
|
|
91
|
+
const ctx = canvas.getContext('2d');
|
|
92
|
+
|
|
93
|
+
if (!ctx) {
|
|
94
|
+
// Even more basic fallback if context fails
|
|
95
|
+
const basicCanvas = document.createElement('canvas');
|
|
96
|
+
basicCanvas.width = 300;
|
|
97
|
+
basicCanvas.height = 200;
|
|
98
|
+
const basicCtx = basicCanvas.getContext('2d');
|
|
99
|
+
if (basicCtx) {
|
|
100
|
+
basicCtx.fillStyle = '#ffcccc';
|
|
101
|
+
basicCtx.fillRect(0, 0, 300, 200);
|
|
102
|
+
basicCtx.fillStyle = '#990000';
|
|
103
|
+
basicCtx.font = '16px Arial';
|
|
104
|
+
basicCtx.textAlign = 'center';
|
|
105
|
+
basicCtx.fillText('Preview failed', 150, 100);
|
|
106
|
+
return basicCanvas.toDataURL('image/png');
|
|
107
|
+
}
|
|
108
|
+
return '';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Background
|
|
112
|
+
ctx.fillStyle = '#f8f8f8';
|
|
113
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
114
|
+
|
|
115
|
+
// Draw a grid pattern
|
|
116
|
+
ctx.strokeStyle = '#e0e0e0';
|
|
117
|
+
ctx.lineWidth = 1;
|
|
118
|
+
const gridSize = 20;
|
|
119
|
+
|
|
120
|
+
for (let x = 0; x < canvas.width; x += gridSize) {
|
|
121
|
+
ctx.beginPath();
|
|
122
|
+
ctx.moveTo(x, 0);
|
|
123
|
+
ctx.lineTo(x, canvas.height);
|
|
124
|
+
ctx.stroke();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (let y = 0; y < canvas.height; y += gridSize) {
|
|
128
|
+
ctx.beginPath();
|
|
129
|
+
ctx.moveTo(0, y);
|
|
130
|
+
ctx.lineTo(canvas.width, y);
|
|
131
|
+
ctx.stroke();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Border
|
|
135
|
+
ctx.strokeStyle = '#cccccc';
|
|
136
|
+
ctx.lineWidth = 2;
|
|
137
|
+
ctx.strokeRect(0, 0, canvas.width, canvas.height);
|
|
138
|
+
|
|
139
|
+
// Preview text
|
|
140
|
+
ctx.fillStyle = '#666666';
|
|
141
|
+
ctx.font = 'bold 24px Arial';
|
|
142
|
+
ctx.textAlign = 'center';
|
|
143
|
+
ctx.fillText('Preview', canvas.width / 2, canvas.height / 2 - 15);
|
|
144
|
+
|
|
145
|
+
// Preview ID
|
|
146
|
+
if (draftPreviewId !== undefined) {
|
|
147
|
+
ctx.font = '16px Arial';
|
|
148
|
+
ctx.fillText(`ID: ${draftPreviewId}`, canvas.width / 2, canvas.height / 2 + 15);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Error message if provided
|
|
152
|
+
if (error) {
|
|
153
|
+
ctx.fillStyle = '#cc0000';
|
|
154
|
+
ctx.font = '14px Arial';
|
|
155
|
+
ctx.fillText(error, canvas.width / 2, canvas.height / 2 + 40);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return canvas.toDataURL('image/png');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Converts a data URL to a canvas element
|
|
163
|
+
*
|
|
164
|
+
* @param dataUrl The data URL to convert
|
|
165
|
+
* @returns Promise that resolves to a Canvas element
|
|
166
|
+
*/
|
|
167
|
+
async function dataUrlToCanvas(dataUrl: string): Promise<HTMLImageElement> {
|
|
168
|
+
// First load the data URL as an image
|
|
169
|
+
return await loadImage(dataUrl);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Process mapped previews, replacing layers in PSD files with rendered layers
|
|
174
|
+
* and converting the results to PNG images
|
|
175
|
+
*
|
|
176
|
+
* @param mappedPreviews Array of mapped previews with draftPreview and draftPreviewLayers
|
|
177
|
+
* @returns Promise that resolves to an array of processed previews with PNG data URLs
|
|
178
|
+
*/
|
|
179
|
+
export async function renderDraftPreviewsWithLayers(
|
|
180
|
+
mappedPreviews: MappedPreview[]
|
|
181
|
+
): Promise<Array<{ draftPreviewId: number | undefined; pngDataUrl: string }>> {
|
|
182
|
+
const results: Array<{ draftPreviewId: number | undefined; pngDataUrl: string }> = [];
|
|
183
|
+
|
|
184
|
+
// Process each preview one by one
|
|
185
|
+
for (const mappedPreview of mappedPreviews) {
|
|
186
|
+
const { draftPreview, draftPreviewLayers } = mappedPreview;
|
|
187
|
+
|
|
188
|
+
// Skip if there's no file to process
|
|
189
|
+
if (!draftPreview?.file?.viewUrl) {
|
|
190
|
+
console.warn('Skipping preview without a file URL', draftPreview?.id);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Check if there are any actual rendered layers we need to use
|
|
195
|
+
const hasRenderedLayers = draftPreviewLayers.some(layer => layer.renderedLayer !== null);
|
|
196
|
+
|
|
197
|
+
// Only try the simple approach if it's not a PSD file
|
|
198
|
+
let simplePngDataUrl: string | null = null;
|
|
199
|
+
|
|
200
|
+
// If we need to do layer replacement, proceed with PSD processing
|
|
201
|
+
try {
|
|
202
|
+
// Fetch the PSD file
|
|
203
|
+
const response = await fetch(draftPreview.file.viewUrl, {
|
|
204
|
+
mode: 'cors',
|
|
205
|
+
credentials: 'same-origin',
|
|
206
|
+
headers: {
|
|
207
|
+
'Accept': '*/*',
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
throw new Error(`Failed to fetch PSD file: ${response.status} ${response.statusText}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
216
|
+
|
|
217
|
+
// Check if we actually got a PSD file (PSD files start with '8BPS')
|
|
218
|
+
const firstBytes = new Uint8Array(arrayBuffer, 0, 4);
|
|
219
|
+
const signature =
|
|
220
|
+
String.fromCharCode(firstBytes[0]) +
|
|
221
|
+
String.fromCharCode(firstBytes[1]) +
|
|
222
|
+
String.fromCharCode(firstBytes[2]) +
|
|
223
|
+
String.fromCharCode(firstBytes[3]);
|
|
224
|
+
|
|
225
|
+
if (signature !== '8BPS') {
|
|
226
|
+
// Use the simple approach result if we have it
|
|
227
|
+
if (simplePngDataUrl) {
|
|
228
|
+
results.push({
|
|
229
|
+
draftPreviewId: draftPreview.id,
|
|
230
|
+
pngDataUrl: simplePngDataUrl
|
|
231
|
+
});
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
throw new Error('Not a PSD file and simple approach failed');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Read the PSD file
|
|
238
|
+
const psd = readPsd(arrayBuffer, {
|
|
239
|
+
skipCompositeImageData: false,
|
|
240
|
+
skipLayerImageData: false,
|
|
241
|
+
skipThumbnail: false
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Create a canvas to draw the composite image
|
|
245
|
+
const canvas = document.createElement('canvas');
|
|
246
|
+
canvas.width = psd.width || 500; // Default size if missing
|
|
247
|
+
canvas.height = psd.height || 400;
|
|
248
|
+
const ctx = canvas.getContext('2d');
|
|
249
|
+
|
|
250
|
+
if (!ctx) {
|
|
251
|
+
throw new Error('Failed to get canvas 2d context');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Draw a background to ensure we have something visible
|
|
255
|
+
ctx.fillStyle = 'transparent';
|
|
256
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
257
|
+
|
|
258
|
+
// First draw the composite image as a base
|
|
259
|
+
if (psd.imageData) {
|
|
260
|
+
const imageData = new ImageData(
|
|
261
|
+
new Uint8ClampedArray(psd.imageData.data),
|
|
262
|
+
psd.width,
|
|
263
|
+
psd.height
|
|
264
|
+
);
|
|
265
|
+
ctx.putImageData(imageData, 0, 0);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Now draw base layers that aren't being replaced
|
|
269
|
+
if (psd.children && psd.children.length > 0) {
|
|
270
|
+
for (const layer of psd.children) {
|
|
271
|
+
// Skip if this layer is hidden or matches one of our replaceable layers
|
|
272
|
+
const shouldReplace = draftPreviewLayers.some(dpl =>
|
|
273
|
+
dpl.layerName === layer.name && dpl.renderedLayer !== null
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
if (!shouldReplace && !layer.hidden && layer.canvas) {
|
|
277
|
+
// Draw the original layer
|
|
278
|
+
ctx.drawImage(layer.canvas, layer.left || 0, layer.top || 0);
|
|
279
|
+
ctx.globalAlpha = 1;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Now process and draw each replacement layer
|
|
285
|
+
if (hasRenderedLayers) {
|
|
286
|
+
for (const draftPreviewLayer of draftPreviewLayers) {
|
|
287
|
+
// Skip if no layer name or no rendered layer
|
|
288
|
+
if (!draftPreviewLayer.layerName || !draftPreviewLayer.renderedLayer) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Find the corresponding layer in the PSD
|
|
293
|
+
const psdLayer = findLayerByName(psd.children, draftPreviewLayer.layerName);
|
|
294
|
+
|
|
295
|
+
if (!psdLayer) {
|
|
296
|
+
console.warn(`Layer "${draftPreviewLayer.layerName}" not found in PSD`);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Load the rendered image
|
|
301
|
+
try {
|
|
302
|
+
// Validate the rendered layer image URL
|
|
303
|
+
if (!draftPreviewLayer.renderedLayer.draft ||
|
|
304
|
+
!draftPreviewLayer.renderedLayer.draft.startsWith('data:image/')) {
|
|
305
|
+
throw new Error('Invalid image URL format');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Convert the data URL to a canvas element instead of an image
|
|
309
|
+
const layerCanvas = await dataUrlToCanvas(draftPreviewLayer.renderedLayer.draft);
|
|
310
|
+
|
|
311
|
+
// Validate the canvas dimensions
|
|
312
|
+
if (layerCanvas.width <= 0 || layerCanvas.height <= 0) {
|
|
313
|
+
console.error('Canvas has invalid dimensions');
|
|
314
|
+
throw new Error('Invalid canvas dimensions');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Use the position and dimensions of the original PSD layer
|
|
318
|
+
const left = psdLayer.left || 0;
|
|
319
|
+
const top = psdLayer.top || 0;
|
|
320
|
+
const width = (psdLayer.right || 0) - (psdLayer.left || 0);
|
|
321
|
+
const height = (psdLayer.bottom || 0) - (psdLayer.top || 0);
|
|
322
|
+
|
|
323
|
+
if (width <= 0 || height <= 0) {
|
|
324
|
+
console.warn(`Invalid layer dimensions: ${width}x${height}, skipping`);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Calculate how to fit the canvas within the layer bounds
|
|
329
|
+
const aspectRatio = layerCanvas.width / layerCanvas.height;
|
|
330
|
+
const targetAspectRatio = width / height;
|
|
331
|
+
|
|
332
|
+
let drawWidth, drawHeight, offsetX = 0, offsetY = 0;
|
|
333
|
+
|
|
334
|
+
if (aspectRatio > targetAspectRatio) {
|
|
335
|
+
// Canvas is wider than target area
|
|
336
|
+
drawWidth = width;
|
|
337
|
+
drawHeight = width / aspectRatio;
|
|
338
|
+
offsetY = (height - drawHeight) / 2;
|
|
339
|
+
} else {
|
|
340
|
+
// Canvas is taller than target area
|
|
341
|
+
drawHeight = height;
|
|
342
|
+
drawWidth = height * aspectRatio;
|
|
343
|
+
offsetX = (width - drawWidth) / 2;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Draw the rendered layer canvas
|
|
347
|
+
// ctx.globalAlpha = psdLayer.opacity !== undefined ? psdLayer.opacity / 255 : 1;
|
|
348
|
+
ctx.restore();
|
|
349
|
+
ctx.drawImage(layerCanvas, left + offsetX, top + offsetY, drawWidth, drawHeight);
|
|
350
|
+
ctx.globalAlpha = 1;
|
|
351
|
+
|
|
352
|
+
ctx.save();
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
} catch (error) {
|
|
358
|
+
console.error(`Failed to load rendered layer image for "${draftPreviewLayer.layerName}"`, error);
|
|
359
|
+
|
|
360
|
+
// If we fail to load the replacement, draw the original layer as fallback
|
|
361
|
+
if (psdLayer.canvas) {
|
|
362
|
+
ctx.globalAlpha = psdLayer.opacity !== undefined ? psdLayer.opacity / 255 : 1;
|
|
363
|
+
ctx.drawImage(psdLayer.canvas, psdLayer.left || 0, psdLayer.top || 0);
|
|
364
|
+
ctx.globalAlpha = 1;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Verify that something was drawn
|
|
371
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
372
|
+
let hasVisiblePixels = false;
|
|
373
|
+
|
|
374
|
+
for (let i = 3; i < imageData.data.length; i += 4) {
|
|
375
|
+
if (imageData.data[i] > 0) {
|
|
376
|
+
hasVisiblePixels = true;
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!hasVisiblePixels) {
|
|
382
|
+
console.warn('WARNING: Canvas appears to be completely transparent!');
|
|
383
|
+
|
|
384
|
+
// Use the simple approach result if available
|
|
385
|
+
if (simplePngDataUrl) {
|
|
386
|
+
results.push({
|
|
387
|
+
draftPreviewId: draftPreview.id,
|
|
388
|
+
pngDataUrl: simplePngDataUrl
|
|
389
|
+
});
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// As a last resort, draw a visible placeholder
|
|
394
|
+
ctx.fillStyle = 'rgba(200, 200, 200, 0.5)';
|
|
395
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
396
|
+
ctx.fillStyle = 'rgba(150, 150, 150, 1)';
|
|
397
|
+
ctx.font = '20px Arial';
|
|
398
|
+
ctx.textAlign = 'center';
|
|
399
|
+
ctx.fillText('Preview', canvas.width / 2, canvas.height / 2);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Convert the canvas to a PNG data URL
|
|
403
|
+
const pngDataUrl = canvas.toDataURL('image/png');
|
|
404
|
+
|
|
405
|
+
// Validate the PNG data URL
|
|
406
|
+
const isValid = await isValidImageDataUrl(pngDataUrl);
|
|
407
|
+
if (!isValid) {
|
|
408
|
+
console.warn('Generated PNG data URL is not valid!');
|
|
409
|
+
|
|
410
|
+
// Use the simple approach as fallback if available
|
|
411
|
+
if (simplePngDataUrl) {
|
|
412
|
+
results.push({
|
|
413
|
+
draftPreviewId: draftPreview.id,
|
|
414
|
+
pngDataUrl: simplePngDataUrl
|
|
415
|
+
});
|
|
416
|
+
} else {
|
|
417
|
+
// Create a debug fallback image with error information
|
|
418
|
+
const fallbackPng = createFallbackImage(500, 400, draftPreview.id, 'Invalid PNG data');
|
|
419
|
+
results.push({
|
|
420
|
+
draftPreviewId: draftPreview.id,
|
|
421
|
+
pngDataUrl: fallbackPng
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
} else {
|
|
425
|
+
// Add the result to our array
|
|
426
|
+
results.push({
|
|
427
|
+
draftPreviewId: draftPreview.id,
|
|
428
|
+
pngDataUrl
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
} catch (error) {
|
|
433
|
+
console.error('Failed to process PSD file for draft preview:', error);
|
|
434
|
+
|
|
435
|
+
// Use the simple approach as fallback if available
|
|
436
|
+
if (simplePngDataUrl) {
|
|
437
|
+
results.push({
|
|
438
|
+
draftPreviewId: draftPreview.id,
|
|
439
|
+
pngDataUrl: simplePngDataUrl
|
|
440
|
+
});
|
|
441
|
+
} else {
|
|
442
|
+
// Create a debug fallback image with error information
|
|
443
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
444
|
+
const fallbackPng = createFallbackImage(500, 400, draftPreview.id, errorMessage);
|
|
445
|
+
results.push({
|
|
446
|
+
draftPreviewId: draftPreview.id,
|
|
447
|
+
pngDataUrl: fallbackPng
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return results;
|
|
453
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { DraftTemplate, SavedCanvasObject } from "../types";
|
|
2
|
+
import { loadRegularImagePromise } from "./imageUtils";
|
|
3
|
+
import { loadPsdOntoCanvas } from "./psdConverter";
|
|
4
|
+
|
|
5
|
+
// Simplify loadTemplateImage to just load the template without variations
|
|
6
|
+
export const renderEditorOrPreview = async (
|
|
7
|
+
fabricCanvas: fabric.Canvas,
|
|
8
|
+
template: DraftTemplate,
|
|
9
|
+
variations: any[],
|
|
10
|
+
savedObjects: SavedCanvasObject[],
|
|
11
|
+
width: number,
|
|
12
|
+
height: number,
|
|
13
|
+
): Promise<void> => {
|
|
14
|
+
// Check if we have a valid image URL
|
|
15
|
+
if (!template.file?.viewUrl) {
|
|
16
|
+
return Promise.resolve();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const file = template.file;
|
|
20
|
+
|
|
21
|
+
const imageUrl = file.viewUrl;
|
|
22
|
+
|
|
23
|
+
// Check if this might be a PSD file
|
|
24
|
+
const isPsd = imageUrl.toLowerCase().endsWith('.psd')
|
|
25
|
+
|| (file.mimetype && file.mimetype.includes('photoshop'));
|
|
26
|
+
|
|
27
|
+
// If it's a PSD file, use our specialized function to process it
|
|
28
|
+
if (isPsd) {
|
|
29
|
+
// First, check if the canvas is still valid
|
|
30
|
+
try {
|
|
31
|
+
// Process the PSD file
|
|
32
|
+
await loadPsdOntoCanvas(fabricCanvas, imageUrl, variations, savedObjects, width, height);
|
|
33
|
+
return Promise.resolve();
|
|
34
|
+
} catch (error) {
|
|
35
|
+
// If PSD processing fails, fall back to our regular image loading
|
|
36
|
+
await loadRegularImagePromise(fabricCanvas, template, width, height);
|
|
37
|
+
return Promise.resolve();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// For non-PSD files, use the regular image loading approach
|
|
42
|
+
await loadRegularImagePromise(fabricCanvas, template, width, height);
|
|
43
|
+
return Promise.resolve();
|
|
44
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es5",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["dom", "es2015"],
|
|
6
|
+
"jsx": "react",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"outDir": "./dist",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"moduleResolution": "node",
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"forceConsistentCasingInFileNames": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|