@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.
Files changed (123) hide show
  1. package/.turbo/turbo-build.log +1 -0
  2. package/README.md +174 -0
  3. package/dist/components/AlignMenu.d.ts +3 -0
  4. package/dist/components/AlignMenu.js +61 -0
  5. package/dist/components/FloatingToolbar.d.ts +4 -0
  6. package/dist/components/FloatingToolbar.js +97 -0
  7. package/dist/components/FormatButtons.d.ts +2 -0
  8. package/dist/components/FormatButtons.js +22 -0
  9. package/dist/components/ImageNavButton.d.ts +7 -0
  10. package/dist/components/ImageNavButton.js +14 -0
  11. package/dist/components/ImageZoomModal.d.ts +12 -0
  12. package/dist/components/ImageZoomModal.js +129 -0
  13. package/dist/components/LayerPanel.d.ts +4 -0
  14. package/dist/components/LayerPanel.js +223 -0
  15. package/dist/components/LoadingOverlay.d.ts +9 -0
  16. package/dist/components/LoadingOverlay.js +17 -0
  17. package/dist/components/ProductEditor.d.ts +4 -0
  18. package/dist/components/ProductEditor.js +67 -0
  19. package/dist/components/ProductImageGallery.d.ts +6 -0
  20. package/dist/components/ProductImageGallery.js +148 -0
  21. package/dist/components/ProductPreviews.d.ts +7 -0
  22. package/dist/components/ProductPreviews.js +281 -0
  23. package/dist/components/TextToolbar.d.ts +4 -0
  24. package/dist/components/TextToolbar.js +117 -0
  25. package/dist/components/Toolbar.d.ts +3 -0
  26. package/dist/components/Toolbar.js +47 -0
  27. package/dist/config/colorConfig.d.ts +1 -0
  28. package/dist/config/colorConfig.js +10 -0
  29. package/dist/config/fontConfig.d.ts +5 -0
  30. package/dist/config/fontConfig.js +21 -0
  31. package/dist/context/ProductEditorContext.d.ts +60 -0
  32. package/dist/context/ProductEditorContext.js +743 -0
  33. package/dist/hooks/useSwipeNavigate.d.ts +11 -0
  34. package/dist/hooks/useSwipeNavigate.js +35 -0
  35. package/dist/index.d.ts +6 -0
  36. package/dist/index.js +30 -0
  37. package/dist/styles/BottomPreviewDisplay.css +65 -0
  38. package/dist/styles/FloatingToolbar.css +41 -0
  39. package/dist/styles/ImageGallery.css +147 -0
  40. package/dist/styles/ImageNavButton.css +35 -0
  41. package/dist/styles/ImageZoomModal.css +101 -0
  42. package/dist/styles/LayerPanel.css +179 -0
  43. package/dist/styles/LoadingOverlay.css +48 -0
  44. package/dist/styles/ProductEditor.css +192 -0
  45. package/dist/styles/TextToolbar.css +327 -0
  46. package/dist/styles/Toolbar.css +125 -0
  47. package/dist/styles/index.css +5 -0
  48. package/dist/styles/textToolbarSelectStyles.d.ts +3 -0
  49. package/dist/styles/textToolbarSelectStyles.js +30 -0
  50. package/dist/types.d.ts +228 -0
  51. package/dist/types.js +17 -0
  52. package/dist/utils/canvasAddText.d.ts +11 -0
  53. package/dist/utils/canvasAddText.js +66 -0
  54. package/dist/utils/canvasUtils.d.ts +37 -0
  55. package/dist/utils/canvasUtils.js +342 -0
  56. package/dist/utils/deviceUtils.d.ts +5 -0
  57. package/dist/utils/deviceUtils.js +33 -0
  58. package/dist/utils/draftTemplateUtils.d.ts +23 -0
  59. package/dist/utils/draftTemplateUtils.js +66 -0
  60. package/dist/utils/grid.d.ts +32 -0
  61. package/dist/utils/grid.js +151 -0
  62. package/dist/utils/imageUtils.d.ts +12 -0
  63. package/dist/utils/imageUtils.js +178 -0
  64. package/dist/utils/job.d.ts +40 -0
  65. package/dist/utils/job.js +115 -0
  66. package/dist/utils/keyboard.d.ts +1 -0
  67. package/dist/utils/keyboard.js +26 -0
  68. package/dist/utils/previewUtils.d.ts +9 -0
  69. package/dist/utils/previewUtils.js +39 -0
  70. package/dist/utils/psdConverter.d.ts +40 -0
  71. package/dist/utils/psdConverter.js +516 -0
  72. package/dist/utils/psdRenderUtils.d.ts +12 -0
  73. package/dist/utils/psdRenderUtils.js +465 -0
  74. package/dist/utils/renderUtils.d.ts +2 -0
  75. package/dist/utils/renderUtils.js +85 -0
  76. package/package.json +66 -0
  77. package/src/components/AlignMenu.tsx +49 -0
  78. package/src/components/FloatingToolbar.tsx +83 -0
  79. package/src/components/FormatButtons.tsx +31 -0
  80. package/src/components/ImageNavButton.tsx +23 -0
  81. package/src/components/ImageZoomModal.tsx +163 -0
  82. package/src/components/LayerPanel.tsx +256 -0
  83. package/src/components/LoadingOverlay.tsx +27 -0
  84. package/src/components/ProductEditor.tsx +104 -0
  85. package/src/components/ProductImageGallery.tsx +181 -0
  86. package/src/components/ProductPreviews.tsx +243 -0
  87. package/src/components/TextToolbar.tsx +164 -0
  88. package/src/components/Toolbar.tsx +86 -0
  89. package/src/config/colorConfig.ts +7 -0
  90. package/src/config/fontConfig.ts +23 -0
  91. package/src/context/ProductEditorContext.tsx +829 -0
  92. package/src/hooks/useSwipeNavigate.ts +40 -0
  93. package/src/index.ts +6 -0
  94. package/src/styles/BottomPreviewDisplay.css +65 -0
  95. package/src/styles/FloatingToolbar.css +41 -0
  96. package/src/styles/ImageGallery.css +147 -0
  97. package/src/styles/ImageNavButton.css +35 -0
  98. package/src/styles/ImageZoomModal.css +101 -0
  99. package/src/styles/LayerPanel.css +179 -0
  100. package/src/styles/LoadingOverlay.css +48 -0
  101. package/src/styles/ProductEditor.css +192 -0
  102. package/src/styles/TextToolbar.css +327 -0
  103. package/src/styles/Toolbar.css +125 -0
  104. package/src/styles/index.css +5 -0
  105. package/src/styles/textToolbarSelectStyles.ts +60 -0
  106. package/src/types/ag-psd/index.d.ts +1 -0
  107. package/src/types/fabric.d.ts +14 -0
  108. package/src/types/lodash/index.d.ts +1 -0
  109. package/src/types/psd.d.ts +53 -0
  110. package/src/types.ts +247 -0
  111. package/src/utils/canvasAddText.ts +72 -0
  112. package/src/utils/canvasUtils.ts +406 -0
  113. package/src/utils/deviceUtils.ts +31 -0
  114. package/src/utils/draftTemplateUtils.ts +70 -0
  115. package/src/utils/grid.ts +154 -0
  116. package/src/utils/imageUtils.ts +218 -0
  117. package/src/utils/job.ts +128 -0
  118. package/src/utils/keyboard.ts +28 -0
  119. package/src/utils/previewUtils.ts +43 -0
  120. package/src/utils/psdConverter.ts +595 -0
  121. package/src/utils/psdRenderUtils.ts +453 -0
  122. package/src/utils/renderUtils.ts +44 -0
  123. package/tsconfig.json +17 -0
@@ -0,0 +1,595 @@
1
+ import { fabric } from 'fabric';
2
+ import { readPsd } from 'ag-psd';
3
+ import { generateUniqueId } from './job';
4
+ import { SavedCanvasObject } from '../types';
5
+
6
+ // Extended Canvas interface with our custom properties
7
+ interface ExtendedCanvas extends fabric.Canvas {
8
+ designBounds?: {
9
+ left: number;
10
+ top: number;
11
+ right: number;
12
+ bottom: number;
13
+ width: number;
14
+ height: number;
15
+ };
16
+ psdUrl?: string;
17
+ anythingABC?: string;
18
+ }
19
+
20
+ /**
21
+ * Extracts layers from a PSD file and renders them to a PNG
22
+ *
23
+ * @param psdUrl URL to the PSD file
24
+ * @returns Promise that resolves to a PNG data URL
25
+ */
26
+ export const extractPsdBaseLayer = async (psdUrl: string): Promise<{ dataUrl: string, designBounds?: { left: number, top: number, right: number, bottom: number } }> => {
27
+ try {
28
+
29
+ // Fetch the PSD file
30
+ const response = await fetch(psdUrl, {
31
+ mode: 'cors',
32
+ credentials: 'same-origin',
33
+ headers: {
34
+ 'Accept': '*/*',
35
+ }
36
+ });
37
+
38
+ if (!response.ok) {
39
+ throw new Error(`Failed to fetch PSD file: ${response.status} ${response.statusText}`);
40
+ }
41
+
42
+ const arrayBuffer = await response.arrayBuffer();
43
+
44
+ // Check if we actually got a PSD file (PSD files start with '8BPS')
45
+ const firstBytes = new Uint8Array(arrayBuffer, 0, 4);
46
+ const signature =
47
+ String.fromCharCode(firstBytes[0]) +
48
+ String.fromCharCode(firstBytes[1]) +
49
+ String.fromCharCode(firstBytes[2]) +
50
+ String.fromCharCode(firstBytes[3]);
51
+
52
+ if (signature !== '8BPS') {
53
+ // TODO handle different file types
54
+ console.warn('Warning: File does not have PSD signature. Got:', signature);
55
+ // Continue anyway - the server might be returning a different format
56
+ }
57
+
58
+ try {
59
+ // Use ag-psd to read the PSD file
60
+ const psd = readPsd(
61
+ arrayBuffer,
62
+ {
63
+ skipCompositeImageData: false,
64
+ skipLayerImageData: false,
65
+ skipThumbnail: false
66
+ }
67
+ );
68
+
69
+ // Create a canvas to draw the layers
70
+ const canvas = document.createElement('canvas');
71
+ canvas.width = psd.width;
72
+ canvas.height = psd.height;
73
+ const ctx = canvas.getContext('2d');
74
+
75
+ if (!ctx) {
76
+ throw new Error('Could not get canvas context');
77
+ }
78
+
79
+ // Variable to store Design layer bounds, if found
80
+ let designBounds: {
81
+ left: number,
82
+ top: number,
83
+ right: number,
84
+ bottom: number
85
+ } | undefined;
86
+
87
+ // If there are no layers or children, try to use the document itself
88
+ if (!psd.children || psd.children.length === 0) {
89
+ if (psd.imageData) {
90
+ // If we have imageData on the main document, use it
91
+ const imageData = new ImageData(
92
+ new Uint8ClampedArray(psd.imageData.data),
93
+ psd.width,
94
+ psd.height
95
+ );
96
+ ctx.putImageData(imageData, 0, 0);
97
+ } else {
98
+ throw new Error('No layers or composite image data found in PSD');
99
+ }
100
+ } else {
101
+ // Categorize and sort layers to handle special layers appropriately
102
+ let layers = [...psd.children];
103
+ let designLayer = null;
104
+ let maskLayer = null;
105
+ let contentLayers: any[] = [];
106
+
107
+ // First pass - identify special layers and regular content layers
108
+ for (const layer of layers) {
109
+ if (layer.name === 'Design') {
110
+ designLayer = layer;
111
+ // Store design layer bounds for future reference
112
+ if (typeof layer.left === 'number' &&
113
+ typeof layer.top === 'number' &&
114
+ typeof layer.right === 'number' &&
115
+ typeof layer.bottom === 'number') {
116
+ designBounds = {
117
+ left: layer.left,
118
+ top: layer.top,
119
+ right: layer.right,
120
+ bottom: layer.bottom
121
+ };
122
+ }
123
+ } else if (layer.name === 'Mask') {
124
+ maskLayer = layer;
125
+ } else {
126
+ contentLayers.push(layer);
127
+ }
128
+ }
129
+
130
+ // Function to render a single layer
131
+ const renderLayer = (layer: any, ctx: CanvasRenderingContext2D) => {
132
+ // Check if the layer has a canvas
133
+ if (layer.canvas &&
134
+ typeof layer.left === 'number' &&
135
+ typeof layer.top === 'number') {
136
+
137
+ // Apply layer blending and opacity
138
+ ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
139
+
140
+ // Draw this layer onto the main canvas at its position
141
+ ctx.drawImage(
142
+ layer.canvas,
143
+ layer.left,
144
+ layer.top
145
+ );
146
+
147
+ // Reset alpha
148
+ ctx.globalAlpha = 1;
149
+ } else if (layer.imageData &&
150
+ typeof layer.left === 'number' &&
151
+ typeof layer.top === 'number') {
152
+
153
+ // Create a temporary canvas for this layer
154
+ const layerCanvas = document.createElement('canvas');
155
+ const width = layer.right - layer.left;
156
+ const height = layer.bottom - layer.top;
157
+ layerCanvas.width = width;
158
+ layerCanvas.height = height;
159
+ const layerCtx = layerCanvas.getContext('2d');
160
+
161
+ if (layerCtx && layer.imageData) {
162
+ // Create an ImageData object from the PSD image data
163
+ const imageData = new ImageData(
164
+ new Uint8ClampedArray(layer.imageData.data),
165
+ width,
166
+ height
167
+ );
168
+
169
+ // Put the layer's image data on this canvas
170
+ layerCtx.putImageData(imageData, 0, 0);
171
+
172
+ // Apply layer blending and opacity
173
+ ctx.globalAlpha = layer.opacity !== undefined ? layer.opacity : 1;
174
+
175
+ // Draw this layer onto the main canvas at its position
176
+ ctx.drawImage(
177
+ layerCanvas,
178
+ layer.left,
179
+ layer.top
180
+ );
181
+
182
+ // Reset alpha
183
+ ctx.globalAlpha = 1;
184
+ }
185
+ } else {
186
+ console.warn(`Layer "${layer.name}" has no canvas or imageData`);
187
+ }
188
+ };
189
+
190
+ // Function to recursively process layers and their children
191
+ const processLayer = (layer: any, ctx: CanvasRenderingContext2D) => {
192
+ // Skip hidden layers
193
+ if (layer.hidden) {
194
+ return;
195
+ }
196
+
197
+ // If this is a group layer with children, process its children
198
+ if (layer.children && layer.children.length > 0) {
199
+ // Process children
200
+ const children = [...layer.children];
201
+ children.forEach(child => processLayer(child, ctx));
202
+ return;
203
+ }
204
+
205
+ // Render the actual layer
206
+ renderLayer(layer, ctx);
207
+ };
208
+
209
+ // Render in the correct order:
210
+ // 1. First render the "Design" layer (margins/boundaries)
211
+ if (designLayer) {
212
+ processLayer(designLayer, ctx);
213
+ }
214
+
215
+ // 2. Then render all content layers
216
+ for (const layer of contentLayers) {
217
+ processLayer(layer, ctx);
218
+ }
219
+
220
+ // 3. Finally render the "Mask" layer on top to hide overflow
221
+ if (maskLayer) {
222
+ processLayer(maskLayer, ctx);
223
+ }
224
+ }
225
+
226
+ const dataUrl = canvas.toDataURL('image/png');
227
+
228
+ // Return both the data URL and the design bounds if found
229
+ return { dataUrl, designBounds };
230
+ } catch (parseError) {
231
+ // Try an alternative fallback approach - direct image extraction
232
+ // Create a blob from the buffer and create an object URL
233
+ const blob = new Blob([arrayBuffer], {
234
+ type: response.headers.get('content-type') || 'application/octet-stream'
235
+ });
236
+ const objectUrl = URL.createObjectURL(blob);
237
+
238
+ // Try loading as a regular image if the server actually returned an image
239
+ const img = new Image();
240
+
241
+ return new Promise((resolve, reject) => {
242
+ img.onload = () => {
243
+ const canvas = document.createElement('canvas');
244
+ canvas.width = img.width;
245
+ canvas.height = img.height;
246
+ const ctx = canvas.getContext('2d');
247
+
248
+ if (!ctx) {
249
+ reject(new Error('Could not get canvas context'));
250
+ return;
251
+ }
252
+
253
+ ctx.drawImage(img, 0, 0);
254
+ const dataUrl = canvas.toDataURL('image/png');
255
+ URL.revokeObjectURL(objectUrl);
256
+ resolve({ dataUrl });
257
+ };
258
+
259
+ img.onerror = () => {
260
+ URL.revokeObjectURL(objectUrl);
261
+ reject(new Error('Could not parse PSD or load as image'));
262
+ };
263
+
264
+ img.src = objectUrl;
265
+ });
266
+ }
267
+ } catch (error) {
268
+ throw error;
269
+ }
270
+ };
271
+
272
+ /**
273
+ * Loads a PSD file onto a fabric.js canvas
274
+ *
275
+ * @param canvas fabric.js Canvas instance
276
+ * @param psdUrl URL to the PSD file
277
+ * @param width Canvas width
278
+ * @param height Canvas height
279
+ * @returns Promise that resolves when the image is added to the canvas
280
+ */
281
+ export const loadPsdOntoCanvas = async (
282
+ canvas: ExtendedCanvas,
283
+ psdUrl: string,
284
+ variations: any[],
285
+ savedObjects: SavedCanvasObject[],
286
+ width: number,
287
+ height: number
288
+ ): Promise<void> => {
289
+ try {
290
+ // Store the canvas reference in a variable for checking
291
+ const canvasRef = canvas;
292
+
293
+ // Store the current psdUrl on the canvas to help track which operation is active
294
+ canvasRef.psdUrl = psdUrl;
295
+
296
+ // Get the rendered PSD and design bounds if available
297
+ const { dataUrl, designBounds } = await extractPsdBaseLayer(psdUrl);
298
+
299
+ return new Promise((resolve, reject) => {
300
+ // Check if the canvas is still the same one that requested this operation
301
+ if (!canvasRef || canvasRef.psdUrl !== psdUrl) {
302
+ console.warn('Canvas changed or disposed during PSD processing, aborting.');
303
+ resolve();
304
+ return;
305
+ }
306
+
307
+ fabric.Image.fromURL(dataUrl, (img) => {
308
+ // Check again if the canvas is still valid
309
+ if (!canvasRef || canvasRef.psdUrl !== psdUrl) {
310
+ console.warn('Canvas changed or disposed during image loading, aborting.');
311
+ resolve();
312
+ return;
313
+ }
314
+
315
+ if (!img || !img.width || !img.height) {
316
+ reject(new Error('Failed to create fabric image from PSD'));
317
+ return;
318
+ }
319
+
320
+ // Scale image to fit canvas
321
+ const scale = Math.min(
322
+ width / img.width,
323
+ height / img.height
324
+ );
325
+
326
+ // Center the image - important for design bounds calculation
327
+ const imgLeft = (width - img.width * scale) / 2;
328
+ const imgTop = (height - img.height * scale) / 2;
329
+
330
+ // Set image scale and position
331
+ img.scaleX = scale;
332
+ img.scaleY = scale;
333
+ img.left = imgLeft;
334
+ img.top = imgTop;
335
+ img.selectable = false;
336
+ img.evented = false;
337
+
338
+ // Add image to canvas - check canvas validity first
339
+ if (!canvasRef || canvasRef.psdUrl !== psdUrl) {
340
+ console.warn('Canvas changed or disposed before adding image, aborting.');
341
+ resolve();
342
+ return;
343
+ }
344
+
345
+ canvasRef.add(img);
346
+
347
+ // If we have design bounds, store them for reference
348
+ if (designBounds) {
349
+ // Calculate scaled design bounds
350
+ const scaledDesignBounds = {
351
+ left: imgLeft + (designBounds.left * scale),
352
+ top: imgTop + (designBounds.top * scale),
353
+ width: (designBounds.right - designBounds.left) * scale,
354
+ height: (designBounds.bottom - designBounds.top) * scale,
355
+ right: imgLeft + (designBounds.right * scale),
356
+ bottom: imgTop + (designBounds.bottom * scale)
357
+ };
358
+
359
+ // Check canvas validity before continuing
360
+ if (!canvasRef || canvasRef.psdUrl !== psdUrl) {
361
+ console.warn('Canvas changed or disposed before adding design bounds, aborting.');
362
+ resolve();
363
+ return;
364
+ }
365
+
366
+ // Store on the canvas for future reference
367
+ canvasRef.designBounds = scaledDesignBounds;
368
+
369
+ // Add a visual indicator of the design area
370
+ const boundaryRect = new fabric.Rect(({
371
+ left: scaledDesignBounds.left,
372
+ top: scaledDesignBounds.top,
373
+ width: scaledDesignBounds.width,
374
+ height: scaledDesignBounds.height,
375
+ fill: 'transparent',
376
+ stroke: 'rgba(255, 0, 0, 0.5)',
377
+ strokeWidth: 1,
378
+ strokeDashArray: [5, 5],
379
+ selectable: false,
380
+ evented: false,
381
+ id: 'design-boundary-rect'
382
+ } as any));
383
+
384
+ // Check canvas validity before adding rect
385
+ if (!canvasRef || canvasRef.psdUrl !== psdUrl) {
386
+ console.warn('Canvas changed or disposed before adding boundary rect, aborting.');
387
+ resolve();
388
+ return;
389
+ }
390
+
391
+ canvasRef.add(boundaryRect);
392
+
393
+ // Create a clip path for the text based on design bounds
394
+ const clipPath = new fabric.Rect({
395
+ left: scaledDesignBounds.left,
396
+ top: scaledDesignBounds.top,
397
+ width: scaledDesignBounds.width,
398
+ height: scaledDesignBounds.height,
399
+ absolutePositioned: true
400
+ });
401
+
402
+ // Convert variations into canvas objects which obay the design bounds here
403
+ if (variations && variations.length > 0) {
404
+ // Process each variation to create appropriate canvas objects
405
+ variations.forEach(variation => {
406
+ if (!variation) {
407
+ return; // Skip invalid variations
408
+ }
409
+
410
+ // Get common positioning within design bounds
411
+ const objectLeft = scaledDesignBounds.left + scaledDesignBounds.width / 2;
412
+ const objectTop = scaledDesignBounds.top + scaledDesignBounds.height / 2;
413
+
414
+ // Extract the field type and value from the variation
415
+ const { canvasObjectType, file, fieldId, fontFamily, fontSize } = variation;
416
+ const value = variation.value;
417
+
418
+ // Check if this variation exists in savedObjects
419
+ const savedObject = savedObjects.find(obj => obj.fieldId === fieldId);
420
+
421
+ // Create canvas
422
+ let canvasObject;
423
+
424
+ // Handle text variations
425
+ if (canvasObjectType === 'text') {
426
+ // Create the text object with either saved or defaults
427
+ if (savedObject) {
428
+ // Create text with default first
429
+ canvasObject = new fabric.IText(value?.toString() || 'Text', {
430
+ ...savedObject,
431
+ left: savedObject.left || objectLeft,
432
+ top: savedObject.top || objectTop,
433
+ fontFamily: savedObject.fontFamily || fontFamily,
434
+ fontSize: savedObject.fontSize || fontSize,
435
+ fill: savedObject.fill || '#000000',
436
+ originX: savedObject.originX || 'center',
437
+ originY: savedObject.originY ||'center',
438
+ selectable: true,
439
+ editable: true,
440
+ text: value?.toString(), // Always use the current variation value for text
441
+ scaleX: savedObject.scaleX || 1,
442
+ scaleY: savedObject.scaleY || 1,
443
+ angle: savedObject.angle || 0,
444
+ });
445
+ } else {
446
+ // Create with default properties
447
+ canvasObject = new fabric.IText(value?.toString() || 'Text', {
448
+ left: objectLeft,
449
+ top: objectTop,
450
+ fontFamily,
451
+ fontSize,
452
+ fill: '#000000',
453
+ originX: 'center',
454
+ originY: 'center',
455
+ selectable: true,
456
+ editable: false,
457
+ text: value?.toString(),
458
+ });
459
+ }
460
+
461
+ // Store fieldId as a custom property
462
+ (canvasObject as any).fieldId = fieldId;
463
+ // Store uniqueId if it exists in the saved object
464
+ (canvasObject as any).uniqueId = savedObject?.uniqueId || generateUniqueId();
465
+
466
+ // Apply the clip path to the text
467
+ // Omit clip path for now to restore movability
468
+ canvasObject.clipPath = clipPath;
469
+
470
+ canvasRef.add(canvasObject);
471
+ }
472
+ // Handle image variations
473
+ else if (canvasObjectType === "image") {
474
+ // Get the file ID for comparison with saved objects
475
+ const fileId = file?.id;
476
+ // Find by both fieldId and fileId for images
477
+ const savedImageObject = fileId ?
478
+ savedObjects.find(obj => obj.fieldId === fieldId && obj.fileId === fileId) :
479
+ savedObject;
480
+
481
+ if (file && file.viewUrl) {
482
+ fabric.Image.fromURL(file.viewUrl, (img) => {
483
+ if (!img) return;
484
+
485
+ // Default scaling to fit design bounds
486
+ const maxWidth = scaledDesignBounds.width * 0.8;
487
+ const maxHeight = scaledDesignBounds.height * 0.8;
488
+ const defaultScale = Math.min(
489
+ maxWidth / img.width!,
490
+ maxHeight / img.height!,
491
+ 1
492
+ );
493
+
494
+ if (savedImageObject) {
495
+ // Apply saved properties
496
+ img.set({
497
+ ...savedImageObject,
498
+ left: savedImageObject.left || objectLeft,
499
+ top: savedImageObject.top || objectTop,
500
+ scaleX: savedImageObject.scaleX || defaultScale,
501
+ scaleY: savedImageObject.scaleY || defaultScale,
502
+ angle: savedImageObject.angle || 0,
503
+ originX: savedImageObject.originX || 'center',
504
+ originY: savedImageObject.originY || 'center',
505
+ cornerSize: 8,
506
+ borderColor: '#303DBF',
507
+ cornerColor: '#303DBF',
508
+ cornerStrokeColor: '#303DBF',
509
+ transparentCorners: false,
510
+ selectable: true,
511
+ crossOrigin: 'anonymous',
512
+ });
513
+
514
+ // Store uniqueId if it exists
515
+ (img as any).uniqueId = savedImageObject?.uniqueId || generateUniqueId();
516
+
517
+ } else {
518
+ // Apply default properties
519
+ img.scale(defaultScale);
520
+ img.set({
521
+ left: objectLeft,
522
+ top: objectTop,
523
+ originX: 'center',
524
+ originY: 'center',
525
+ cornerSize: 8,
526
+ borderColor: '#303DBF',
527
+ cornerColor: '#303DBF',
528
+ cornerStrokeColor: '#303DBF',
529
+ transparentCorners: false,
530
+ selectable: true,
531
+ crossOrigin: 'anonymous',
532
+ });
533
+ }
534
+
535
+ // Store fieldId and fileId as custom properties
536
+ (img as any).fieldId = fieldId;
537
+ if (fileId) {
538
+ (img as any).fileId = fileId;
539
+ }
540
+ (img as any).uniqueId = generateUniqueId();
541
+
542
+ // Apply clip path
543
+ const imageClipPath = new fabric.Rect({
544
+ left: scaledDesignBounds.left,
545
+ top: scaledDesignBounds.top,
546
+ width: scaledDesignBounds.width,
547
+ height: scaledDesignBounds.height,
548
+ absolutePositioned: true
549
+ });
550
+ img.clipPath = imageClipPath;
551
+
552
+ // Check canvas validity before adding
553
+ if (!canvasRef || canvasRef.psdUrl !== psdUrl) {
554
+ return;
555
+ }
556
+ canvasRef.add(img);
557
+ canvasRef.renderAll();
558
+ }, { crossOrigin: 'anonymous' });
559
+ }
560
+ }
561
+ });
562
+ }
563
+
564
+ // Check canvas validity before adding text
565
+ if (!canvasRef || canvasRef.psdUrl !== psdUrl) {
566
+ console.warn('Canvas changed or disposed before adding test text, aborting.');
567
+ resolve();
568
+ return;
569
+ }
570
+ }
571
+
572
+ // Final check before renderAll
573
+ if (!canvasRef || canvasRef.psdUrl !== psdUrl) {
574
+ console.warn('Canvas changed or disposed before final render, aborting.');
575
+ resolve();
576
+ return;
577
+ }
578
+
579
+ // Try to render the canvas, with error handling
580
+ try {
581
+ if (canvasRef && canvasRef.renderAll) {
582
+ canvasRef.renderAll();
583
+ }
584
+ } catch (e) {
585
+ console.error('Error rendering canvas:', e);
586
+ }
587
+
588
+ resolve();
589
+ }, { crossOrigin: 'anonymous' });
590
+ });
591
+ } catch (error) {
592
+ console.error('Error loading PSD onto canvas:', error);
593
+ throw error;
594
+ }
595
+ };