embroidery-qc-image 1.0.0

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/dist/index.js ADDED
@@ -0,0 +1,755 @@
1
+ 'use strict';
2
+
3
+ var jsxRuntime = require('react/jsx-runtime');
4
+ var react = require('react');
5
+
6
+ // Color mapping
7
+ const COLOR_MAP = {
8
+ "Army (1394)": "#4B5320",
9
+ Army: "#4B5320",
10
+ "Black (8)": "#000000",
11
+ Black: "#000000",
12
+ "Bubblegum (1309)": "#FFC1CC",
13
+ Bubblegum: "#FFC1CC",
14
+ "Carolina Blue (1274)": "#7BAFD4",
15
+ "Carolina Blue": "#7BAFD4",
16
+ "Celadon (1098)": "#ACE1AF",
17
+ Celadon: "#ACE1AF",
18
+ "Coffee Bean (1145)": "#6F4E37",
19
+ "Coffee Bean": "#6F4E37",
20
+ "Daffodil (1180)": "#FFFF31",
21
+ Daffodil: "#FFFF31",
22
+ "Dark Gray (1131)": "#A9A9A9",
23
+ "Dark Gray": "#A9A9A9",
24
+ "Doe Skin Beige (1344)": "#F5E6D3",
25
+ "Doe Skin Beige": "#F5E6D3",
26
+ "Dusty Blue (1373)": "#6699CC",
27
+ "Dusty Blue": "#6699CC",
28
+ "Forest Green (1397)": "#228B22",
29
+ "Forest Green": "#228B22",
30
+ "Gold (1425)": "#FFD700",
31
+ Gold: "#FFD700",
32
+ "Gray (1118)": "#808080",
33
+ Gray: "#808080",
34
+ "Ivory (1072)": "#FFFFF0",
35
+ Ivory: "#FFFFF0",
36
+ "Lavender (1032)": "#E6E6FA",
37
+ Lavender: "#E6E6FA",
38
+ "Light Denim (1133)": "#B0C4DE",
39
+ "Light Denim": "#B0C4DE",
40
+ "Light Salmon (1018)": "#FFA07A",
41
+ "Light Salmon": "#FFA07A",
42
+ "Maroon (1374)": "#800000",
43
+ Maroon: "#800000",
44
+ "Navy Blue (1044)": "#000080",
45
+ "Navy Blue": "#000080",
46
+ "Olive Green (1157)": "#556B2F",
47
+ "Olive Green": "#556B2F",
48
+ "Orange (1278)": "#FFA500",
49
+ Orange: "#FFA500",
50
+ "Peach Blush (1053)": "#FFCCCB",
51
+ "Peach Blush": "#FFCCCB",
52
+ "Pink (1148)": "#FFC0CB",
53
+ Pink: "#FFC0CB",
54
+ "Purple (1412)": "#800080",
55
+ Purple: "#800080",
56
+ "Red (1037)": "#FF0000",
57
+ Red: "#FF0000",
58
+ "Silver Sage (1396)": "#A8A8A8",
59
+ "Silver Sage": "#A8A8A8",
60
+ "Summer Sky (1432)": "#87CEEB",
61
+ "Summer Sky": "#87CEEB",
62
+ "Terra Cotta (1477)": "#E2725B",
63
+ "Terra Cotta": "#E2725B",
64
+ "Sand (1055)": "#F4A460",
65
+ Sand: "#F4A460",
66
+ "White (9)": "#FFFFFF",
67
+ White: "#FFFFFF",
68
+ };
69
+ const FONT_BASE_URL = "https://s3.hn-1.cloud.cmctelecom.vn/god-system-images/embroidery/fonts";
70
+ const ICON_BASE_URL = "https://s3.hn-1.cloud.cmctelecom.vn/god-system-images/embroidery/icons";
71
+ const FLORAL_BASE_URL = "https://s3.hn-1.cloud.cmctelecom.vn/god-system-images/embroidery/florals";
72
+ const THREAD_COLOR_BASE_URL = "https://s3.hn-1.cloud.cmctelecom.vn/god-system-images/embroidery/thread-colors";
73
+ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
74
+ const [canvasSize] = react.useState({ width: 4200, height: 4800 });
75
+ const [loadedFonts, setLoadedFonts] = react.useState(new Set());
76
+ const [imagesLoaded, setImagesLoaded] = react.useState(0);
77
+ const canvasRef = react.useRef(null);
78
+ const imageRefs = react.useRef(new Map());
79
+ // Load fonts
80
+ react.useEffect(() => {
81
+ const loadFonts = async () => {
82
+ if (!config.sides || config.sides.length === 0)
83
+ return;
84
+ const fontsToLoad = new Set();
85
+ config.sides.forEach((side) => {
86
+ side.postitions.forEach((position) => {
87
+ if (position.type === "TEXT" && position.font) {
88
+ fontsToLoad.add(position.font);
89
+ }
90
+ });
91
+ });
92
+ for (const fontName of fontsToLoad) {
93
+ if (loadedFonts.has(fontName))
94
+ continue;
95
+ try {
96
+ await loadFont(fontName);
97
+ setLoadedFonts((prev) => new Set(prev).add(fontName));
98
+ }
99
+ catch (error) {
100
+ console.warn(`Could not load font ${fontName}:`, error);
101
+ }
102
+ }
103
+ };
104
+ loadFonts();
105
+ }, [config.sides, loadedFonts]);
106
+ // Load images
107
+ react.useEffect(() => {
108
+ const loadImages = async () => {
109
+ if (!config.sides || config.sides.length === 0)
110
+ return;
111
+ // Load mockup image (not background). It will be drawn at bottom-right.
112
+ if (config.image_url) {
113
+ // Try with CORS first. If it fails (no CORS headers), retry without CORS
114
+ const loadMockup = (useCors) => {
115
+ const img = new Image();
116
+ if (useCors)
117
+ img.crossOrigin = "anonymous";
118
+ img.onload = () => {
119
+ imageRefs.current.set("mockup", img);
120
+ setImagesLoaded((prev) => prev + 1);
121
+ };
122
+ img.onerror = () => {
123
+ if (useCors) {
124
+ // Retry without CORS; canvas may become tainted on export
125
+ loadMockup(false);
126
+ }
127
+ };
128
+ img.src = config.image_url;
129
+ };
130
+ loadMockup(true);
131
+ }
132
+ // Load icons
133
+ config.sides.forEach((side) => {
134
+ side.postitions.forEach((position) => {
135
+ if (position.type === "ICON") {
136
+ const iconUrl = `${ICON_BASE_URL}/${position.icon}.png`;
137
+ if (!imageRefs.current.has(iconUrl)) {
138
+ const img = new Image();
139
+ img.crossOrigin = "anonymous";
140
+ img.src = iconUrl;
141
+ img.onload = () => setImagesLoaded((prev) => prev + 1);
142
+ imageRefs.current.set(iconUrl, img);
143
+ }
144
+ }
145
+ if (position.type === "TEXT" && position.floral_pattern) {
146
+ const floralUrl = `${FLORAL_BASE_URL}/${position.floral_pattern}.png`;
147
+ if (!imageRefs.current.has(floralUrl)) {
148
+ const img = new Image();
149
+ img.crossOrigin = "anonymous";
150
+ img.src = floralUrl;
151
+ img.onload = () => setImagesLoaded((prev) => prev + 1);
152
+ imageRefs.current.set(floralUrl, img);
153
+ }
154
+ }
155
+ // Load thread color images for TEXT positions
156
+ if (position.type === "TEXT") {
157
+ // Load color image if position has color
158
+ if (position.color) {
159
+ const threadColorUrl = `${THREAD_COLOR_BASE_URL}/${position.color}.png`;
160
+ if (!imageRefs.current.has(threadColorUrl)) {
161
+ const img = new Image();
162
+ img.crossOrigin = "anonymous";
163
+ img.src = threadColorUrl;
164
+ img.onload = () => setImagesLoaded((prev) => prev + 1);
165
+ imageRefs.current.set(threadColorUrl, img);
166
+ }
167
+ }
168
+ // Load character color images
169
+ if (position.character_colors &&
170
+ position.character_colors.length > 0) {
171
+ position.character_colors.forEach((color) => {
172
+ const threadColorUrl = `${THREAD_COLOR_BASE_URL}/${color}.png`;
173
+ if (!imageRefs.current.has(threadColorUrl)) {
174
+ const img = new Image();
175
+ img.crossOrigin = "anonymous";
176
+ img.src = threadColorUrl;
177
+ img.onload = () => setImagesLoaded((prev) => prev + 1);
178
+ imageRefs.current.set(threadColorUrl, img);
179
+ }
180
+ });
181
+ }
182
+ }
183
+ // Load thread color images for ICON positions
184
+ if (position.type === "ICON" && position.layer_colors) {
185
+ position.layer_colors.forEach((color) => {
186
+ const threadColorUrl = `${THREAD_COLOR_BASE_URL}/${color}.png`;
187
+ if (!imageRefs.current.has(threadColorUrl)) {
188
+ const img = new Image();
189
+ img.crossOrigin = "anonymous";
190
+ img.src = threadColorUrl;
191
+ img.onload = () => setImagesLoaded((prev) => prev + 1);
192
+ imageRefs.current.set(threadColorUrl, img);
193
+ }
194
+ });
195
+ }
196
+ });
197
+ });
198
+ };
199
+ loadImages();
200
+ }, [config]);
201
+ // Render canvas
202
+ react.useEffect(() => {
203
+ const renderCanvas = () => {
204
+ if (!canvasRef.current || !config.sides || config.sides.length === 0) {
205
+ return;
206
+ }
207
+ const canvas = canvasRef.current;
208
+ const ctx = canvas.getContext("2d");
209
+ if (!ctx)
210
+ return;
211
+ canvas.width = canvasSize.width;
212
+ canvas.height = canvasSize.height;
213
+ // Clear with white background
214
+ ctx.fillStyle = "#FFFFFF";
215
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
216
+ // Collect floral images (for later drawing)
217
+ const floralAssets = [];
218
+ const seenFlorals = new Set();
219
+ if (config.sides) {
220
+ config.sides.forEach((side) => {
221
+ side.postitions.forEach((position) => {
222
+ if (position.type === "TEXT" && position.floral_pattern) {
223
+ const floralUrl = `${FLORAL_BASE_URL}/${position.floral_pattern}.png`;
224
+ if (!seenFlorals.has(floralUrl)) {
225
+ const img = imageRefs.current.get(floralUrl);
226
+ if (img && img.complete && img.naturalWidth > 0) {
227
+ floralAssets.push(img);
228
+ seenFlorals.add(floralUrl);
229
+ }
230
+ }
231
+ }
232
+ });
233
+ });
234
+ }
235
+ // Helper function to draw mockup and florals
236
+ const drawMockupAndFlorals = () => {
237
+ const mockupImg = imageRefs.current.get("mockup");
238
+ const margin = 40; // small padding
239
+ let mockupBox = null;
240
+ if (mockupImg && mockupImg.complete && mockupImg.naturalWidth > 0) {
241
+ const maxTargetWidth = Math.min(1800, canvas.width * 0.375);
242
+ const maxTargetHeight = canvas.height * 0.375;
243
+ const scale = Math.min(maxTargetWidth / mockupImg.naturalWidth, maxTargetHeight / mockupImg.naturalHeight);
244
+ const targetWidth = Math.max(1, Math.floor(mockupImg.naturalWidth * scale));
245
+ const targetHeight = Math.max(1, Math.floor(mockupImg.naturalHeight * scale));
246
+ const targetX = canvas.width - margin - targetWidth;
247
+ const targetY = canvas.height - margin - targetHeight;
248
+ mockupBox = {
249
+ x: targetX,
250
+ y: targetY,
251
+ w: targetWidth,
252
+ h: targetHeight,
253
+ };
254
+ ctx.drawImage(mockupImg, targetX, targetY, targetWidth, targetHeight);
255
+ }
256
+ // Draw florals to the left of mockup
257
+ if (mockupBox && floralAssets.length > 0) {
258
+ const spacing = 300;
259
+ const targetHeight = mockupBox.h;
260
+ const floralFixedH = Math.min(900, targetHeight);
261
+ let currentX = mockupBox.x - spacing;
262
+ for (let i = floralAssets.length - 1; i >= 0; i--) {
263
+ const img = floralAssets[i];
264
+ const ratio = img.naturalWidth / Math.max(1, img.naturalHeight);
265
+ const h = floralFixedH;
266
+ const w = Math.max(1, Math.floor(h * ratio));
267
+ currentX -= w;
268
+ const y = mockupBox.y + (targetHeight - h);
269
+ ctx.drawImage(img, currentX, y, w, h);
270
+ currentX -= spacing;
271
+ }
272
+ }
273
+ };
274
+ // New approach: Draw images first (bottom layer), then text on top
275
+ // This allows text to overlay images when needed
276
+ // Pass 1: Measure actual height with original size (use offscreen canvas for measurement)
277
+ const measureCanvas = document.createElement("canvas");
278
+ measureCanvas.width = canvas.width;
279
+ measureCanvas.height = canvas.height;
280
+ const measureCtx = measureCanvas.getContext("2d");
281
+ if (!measureCtx)
282
+ return;
283
+ // Set up measurement context
284
+ measureCtx.font = ctx.font;
285
+ measureCtx.textAlign = ctx.textAlign;
286
+ measureCtx.textBaseline = ctx.textBaseline;
287
+ let measureY = 40;
288
+ const measureSpacing = 100;
289
+ config.sides.forEach((side) => {
290
+ const sideHeight = renderSide(measureCtx, side, measureY, canvas.width, 1);
291
+ measureY += sideHeight + measureSpacing;
292
+ });
293
+ const totalMeasuredHeight = measureY; // Total height used
294
+ // Calculate scale factor - only scale down when necessary
295
+ // Keep original font sizes (no scale up) - font size is the maximum
296
+ const topPadding = 40;
297
+ // No bottom padding - content can go to bottom, mockup will overlay
298
+ const targetContentHeight = canvas.height - topPadding;
299
+ // Only scale down if content exceeds canvas height
300
+ // Never scale up - preserve original font sizes
301
+ let scaleFactor = 1;
302
+ if (totalMeasuredHeight > targetContentHeight) {
303
+ // Scale down to fit exactly
304
+ scaleFactor = targetContentHeight / totalMeasuredHeight;
305
+ scaleFactor = Math.max(0.5, scaleFactor); // Minimum scale to prevent tiny fonts
306
+ }
307
+ // If content fits, keep scaleFactor = 1 (original font sizes)
308
+ // Draw mockup and florals first (bottom layer)
309
+ drawMockupAndFlorals();
310
+ // Draw content on top (top layer) - text will overlay images if needed
311
+ let currentY = topPadding * scaleFactor;
312
+ config.sides.forEach((side) => {
313
+ const sideHeight = renderSide(ctx, side, currentY, canvas.width, scaleFactor);
314
+ currentY += sideHeight + measureSpacing * scaleFactor;
315
+ });
316
+ };
317
+ // Delay rendering to ensure fonts and images are loaded
318
+ const timer = setTimeout(renderCanvas, 100);
319
+ return () => clearTimeout(timer);
320
+ }, [config, canvasSize, loadedFonts, imagesLoaded]);
321
+ // Helper function to wrap and draw text with word wrapping
322
+ // Returns: { height: number, lastLineWidth: number, lastLineY: number }
323
+ const fillTextWrapped = (ctx, text, x, y, maxWidth, lineHeight) => {
324
+ const words = text.split(" ");
325
+ const lines = [];
326
+ let currentLine = words[0];
327
+ for (let i = 1; i < words.length; i++) {
328
+ const word = words[i];
329
+ const testLine = currentLine + " " + word;
330
+ const metrics = ctx.measureText(testLine);
331
+ if (metrics.width > maxWidth && currentLine.length > 0) {
332
+ lines.push(currentLine);
333
+ currentLine = word;
334
+ }
335
+ else {
336
+ currentLine = testLine;
337
+ }
338
+ }
339
+ lines.push(currentLine);
340
+ let currentY = y;
341
+ lines.forEach((line) => {
342
+ ctx.fillText(line, x, currentY);
343
+ currentY += lineHeight;
344
+ });
345
+ const lastLineWidth = ctx.measureText(lines[lines.length - 1]).width;
346
+ const lastLineY = y + (lines.length - 1) * lineHeight;
347
+ return {
348
+ height: lines.length * lineHeight,
349
+ lastLineWidth,
350
+ lastLineY,
351
+ };
352
+ };
353
+ // Helper to wrap and draw multi-color text (for character_colors)
354
+ const fillTextWrappedMultiColor = (ctx, text, colors, x, y, maxWidth, lineHeight) => {
355
+ const words = text.split(" ");
356
+ const lines = [];
357
+ const lineStartIndices = [0];
358
+ let currentLine = words[0];
359
+ let currentCharIndex = words[0].length;
360
+ for (let i = 1; i < words.length; i++) {
361
+ const word = words[i];
362
+ const testLine = currentLine + " " + word;
363
+ const metrics = ctx.measureText(testLine);
364
+ if (metrics.width > maxWidth && currentLine.length > 0) {
365
+ lines.push(currentLine);
366
+ lineStartIndices.push(currentCharIndex + 1); // +1 for space
367
+ currentLine = word;
368
+ currentCharIndex += word.length + 1;
369
+ }
370
+ else {
371
+ currentLine = testLine;
372
+ currentCharIndex += word.length + 1;
373
+ }
374
+ }
375
+ lines.push(currentLine);
376
+ let currentY = y;
377
+ lines.forEach((line, lineIdx) => {
378
+ let currentX = x;
379
+ const startCharIdx = lineIdx > 0 ? lineStartIndices[lineIdx] : 0;
380
+ for (let i = 0; i < line.length; i++) {
381
+ const char = line[i];
382
+ const globalCharIdx = startCharIdx + i;
383
+ const colorIndex = globalCharIdx % colors.length;
384
+ const color = colors[colorIndex];
385
+ ctx.fillStyle = COLOR_MAP[color] || "#000000";
386
+ ctx.fillText(char, currentX, currentY);
387
+ currentX += ctx.measureText(char).width;
388
+ }
389
+ currentY += lineHeight;
390
+ });
391
+ return lines.length * lineHeight;
392
+ };
393
+ const renderSide = (ctx, side, startY, width, scaleFactor = 1) => {
394
+ let currentY = startY;
395
+ const padding = 40 * scaleFactor;
396
+ const sideWidth = width - 2 * padding;
397
+ const sectionHeight = 200 * scaleFactor;
398
+ // No background section anymore - just white background
399
+ // Group positions by common properties for optimization
400
+ const textGroups = [];
401
+ let currentGroup = null;
402
+ let currentProps = null;
403
+ side.postitions.forEach((position) => {
404
+ if (position.type === "TEXT") {
405
+ if (!currentGroup ||
406
+ currentProps.font !== position.font ||
407
+ currentProps.text_shape !== position.text_shape ||
408
+ currentProps.color !== position.color ||
409
+ currentProps.character_colors?.join(",") !==
410
+ position.character_colors?.join(",")) {
411
+ // Start new group
412
+ if (currentGroup) {
413
+ textGroups.push({
414
+ positions: currentGroup,
415
+ properties: currentProps,
416
+ });
417
+ }
418
+ currentGroup = [position];
419
+ currentProps = {
420
+ font: position.font,
421
+ text_shape: position.text_shape,
422
+ color: position.color,
423
+ character_colors: position.character_colors,
424
+ };
425
+ }
426
+ else {
427
+ currentGroup.push(position);
428
+ }
429
+ }
430
+ });
431
+ if (currentGroup) {
432
+ textGroups.push({ positions: currentGroup, properties: currentProps });
433
+ }
434
+ // Draw side header
435
+ ctx.save();
436
+ const headerFontSize = 200 * scaleFactor;
437
+ ctx.font = `bold ${headerFontSize}px Times New Roman`;
438
+ ctx.fillStyle = "#000000";
439
+ ctx.textAlign = "left";
440
+ ctx.textBaseline = "top";
441
+ const headerResult = fillTextWrapped(ctx, side.print_side.toUpperCase(), padding, currentY, sideWidth, headerFontSize);
442
+ currentY += headerResult.height + 50 * scaleFactor;
443
+ ctx.restore();
444
+ // Compute side-level uniform properties across all TEXT positions
445
+ const allTextPositions = [];
446
+ side.postitions.forEach((position) => {
447
+ if (position.type === "TEXT")
448
+ allTextPositions.push(position);
449
+ });
450
+ const sideFonts = new Set(allTextPositions.map((p) => p.font));
451
+ const sideShapes = new Set(allTextPositions.map((p) => p.text_shape));
452
+ const sideFlorals = new Set(allTextPositions.map((p) => p.floral_pattern ?? "None"));
453
+ const colorKeyOf = (p) => p.character_colors && p.character_colors.length > 0
454
+ ? p.character_colors.join(",")
455
+ : p.color ?? "None";
456
+ const sideColors = new Set(allTextPositions.map((p) => colorKeyOf(p)));
457
+ const sideUniform = {
458
+ font: sideFonts.size === 1,
459
+ shape: sideShapes.size === 1,
460
+ floral: sideFlorals.size === 1,
461
+ color: sideColors.size === 1,
462
+ };
463
+ // Render side-level labels once for uniform properties
464
+ currentY += renderSideUniformLabels(ctx, {
465
+ font: sideUniform.font ? [...sideFonts][0] : null,
466
+ shape: sideUniform.shape ? [...sideShapes][0] : null,
467
+ floral: sideUniform.floral ? [...sideFlorals][0] : null,
468
+ color: sideUniform.color ? [...sideColors][0] : null,
469
+ }, padding, currentY, sideWidth, scaleFactor);
470
+ // Render text groups first
471
+ let sideTextCounter = 1;
472
+ textGroups.forEach((group, groupIndex) => {
473
+ group.positions.forEach((position, index) => {
474
+ if (index === 0 && groupIndex !== 0)
475
+ currentY += 50 * scaleFactor;
476
+ const drawnHeight = renderText(ctx, position, padding, currentY, sideWidth, group.properties, group.positions.length, index, sideTextCounter, {
477
+ font: !sideUniform.font,
478
+ shape: !sideUniform.shape,
479
+ floral: !sideUniform.floral,
480
+ color: !sideUniform.color,
481
+ }, scaleFactor);
482
+ sideTextCounter += 1;
483
+ // add padding only if something was actually drawn
484
+ if (drawnHeight > 0) {
485
+ currentY += drawnHeight + 40 * scaleFactor;
486
+ }
487
+ });
488
+ });
489
+ // Render ICON titles/values (no images here)
490
+ currentY += 30 * scaleFactor; // minimal spacing before icon labels
491
+ side.postitions.forEach((position) => {
492
+ if (position.type === "ICON") {
493
+ currentY += renderIconLabels(ctx, position, padding, currentY, sideWidth, scaleFactor);
494
+ currentY += 10 * scaleFactor;
495
+ }
496
+ });
497
+ return Math.max(currentY - startY, sectionHeight);
498
+ };
499
+ const renderSideUniformLabels = (ctx, values, x, y, maxWidth, scaleFactor = 1) => {
500
+ const labelFontFamily = "Arial";
501
+ const fontSize = 180 * scaleFactor;
502
+ const lineGap = 20 * scaleFactor;
503
+ ctx.save();
504
+ ctx.font = `${fontSize}px ${labelFontFamily}`;
505
+ ctx.textAlign = "left";
506
+ ctx.textBaseline = "top";
507
+ ctx.fillStyle = "#444444";
508
+ let cursorY = y;
509
+ let rendered = 0;
510
+ if (values.font) {
511
+ const fontText = `Font: ${values.font}`;
512
+ const result = fillTextWrapped(ctx, fontText, x, cursorY, maxWidth, fontSize + lineGap);
513
+ cursorY += result.height;
514
+ rendered++;
515
+ }
516
+ if (values.shape && values.shape !== "None") {
517
+ const shapeText = `Kiểu chữ: ${values.shape}`;
518
+ const result = fillTextWrapped(ctx, shapeText, x, cursorY, maxWidth, fontSize + lineGap);
519
+ cursorY += result.height;
520
+ rendered++;
521
+ }
522
+ if (values.color && values.color !== "None") {
523
+ const colorText = `Màu chỉ: ${values.color}`;
524
+ // Reserve space for swatches (estimate: max 5 swatches × 200px each = 1000px)
525
+ const swatchReserved = 1000 * scaleFactor;
526
+ const textMaxWidth = Math.max(400 * scaleFactor, maxWidth - swatchReserved);
527
+ const result = fillTextWrapped(ctx, colorText, x, cursorY, textMaxWidth, fontSize + lineGap);
528
+ // Draw swatches inline for side-level color, preserving aspect ratio; 75% of previous size
529
+ // Position swatches after the last line of wrapped text
530
+ const swatchH = Math.floor(fontSize * 2.025);
531
+ let swatchX = x + Math.ceil(result.lastLineWidth) + 100 * scaleFactor;
532
+ const swatchY = result.lastLineY + Math.floor(fontSize / 2 - swatchH / 2);
533
+ const colorTokens = values.color.includes(",")
534
+ ? values.color.split(",").map((s) => s.trim())
535
+ : [values.color];
536
+ colorTokens.forEach((color) => {
537
+ const threadColorUrl = `${THREAD_COLOR_BASE_URL}/${color}.png`;
538
+ const img = imageRefs.current.get(threadColorUrl);
539
+ if (img && img.complete && img.naturalHeight > 0) {
540
+ const ratio = img.naturalWidth / img.naturalHeight;
541
+ const swatchW = Math.max(1, Math.floor(swatchH * ratio));
542
+ ctx.drawImage(img, swatchX, swatchY, swatchW, swatchH);
543
+ swatchX += swatchW + 25 * scaleFactor;
544
+ }
545
+ });
546
+ cursorY += result.height;
547
+ rendered++;
548
+ }
549
+ if (values.floral && values.floral !== "None") {
550
+ const floralText = `Mẫu hoa: ${values.floral}`;
551
+ const result = fillTextWrapped(ctx, floralText, x, cursorY, maxWidth, fontSize + lineGap);
552
+ cursorY += result.height;
553
+ rendered++;
554
+ }
555
+ if (rendered > 0)
556
+ cursorY += 50 * scaleFactor; // extra gap before first text line
557
+ ctx.restore();
558
+ return cursorY - y;
559
+ };
560
+ const renderText = (ctx, position, x, y, maxWidth, properties, totalTexts, textIndex, displayIndex, showLabels, scaleFactor = 1) => {
561
+ ctx.save();
562
+ // Info labels
563
+ // Unified font sizing for labels and content (side title uses its own larger size)
564
+ const infoLineGap = 30 * scaleFactor;
565
+ const labelFontFamily = "Arial";
566
+ // Use a unified content font size for both labels and text content
567
+ const fontSize = 180 * scaleFactor;
568
+ const infoFontSize = fontSize;
569
+ ctx.font = `${infoFontSize}px ${labelFontFamily}`;
570
+ ctx.textAlign = "left";
571
+ ctx.textBaseline = "top";
572
+ ctx.fillStyle = "#444444";
573
+ let currentYCursor = y;
574
+ let drawnHeight = 0; // accumulate only when something is actually drawn
575
+ // Text value with unified font size
576
+ let displayText = position.text;
577
+ if (position.change_character_to_heart && displayText.includes("<3")) {
578
+ displayText = displayText.replace(/<3/g, "❤");
579
+ }
580
+ ctx.textAlign = "left";
581
+ ctx.textBaseline = "top";
582
+ // Label for text line
583
+ const textLabel = `Text ${displayIndex}: `;
584
+ ctx.font = `${fontSize}px ${labelFontFamily}`;
585
+ const labelWidth = ctx.measureText(textLabel).width;
586
+ ctx.fillStyle = "#444444";
587
+ ctx.fillText(textLabel, x, currentYCursor);
588
+ // Calculate available width for text content
589
+ const textMaxWidth = maxWidth - labelWidth;
590
+ // Handle character_colors (alternating colors)
591
+ if (position.character_colors && position.character_colors.length > 0) {
592
+ // Switch to content font
593
+ ctx.font = `${fontSize}px ${position.font}`;
594
+ const textHeight = fillTextWrappedMultiColor(ctx, displayText, position.character_colors, x + labelWidth, currentYCursor, textMaxWidth, fontSize);
595
+ currentYCursor += textHeight;
596
+ drawnHeight += textHeight;
597
+ }
598
+ else {
599
+ // No color specified
600
+ // Draw text in content font, black (non-bold)
601
+ ctx.font = `${fontSize}px ${position.font}`;
602
+ ctx.fillStyle = COLOR_MAP[position.color ?? "None"] || "#000000";
603
+ const textResult = fillTextWrapped(ctx, displayText, x + labelWidth, currentYCursor, textMaxWidth, fontSize);
604
+ currentYCursor += textResult.height;
605
+ drawnHeight += textResult.height;
606
+ }
607
+ // After text, print Kiểu chữ (when not uniform), then Font and Color as needed
608
+ currentYCursor += infoLineGap;
609
+ ctx.font = `${infoFontSize}px ${labelFontFamily}`;
610
+ ctx.fillStyle = "#444444";
611
+ if (showLabels.shape && position.text_shape) {
612
+ const shapeLabelAfter = `Kiểu chữ: ${position.text_shape}`;
613
+ const result = fillTextWrapped(ctx, shapeLabelAfter, x, currentYCursor, maxWidth, infoFontSize + infoLineGap);
614
+ currentYCursor += result.height;
615
+ drawnHeight += result.height;
616
+ }
617
+ if (showLabels.font && position.font) {
618
+ const fontLabel = `Font: ${position.font}`;
619
+ const result = fillTextWrapped(ctx, fontLabel, x, currentYCursor, maxWidth, infoFontSize + infoLineGap);
620
+ currentYCursor += result.height;
621
+ drawnHeight += result.height;
622
+ }
623
+ if (showLabels.color) {
624
+ let colorLabelValue = "None";
625
+ if (position.character_colors && position.character_colors.length > 0) {
626
+ colorLabelValue = position.character_colors.join(", ");
627
+ }
628
+ else if (position.color) {
629
+ colorLabelValue = position.color;
630
+ }
631
+ if (colorLabelValue !== "None") {
632
+ const colorLabel = `Màu chỉ: ${colorLabelValue}`;
633
+ // Reserve space for swatches
634
+ const swatchReserved = 1000 * scaleFactor;
635
+ const textMaxWidth = Math.max(400 * scaleFactor, maxWidth - swatchReserved);
636
+ const result = fillTextWrapped(ctx, colorLabel, x, currentYCursor, textMaxWidth, infoFontSize + infoLineGap);
637
+ // Draw color swatch images inline with Color label for TEXT, preserve aspect ratio; 75% of previous size
638
+ // Position swatches after the last line of wrapped text
639
+ const swatchH = Math.floor(infoFontSize * 2.025);
640
+ let swatchX = x + Math.ceil(result.lastLineWidth) + 100 * scaleFactor; // spacing after text
641
+ const swatchY = result.lastLineY + Math.floor(infoFontSize / 2 - swatchH / 2);
642
+ if (position.character_colors && position.character_colors.length > 0) {
643
+ position.character_colors.forEach((color) => {
644
+ const threadColorUrl = `${THREAD_COLOR_BASE_URL}/${color}.png`;
645
+ const img = imageRefs.current.get(threadColorUrl);
646
+ if (img && img.complete && img.naturalHeight > 0) {
647
+ const ratio = img.naturalWidth / img.naturalHeight;
648
+ const swatchW = Math.max(1, Math.floor(swatchH * ratio));
649
+ ctx.drawImage(img, swatchX, swatchY, swatchW, swatchH);
650
+ swatchX += swatchW + 25 * scaleFactor;
651
+ }
652
+ });
653
+ }
654
+ else if (position.color) {
655
+ const threadColorUrl = `${THREAD_COLOR_BASE_URL}/${position.color}.png`;
656
+ const img = imageRefs.current.get(threadColorUrl);
657
+ if (img && img.complete && img.naturalHeight > 0) {
658
+ const ratio = img.naturalWidth / img.naturalHeight;
659
+ const swatchW = Math.max(1, Math.floor(swatchH * ratio));
660
+ ctx.drawImage(img, swatchX, swatchY, swatchW, swatchH);
661
+ }
662
+ }
663
+ currentYCursor += result.height;
664
+ drawnHeight += result.height;
665
+ }
666
+ }
667
+ // Show floral label after color block when not uniform at side level
668
+ if (showLabels.floral && position.floral_pattern) {
669
+ const floralText = `Mẫu hoa: ${position.floral_pattern}`;
670
+ const result = fillTextWrapped(ctx, floralText, x, currentYCursor, maxWidth, infoFontSize + infoLineGap);
671
+ currentYCursor += result.height;
672
+ drawnHeight += result.height;
673
+ }
674
+ // (Floral per-position label is printed above the text when needed; avoid duplicate after text)
675
+ ctx.restore();
676
+ return drawnHeight;
677
+ };
678
+ const renderIconLabels = (ctx, position, x, y, maxWidth, scaleFactor = 1) => {
679
+ const labelFontFamily = "Arial";
680
+ const fontSize = 180 * scaleFactor;
681
+ const lineGap = 30 * scaleFactor;
682
+ ctx.save();
683
+ ctx.font = `${fontSize}px ${labelFontFamily}`;
684
+ ctx.textAlign = "left";
685
+ ctx.textBaseline = "top";
686
+ ctx.fillStyle = "#444444";
687
+ let cursorY = y;
688
+ const iconText = `Icon: ${position.icon}`;
689
+ const iconResult = fillTextWrapped(ctx, iconText, x, cursorY, maxWidth, fontSize + lineGap);
690
+ // draw icon image inline with text, preserve aspect ratio; match line height
691
+ const iconUrl = `${ICON_BASE_URL}/${position.icon}.png`;
692
+ {
693
+ const img = imageRefs.current.get(iconUrl);
694
+ if (img && img.complete && img.naturalHeight > 0) {
695
+ const swatchH = fontSize;
696
+ const ratio = img.naturalWidth / img.naturalHeight;
697
+ const swatchW = Math.max(1, Math.floor(swatchH * ratio));
698
+ // Put icon on last line of wrapped text
699
+ const iconX = x + Math.ceil(iconResult.lastLineWidth) + 100 * scaleFactor;
700
+ const iconY = iconResult.lastLineY + Math.floor(fontSize / 2 - swatchH / 2);
701
+ ctx.drawImage(img, iconX, iconY, swatchW, swatchH);
702
+ }
703
+ }
704
+ cursorY += iconResult.height;
705
+ // Draw color line only when there are layer colors
706
+ if (position.layer_colors && position.layer_colors.length > 0) {
707
+ const colorLabelValue = position.layer_colors.join(", ");
708
+ const colorText = `Màu chỉ: ${colorLabelValue}`;
709
+ // Reserve space for swatches
710
+ const swatchReserved = 1000 * scaleFactor;
711
+ const textMaxWidth = Math.max(400 * scaleFactor, maxWidth - swatchReserved);
712
+ const colorResult = fillTextWrapped(ctx, colorText, x, cursorY, textMaxWidth, fontSize + lineGap);
713
+ // Draw color swatch images (only for icon)
714
+ // Position swatches after the last line of wrapped text
715
+ const swatchH = Math.floor(fontSize * 2.025); // 75% of previous size
716
+ let swatchX = x + Math.ceil(colorResult.lastLineWidth) + 100 * scaleFactor; // spacing after text
717
+ const swatchY = colorResult.lastLineY + Math.floor(fontSize / 2 - swatchH / 2);
718
+ position.layer_colors.forEach((color) => {
719
+ const threadColorUrl = `${THREAD_COLOR_BASE_URL}/${color}.png`;
720
+ const img = imageRefs.current.get(threadColorUrl);
721
+ if (img && img.complete && img.naturalHeight > 0) {
722
+ const ratio = img.naturalWidth / img.naturalHeight;
723
+ const swatchW = Math.max(1, Math.floor(swatchH * ratio));
724
+ ctx.drawImage(img, swatchX, swatchY, swatchW, swatchH);
725
+ swatchX += swatchW + 25 * scaleFactor; // spacing between swatches
726
+ }
727
+ });
728
+ cursorY += colorResult.height;
729
+ }
730
+ ctx.restore();
731
+ return cursorY - y;
732
+ };
733
+ const loadFont = (fontName) => {
734
+ return new Promise((resolve, reject) => {
735
+ // Try to load from CDN
736
+ const fontUrl = `${FONT_BASE_URL}/${fontName}.woff2`;
737
+ const fontFace = new FontFace(fontName, `url(${fontUrl})`);
738
+ fontFace
739
+ .load()
740
+ .then((loadedFont) => {
741
+ document.fonts.add(loadedFont);
742
+ resolve();
743
+ })
744
+ .catch(() => {
745
+ // Font loading failed, will use fallback
746
+ console.warn(`Could not load font ${fontName} from CDN`);
747
+ resolve(); // Still resolve to not block rendering
748
+ });
749
+ });
750
+ };
751
+ return (jsxRuntime.jsx("div", { className: `render-embroidery ${className}`, style: style, children: jsxRuntime.jsx("canvas", { ref: canvasRef, className: "render-embroidery-canvas" }) }));
752
+ };
753
+
754
+ exports.EmbroideryQCImage = EmbroideryQCImage;
755
+ //# sourceMappingURL=index.js.map