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