embroidery-qc-image 1.0.7 → 1.0.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/dist/index.esm.js CHANGED
@@ -35,37 +35,68 @@ styleInject(css_248z);
35
35
  // CONSTANTS
36
36
  // ============================================================================
37
37
  const COLOR_MAP = {
38
- "Army (1394)": "#4B5320", Army: "#4B5320",
39
- "Black (8)": "#000000", Black: "#000000",
40
- "Bubblegum (1309)": "#FFC1CC", Bubblegum: "#FFC1CC",
41
- "Carolina Blue (1274)": "#7BAFD4", "Carolina Blue": "#7BAFD4",
42
- "Celadon (1098)": "#ACE1AF", Celadon: "#ACE1AF",
43
- "Coffee Bean (1145)": "#6F4E37", "Coffee Bean": "#6F4E37",
44
- "Daffodil (1180)": "#FFFF31", Daffodil: "#FFFF31",
45
- "Dark Gray (1131)": "#A9A9A9", "Dark Gray": "#A9A9A9",
46
- "Doe Skin Beige (1344)": "#F5E6D3", "Doe Skin Beige": "#F5E6D3",
47
- "Dusty Blue (1373)": "#6699CC", "Dusty Blue": "#6699CC",
48
- "Forest Green (1397)": "#228B22", "Forest Green": "#228B22",
49
- "Gold (1425)": "#FFD700", Gold: "#FFD700",
50
- "Gray (1118)": "#808080", Gray: "#808080",
51
- "Ivory (1072)": "#FFFFF0", Ivory: "#FFFFF0",
52
- "Lavender (1032)": "#E6E6FA", Lavender: "#E6E6FA",
53
- "Light Denim (1133)": "#B0C4DE", "Light Denim": "#B0C4DE",
54
- "Light Salmon (1018)": "#FFA07A", "Light Salmon": "#FFA07A",
55
- "Maroon (1374)": "#800000", Maroon: "#800000",
56
- "Navy Blue (1044)": "#000080", "Navy Blue": "#000080",
57
- "Olive Green (1157)": "#556B2F", "Olive Green": "#556B2F",
58
- "Orange (1278)": "#FFA500", Orange: "#FFA500",
59
- "Peach Blush (1053)": "#FFCCCB", "Peach Blush": "#FFCCCB",
60
- "Pink (1148)": "#FFC0CB", Pink: "#FFC0CB",
61
- "Purple (1412)": "#800080", Purple: "#800080",
62
- "Red (1037)": "#FF0000", Red: "#FF0000",
63
- "Silver Sage (1396)": "#A8A8A8", "Silver Sage": "#A8A8A8",
64
- "Summer Sky (1432)": "#87CEEB", "Summer Sky": "#87CEEB",
65
- "Terra Cotta (1477)": "#E2725B", "Terra Cotta": "#E2725B",
66
- "Sand (1055)": "#F4A460", Sand: "#F4A460",
67
- "White (9)": "#FFFFFF", White: "#FFFFFF",
38
+ "Army (1394)": "#4B5320",
39
+ Army: "#4B5320",
40
+ "Black (8)": "#000000",
41
+ Black: "#000000",
42
+ "Bubblegum (1309)": "#FFC1CC",
43
+ Bubblegum: "#FFC1CC",
44
+ "Carolina Blue (1274)": "#7BAFD4",
45
+ "Carolina Blue": "#7BAFD4",
46
+ "Celadon (1098)": "#ACE1AF",
47
+ Celadon: "#ACE1AF",
48
+ "Coffee Bean (1145)": "#6F4E37",
49
+ "Coffee Bean": "#6F4E37",
50
+ "Daffodil (1180)": "#FFFF31",
51
+ Daffodil: "#FFFF31",
52
+ "Dark Gray (1131)": "#A9A9A9",
53
+ "Dark Gray": "#A9A9A9",
54
+ "Doe Skin Beige (1344)": "#F5E6D3",
55
+ "Doe Skin Beige": "#F5E6D3",
56
+ "Dusty Blue (1373)": "#6699CC",
57
+ "Dusty Blue": "#6699CC",
58
+ "Forest Green (1397)": "#228B22",
59
+ "Forest Green": "#228B22",
60
+ "Gold (1425)": "#FFD700",
61
+ Gold: "#FFD700",
62
+ "Gray (1118)": "#808080",
63
+ Gray: "#808080",
64
+ "Ivory (1072)": "#FFFFF0",
65
+ Ivory: "#FFFFF0",
66
+ "Lavender (1032)": "#E6E6FA",
67
+ Lavender: "#E6E6FA",
68
+ "Light Denim (1133)": "#B0C4DE",
69
+ "Light Denim": "#B0C4DE",
70
+ "Light Salmon (1018)": "#FFA07A",
71
+ "Light Salmon": "#FFA07A",
72
+ "Maroon (1374)": "#800000",
73
+ Maroon: "#800000",
74
+ "Navy Blue (1044)": "#000080",
75
+ "Navy Blue": "#000080",
76
+ "Olive Green (1157)": "#556B2F",
77
+ "Olive Green": "#556B2F",
78
+ "Orange (1278)": "#FFA500",
79
+ Orange: "#FFA500",
80
+ "Peach Blush (1053)": "#FFCCCB",
81
+ "Peach Blush": "#FFCCCB",
82
+ "Pink (1148)": "#FFC0CB",
83
+ Pink: "#FFC0CB",
84
+ "Purple (1412)": "#800080",
85
+ Purple: "#800080",
86
+ "Red (1037)": "#FF0000",
87
+ Red: "#FF0000",
88
+ "Silver Sage (1396)": "#A8A8A8",
89
+ "Silver Sage": "#A8A8A8",
90
+ "Summer Sky (1432)": "#87CEEB",
91
+ "Summer Sky": "#87CEEB",
92
+ "Terra Cotta (1477)": "#E2725B",
93
+ "Terra Cotta": "#E2725B",
94
+ "Sand (1055)": "#F4A460",
95
+ Sand: "#F4A460",
96
+ "White (9)": "#FFFFFF",
97
+ White: "#FFFFFF",
68
98
  };
99
+ const DEFAULT_ERROR_COLOR = "#CC1F1A";
69
100
  const BASE_URLS = {
70
101
  FONT: "https://s3.hn-1.cloud.cmctelecom.vn/god-system-images/embroidery/fonts",
71
102
  ICON: "https://s3.hn-1.cloud.cmctelecom.vn/god-system-images/embroidery/icons",
@@ -128,14 +159,170 @@ const getImageUrl = (type, value) => {
128
159
  return `${BASE_URLS.FLORAL}/${value}.png`;
129
160
  return `${BASE_URLS.THREAD_COLOR}/${value}.png`;
130
161
  };
131
- const loadImage = (url, imageRefs, onLoad) => {
132
- if (imageRefs.current.has(url))
133
- return;
162
+ const getProxyUrl = (url) => `https://proxy-img.c8p.workers.dev?url=${encodeURIComponent(url)}`;
163
+ const ensureImage = (existing) => {
164
+ if (existing && existing.crossOrigin === "anonymous") {
165
+ return existing;
166
+ }
134
167
  const img = new Image();
135
168
  img.crossOrigin = "anonymous";
136
- img.src = url;
137
- img.onload = onLoad;
169
+ img.decoding = "async";
170
+ return img;
171
+ };
172
+ const loadImage = (url, imageRefs, onLoad) => {
173
+ const existing = imageRefs.current.get(url);
174
+ if (existing?.complete &&
175
+ existing.naturalWidth > 0 &&
176
+ existing.crossOrigin === "anonymous") {
177
+ return;
178
+ }
179
+ const img = ensureImage(existing);
138
180
  imageRefs.current.set(url, img);
181
+ let attemptedProxy = existing?.dataset?.proxyUsed === "true";
182
+ const cleanup = () => {
183
+ img.onload = null;
184
+ img.onerror = null;
185
+ };
186
+ img.onload = () => {
187
+ img.dataset.proxyUsed = attemptedProxy ? "true" : "false";
188
+ cleanup();
189
+ onLoad();
190
+ };
191
+ img.onerror = () => {
192
+ if (!attemptedProxy) {
193
+ attemptedProxy = true;
194
+ img.src = getProxyUrl(url);
195
+ return;
196
+ }
197
+ img.dataset.proxyUsed = attemptedProxy ? "true" : "false";
198
+ cleanup();
199
+ onLoad();
200
+ };
201
+ img.src = attemptedProxy ? getProxyUrl(url) : url;
202
+ };
203
+ const loadImageAsync = (url, imageRefs, cacheKey) => {
204
+ const key = cacheKey ?? url;
205
+ const existing = imageRefs.current.get(key) ?? imageRefs.current.get(url);
206
+ if (existing?.complete &&
207
+ existing.naturalWidth > 0 &&
208
+ existing.crossOrigin === "anonymous" &&
209
+ existing.dataset?.proxyUsed !== undefined) {
210
+ if (existing !== imageRefs.current.get(key)) {
211
+ imageRefs.current.set(key, existing);
212
+ }
213
+ if (existing !== imageRefs.current.get(url)) {
214
+ imageRefs.current.set(url, existing);
215
+ }
216
+ return Promise.resolve(existing);
217
+ }
218
+ return new Promise((resolve) => {
219
+ const target = ensureImage(existing);
220
+ if (target !== existing) {
221
+ imageRefs.current.set(key, target);
222
+ imageRefs.current.set(url, target);
223
+ }
224
+ let attemptedProxy = target.dataset.proxyUsed === "true";
225
+ const finalize = () => {
226
+ target.onload = null;
227
+ target.onerror = null;
228
+ if (target.complete && target.naturalWidth > 0) {
229
+ imageRefs.current.set(key, target);
230
+ imageRefs.current.set(url, target);
231
+ }
232
+ target.dataset.proxyUsed = attemptedProxy ? "true" : "false";
233
+ resolve(target);
234
+ };
235
+ target.onload = finalize;
236
+ target.onerror = () => {
237
+ if (!attemptedProxy) {
238
+ attemptedProxy = true;
239
+ target.src = getProxyUrl(url);
240
+ return;
241
+ }
242
+ target.dataset.proxyUsed = attemptedProxy ? "true" : "false";
243
+ finalize();
244
+ };
245
+ const desiredSrc = attemptedProxy ? getProxyUrl(url) : url;
246
+ if (target.src !== desiredSrc) {
247
+ target.src = desiredSrc;
248
+ }
249
+ else if (target.complete && target.naturalWidth > 0) {
250
+ finalize();
251
+ }
252
+ });
253
+ };
254
+ const preloadFonts = async (config) => {
255
+ if (config.error_message || !config.sides?.length)
256
+ return;
257
+ const fonts = new Set();
258
+ config.sides.forEach((side) => {
259
+ side.positions.forEach((position) => {
260
+ if (position.type === "TEXT" && position.font) {
261
+ fonts.add(position.font);
262
+ }
263
+ });
264
+ });
265
+ if (fonts.size === 0)
266
+ return;
267
+ await Promise.all([...fonts].map((font) => loadFont(font)));
268
+ };
269
+ const preloadImages = async (config, imageRefs) => {
270
+ const entries = [];
271
+ const seen = new Set();
272
+ if (config.image_url) {
273
+ entries.push({ url: config.image_url, cacheKey: "mockup" });
274
+ seen.add(config.image_url);
275
+ }
276
+ if (!config.sides?.length) {
277
+ await Promise.all(entries.map(({ url, cacheKey }) => loadImageAsync(url, imageRefs, cacheKey)));
278
+ return;
279
+ }
280
+ config.sides.forEach((side) => {
281
+ side.positions.forEach((position) => {
282
+ if (position.type === "ICON") {
283
+ if (position.icon !== 0) {
284
+ const iconUrl = getImageUrl("icon", position.icon);
285
+ if (!seen.has(iconUrl)) {
286
+ entries.push({ url: iconUrl });
287
+ seen.add(iconUrl);
288
+ }
289
+ }
290
+ position.layer_colors?.forEach((color) => {
291
+ const colorUrl = getImageUrl("threadColor", color);
292
+ if (!seen.has(colorUrl)) {
293
+ entries.push({ url: colorUrl });
294
+ seen.add(colorUrl);
295
+ }
296
+ });
297
+ }
298
+ if (position.type === "TEXT") {
299
+ if (position.floral_pattern) {
300
+ const floralUrl = getImageUrl("floral", position.floral_pattern);
301
+ if (!seen.has(floralUrl)) {
302
+ entries.push({ url: floralUrl });
303
+ seen.add(floralUrl);
304
+ }
305
+ }
306
+ if (position.color) {
307
+ const threadUrl = getImageUrl("threadColor", position.color);
308
+ if (!seen.has(threadUrl)) {
309
+ entries.push({ url: threadUrl });
310
+ seen.add(threadUrl);
311
+ }
312
+ }
313
+ position.character_colors?.forEach((color) => {
314
+ const characterColorUrl = getImageUrl("threadColor", color);
315
+ if (!seen.has(characterColorUrl)) {
316
+ entries.push({ url: characterColorUrl });
317
+ seen.add(characterColorUrl);
318
+ }
319
+ });
320
+ }
321
+ });
322
+ });
323
+ if (entries.length === 0)
324
+ return;
325
+ await Promise.all(entries.map(({ url, cacheKey }) => loadImageAsync(url, imageRefs, cacheKey)));
139
326
  };
140
327
  const wrapText = (ctx, text, x, y, maxWidth, lineHeight) => {
141
328
  const words = text.split(" ");
@@ -163,6 +350,31 @@ const wrapText = (ctx, text, x, y, maxWidth, lineHeight) => {
163
350
  lastLineY: y + (lines.length - 1) * lineHeight,
164
351
  };
165
352
  };
353
+ const buildWrappedLines = (ctx, text, maxWidth) => {
354
+ const words = text.split(" ").filter((word) => word.length > 0);
355
+ if (words.length === 0)
356
+ return [""];
357
+ const lines = [];
358
+ let currentLine = words[0];
359
+ for (let i = 1; i < words.length; i++) {
360
+ const testLine = `${currentLine} ${words[i]}`;
361
+ if (ctx.measureText(testLine).width > maxWidth && currentLine.length > 0) {
362
+ lines.push(currentLine);
363
+ currentLine = words[i];
364
+ }
365
+ else {
366
+ currentLine = testLine;
367
+ }
368
+ }
369
+ lines.push(currentLine);
370
+ return lines;
371
+ };
372
+ const isLightColor = (colorName) => {
373
+ return (colorName === "White" ||
374
+ colorName === "White (9)" ||
375
+ colorName === "Ivory" ||
376
+ colorName === "Ivory (1072)");
377
+ };
166
378
  const wrapTextMultiColor = (ctx, text, colors, x, y, maxWidth, lineHeight) => {
167
379
  const words = text.split(" ");
168
380
  const lines = [];
@@ -183,6 +395,7 @@ const wrapTextMultiColor = (ctx, text, colors, x, y, maxWidth, lineHeight) => {
183
395
  }
184
396
  }
185
397
  lines.push(currentLine);
398
+ const hasLightColor = colors.some(isLightColor);
186
399
  let currentY = y;
187
400
  lines.forEach((line, lineIdx) => {
188
401
  let currentX = x;
@@ -192,7 +405,12 @@ const wrapTextMultiColor = (ctx, text, colors, x, y, maxWidth, lineHeight) => {
192
405
  const globalCharIdx = startCharIdx + i;
193
406
  const colorIndex = globalCharIdx % colors.length;
194
407
  const color = colors[colorIndex];
195
- ctx.fillStyle = COLOR_MAP[color] || "#000000";
408
+ if (hasLightColor) {
409
+ ctx.fillStyle = LAYOUT.LABEL_COLOR;
410
+ }
411
+ else {
412
+ ctx.fillStyle = COLOR_MAP[color] || LAYOUT.LABEL_COLOR;
413
+ }
196
414
  ctx.fillText(char, currentX, currentY);
197
415
  currentX += ctx.measureText(char).width;
198
416
  }
@@ -225,7 +443,7 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
225
443
  // Load fonts
226
444
  useEffect(() => {
227
445
  const loadFonts = async () => {
228
- if (!config.sides?.length)
446
+ if (config.error_message || !config.sides?.length)
229
447
  return;
230
448
  const fontsToLoad = new Set();
231
449
  config.sides.forEach((side) => {
@@ -251,23 +469,12 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
251
469
  }, [config.sides, loadedFonts]);
252
470
  // Load images
253
471
  useEffect(() => {
254
- if (!config.sides?.length)
472
+ if (config.error_message || !config.sides?.length)
255
473
  return;
256
474
  const incrementCounter = () => setImagesLoaded((prev) => prev + 1);
257
475
  // Load mockup
258
476
  if (config.image_url) {
259
- const loadMockup = (useCors) => {
260
- const img = new Image();
261
- if (useCors)
262
- img.crossOrigin = "anonymous";
263
- img.onload = () => {
264
- imageRefs.current.set("mockup", img);
265
- incrementCounter();
266
- };
267
- img.onerror = () => useCors && loadMockup(false);
268
- img.src = config.image_url;
269
- };
270
- loadMockup(true);
477
+ loadImage(config.image_url, imageRefs, incrementCounter);
271
478
  }
272
479
  // Load all other images
273
480
  config.sides.forEach((side) => {
@@ -297,61 +504,9 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
297
504
  // Render canvas
298
505
  useEffect(() => {
299
506
  const renderCanvas = () => {
300
- if (!canvasRef.current || !config.sides?.length)
301
- return;
302
- const canvas = canvasRef.current;
303
- const ctx = canvas.getContext("2d");
304
- if (!ctx)
305
- return;
306
- canvas.width = canvasSize.width;
307
- canvas.height = canvasSize.height;
308
- // Set text alignment once
309
- ctx.textAlign = LAYOUT.TEXT_ALIGN;
310
- ctx.textBaseline = LAYOUT.TEXT_BASELINE;
311
- // Clear background
312
- ctx.fillStyle = LAYOUT.BACKGROUND_COLOR;
313
- ctx.fillRect(0, 0, canvas.width, canvas.height);
314
- // Collect floral assets
315
- const floralAssets = [];
316
- const seenFlorals = new Set();
317
- config.sides.forEach((side) => {
318
- side.positions.forEach((position) => {
319
- if (position.type === "TEXT" && position.floral_pattern) {
320
- const url = getImageUrl("floral", position.floral_pattern);
321
- if (!seenFlorals.has(url)) {
322
- const img = imageRefs.current.get(url);
323
- if (img?.complete && img.naturalWidth > 0) {
324
- floralAssets.push(img);
325
- seenFlorals.add(url);
326
- }
327
- }
328
- }
329
- });
330
- });
331
- // Calculate scale factor
332
- const measureCanvas = document.createElement("canvas");
333
- measureCanvas.width = canvas.width;
334
- measureCanvas.height = canvas.height;
335
- const measureCtx = measureCanvas.getContext("2d");
336
- if (!measureCtx)
507
+ if (!canvasRef.current)
337
508
  return;
338
- measureCtx.textAlign = LAYOUT.TEXT_ALIGN;
339
- measureCtx.textBaseline = LAYOUT.TEXT_BASELINE;
340
- let measureY = LAYOUT.PADDING;
341
- const measureSpacing = LAYOUT.ELEMENT_SPACING;
342
- config.sides.forEach((side) => {
343
- const sideHeight = renderSide(measureCtx, side, measureY, canvas.width, 1, imageRefs);
344
- measureY += sideHeight + measureSpacing;
345
- });
346
- const scaleFactor = Math.max(0.5, Math.min(1, (canvas.height - LAYOUT.PADDING) / measureY));
347
- // Draw mockup and florals
348
- drawMockupAndFlorals(ctx, canvas, floralAssets, imageRefs);
349
- // Draw content
350
- let currentY = LAYOUT.PADDING * scaleFactor;
351
- config.sides.forEach((side) => {
352
- const sideHeight = renderSide(ctx, side, currentY, canvas.width, scaleFactor, imageRefs);
353
- currentY += sideHeight + measureSpacing * scaleFactor;
354
- });
509
+ renderEmbroideryCanvas(canvasRef.current, config, canvasSize, imageRefs);
355
510
  };
356
511
  const timer = setTimeout(renderCanvas, 100);
357
512
  return () => clearTimeout(timer);
@@ -361,6 +516,102 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
361
516
  // ============================================================================
362
517
  // RENDERING FUNCTIONS
363
518
  // ============================================================================
519
+ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
520
+ const ctx = canvas.getContext("2d");
521
+ if (!ctx)
522
+ return;
523
+ canvas.width = canvasSize.width;
524
+ canvas.height = canvasSize.height;
525
+ ctx.fillStyle = LAYOUT.BACKGROUND_COLOR;
526
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
527
+ if (config.error_message) {
528
+ renderErrorState(ctx, canvas, config.error_message);
529
+ return;
530
+ }
531
+ if (!config.sides?.length)
532
+ return;
533
+ ctx.textAlign = LAYOUT.TEXT_ALIGN;
534
+ ctx.textBaseline = LAYOUT.TEXT_BASELINE;
535
+ if (config.image_url) {
536
+ const mockupImage = imageRefs.current.get(config.image_url);
537
+ if (mockupImage) {
538
+ imageRefs.current.set("mockup", mockupImage);
539
+ }
540
+ }
541
+ const floralAssets = [];
542
+ const seenFlorals = new Set();
543
+ config.sides.forEach((side) => {
544
+ side.positions.forEach((position) => {
545
+ if (position.type === "TEXT" && position.floral_pattern) {
546
+ const url = getImageUrl("floral", position.floral_pattern);
547
+ if (!seenFlorals.has(url)) {
548
+ const img = imageRefs.current.get(url);
549
+ if (img?.complete && img.naturalWidth > 0) {
550
+ floralAssets.push(img);
551
+ seenFlorals.add(url);
552
+ }
553
+ }
554
+ }
555
+ });
556
+ });
557
+ const measureCanvas = document.createElement("canvas");
558
+ measureCanvas.width = canvas.width;
559
+ measureCanvas.height = canvas.height;
560
+ const measureCtx = measureCanvas.getContext("2d");
561
+ if (!measureCtx)
562
+ return;
563
+ measureCtx.textAlign = LAYOUT.TEXT_ALIGN;
564
+ measureCtx.textBaseline = LAYOUT.TEXT_BASELINE;
565
+ let measureY = LAYOUT.PADDING;
566
+ const measureSpacing = LAYOUT.ELEMENT_SPACING;
567
+ config.sides.forEach((side) => {
568
+ const sideHeight = renderSide(measureCtx, side, measureY, canvas.width, 1, imageRefs);
569
+ measureY += sideHeight + measureSpacing;
570
+ });
571
+ const scaleFactor = Math.max(0.5, Math.min(1, (canvas.height - LAYOUT.PADDING) / measureY));
572
+ drawMockupAndFlorals(ctx, canvas, floralAssets, imageRefs);
573
+ let currentY = LAYOUT.PADDING * scaleFactor;
574
+ config.sides.forEach((side) => {
575
+ const sideHeight = renderSide(ctx, side, currentY, canvas.width, scaleFactor, imageRefs);
576
+ currentY += sideHeight + measureSpacing * scaleFactor;
577
+ });
578
+ };
579
+ const renderErrorState = (ctx, canvas, message) => {
580
+ const sanitizedMessage = message.trim() || "Đã xảy ra lỗi";
581
+ const horizontalPadding = LAYOUT.PADDING * 3;
582
+ const maxWidth = canvas.width - horizontalPadding * 2;
583
+ const baseFontSize = LAYOUT.HEADER_FONT_SIZE;
584
+ const minFontSize = 60;
585
+ const centerX = canvas.width / 2;
586
+ ctx.save();
587
+ ctx.textAlign = "center";
588
+ ctx.textBaseline = "top";
589
+ ctx.fillStyle = DEFAULT_ERROR_COLOR;
590
+ let fontSize = baseFontSize;
591
+ let lineGap = LAYOUT.LINE_GAP;
592
+ let lineHeight = fontSize + lineGap;
593
+ const adjustMetrics = () => {
594
+ ctx.font = `bold ${fontSize}px ${LAYOUT.FONT_FAMILY}`;
595
+ lineGap = LAYOUT.LINE_GAP * (fontSize / baseFontSize);
596
+ lineHeight = fontSize + lineGap;
597
+ };
598
+ adjustMetrics();
599
+ let lines = buildWrappedLines(ctx, sanitizedMessage, maxWidth);
600
+ let longestLineWidth = Math.max(...lines.map((line) => ctx.measureText(line).width));
601
+ while (longestLineWidth > maxWidth && fontSize > minFontSize) {
602
+ fontSize = Math.max(minFontSize, Math.floor(fontSize * 0.9));
603
+ adjustMetrics();
604
+ lines = buildWrappedLines(ctx, sanitizedMessage, maxWidth);
605
+ longestLineWidth = Math.max(...lines.map((line) => ctx.measureText(line).width));
606
+ }
607
+ const totalHeight = lines.length * lineHeight;
608
+ const startY = Math.max(LAYOUT.PADDING * 2, canvas.height / 2 - totalHeight / 2);
609
+ lines.forEach((line, index) => {
610
+ const y = startY + index * lineHeight;
611
+ ctx.fillText(line, centerX, y);
612
+ });
613
+ ctx.restore();
614
+ };
364
615
  const drawMockupAndFlorals = (ctx, canvas, floralAssets, imageRefs) => {
365
616
  const mockupImg = imageRefs.current.get("mockup");
366
617
  if (!mockupImg?.complete || !mockupImg.naturalWidth)
@@ -446,7 +697,7 @@ const renderSide = (ctx, side, startY, width, scaleFactor, imageRefs) => {
446
697
  side.positions.forEach((position) => {
447
698
  if (position.type === "ICON") {
448
699
  currentY += renderIconPosition(ctx, position, padding, currentY, sideWidth, scaleFactor, imageRefs);
449
- currentY += LAYOUT.LINE_GAP / 3 * scaleFactor;
700
+ currentY += (LAYOUT.LINE_GAP / 3) * scaleFactor;
450
701
  }
451
702
  });
452
703
  return currentY - startY;
@@ -492,7 +743,9 @@ const computeUniformProperties = (textPositions) => {
492
743
  const fonts = new Set(textPositions.map((p) => p.font));
493
744
  const shapes = new Set(textPositions.map((p) => p.text_shape));
494
745
  const florals = new Set(textPositions.map((p) => p.floral_pattern ?? "None"));
495
- const colors = new Set(textPositions.map((p) => p.character_colors?.length ? p.character_colors.join(",") : p.color ?? "None"));
746
+ const colors = new Set(textPositions.map((p) => p.character_colors?.length
747
+ ? p.character_colors.join(",")
748
+ : p.color ?? "None"));
496
749
  return {
497
750
  values: {
498
751
  font: fonts.size === 1 ? [...fonts][0] : null,
@@ -531,9 +784,13 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
531
784
  const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
532
785
  const result = wrapText(ctx, `Màu chỉ: ${values.color}`, x, cursorY, textMaxWidth, fontSize + lineGap);
533
786
  const swatchH = Math.floor(fontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
534
- const swatchX = x + Math.ceil(result.lastLineWidth) + LAYOUT.ELEMENT_SPACING * scaleFactor;
787
+ const swatchX = x +
788
+ Math.ceil(result.lastLineWidth) +
789
+ LAYOUT.ELEMENT_SPACING * scaleFactor;
535
790
  const swatchY = result.lastLineY + Math.floor(fontSize / 2 - swatchH / 2);
536
- const colors = values.color.includes(",") ? values.color.split(",").map((s) => s.trim()) : [values.color];
791
+ const colors = values.color.includes(",")
792
+ ? values.color.split(",").map((s) => s.trim())
793
+ : [values.color];
537
794
  drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
538
795
  cursorY += result.height;
539
796
  rendered++;
@@ -568,7 +825,7 @@ const renderTextPosition = (ctx, position, x, y, maxWidth, displayIndex, showLab
568
825
  if (isEmptyText) {
569
826
  ctx.font = `${textFontSize}px ${LAYOUT.FONT_FAMILY}`;
570
827
  ctx.fillStyle = LAYOUT.LABEL_COLOR;
571
- const textResult = wrapText(ctx, "không thay đổi", x + labelWidth, currentY, textMaxWidth, textFontSize);
828
+ const textResult = wrapText(ctx, "(không text)", x + labelWidth, currentY, textMaxWidth, textFontSize);
572
829
  currentY += textResult.height;
573
830
  drawnHeight += textResult.height;
574
831
  }
@@ -580,7 +837,8 @@ const renderTextPosition = (ctx, position, x, y, maxWidth, displayIndex, showLab
580
837
  }
581
838
  else {
582
839
  ctx.font = `${textFontSize}px ${position.font}`;
583
- ctx.fillStyle = COLOR_MAP[position.color ?? "None"] || "#000000";
840
+ const isLight = isLightColor(position.color ?? "");
841
+ ctx.fillStyle = isLight ? LAYOUT.LABEL_COLOR : (COLOR_MAP[position.color ?? "None"] || "#000000");
584
842
  const textResult = wrapText(ctx, position.text, x + labelWidth, currentY, textMaxWidth, textFontSize);
585
843
  currentY += textResult.height;
586
844
  drawnHeight += textResult.height;
@@ -605,7 +863,9 @@ const renderTextPosition = (ctx, position, x, y, maxWidth, displayIndex, showLab
605
863
  const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
606
864
  const result = wrapText(ctx, `Màu chỉ: ${colorValue}`, x, currentY, textMaxWidth, otherFontSize + lineGap);
607
865
  const swatchH = Math.floor(otherFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
608
- const swatchX = x + Math.ceil(result.lastLineWidth) + LAYOUT.ELEMENT_SPACING * scaleFactor;
866
+ const swatchX = x +
867
+ Math.ceil(result.lastLineWidth) +
868
+ LAYOUT.ELEMENT_SPACING * scaleFactor;
609
869
  const swatchY = result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
610
870
  const colors = position.character_colors || [position.color];
611
871
  drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
@@ -628,7 +888,9 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
628
888
  ctx.font = `${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
629
889
  ctx.fillStyle = LAYOUT.LABEL_COLOR;
630
890
  let cursorY = y;
631
- const iconText = position.icon === 0 ? `Icon: icon mặc định theo file thêu` : `Icon: ${position.icon}`;
891
+ const iconText = position.icon === 0
892
+ ? `Icon: (icon mặc định theo file thêu)`
893
+ : `Icon: ${position.icon}`;
632
894
  const iconResult = wrapText(ctx, iconText, x, cursorY, maxWidth, iconFontSize + lineGap);
633
895
  // Draw icon image
634
896
  if (position.icon !== 0) {
@@ -637,7 +899,9 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
637
899
  if (img?.complete && img.naturalHeight > 0) {
638
900
  const ratio = img.naturalWidth / img.naturalHeight;
639
901
  const swatchW = Math.max(1, Math.floor(iconFontSize * ratio));
640
- const iconX = x + Math.ceil(iconResult.lastLineWidth) + LAYOUT.ELEMENT_SPACING * scaleFactor;
902
+ const iconX = x +
903
+ Math.ceil(iconResult.lastLineWidth) +
904
+ LAYOUT.ELEMENT_SPACING * scaleFactor;
641
905
  const iconY = iconResult.lastLineY + Math.floor(iconFontSize / 2 - iconFontSize / 2);
642
906
  ctx.drawImage(img, iconX, iconY, swatchW, iconFontSize);
643
907
  }
@@ -648,7 +912,9 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
648
912
  const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
649
913
  const colorResult = wrapText(ctx, `Màu chỉ: ${position.layer_colors.join(", ")}`, x, cursorY, textMaxWidth, iconFontSize + lineGap);
650
914
  const swatchH = Math.floor(iconFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
651
- const swatchX = x + Math.ceil(colorResult.lastLineWidth) + LAYOUT.ELEMENT_SPACING * scaleFactor;
915
+ const swatchX = x +
916
+ Math.ceil(colorResult.lastLineWidth) +
917
+ LAYOUT.ELEMENT_SPACING * scaleFactor;
652
918
  const swatchY = colorResult.lastLineY + Math.floor(iconFontSize / 2 - swatchH / 2);
653
919
  drawSwatches(ctx, position.layer_colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
654
920
  cursorY += colorResult.height;
@@ -656,6 +922,48 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
656
922
  ctx.restore();
657
923
  return cursorY - y;
658
924
  };
925
+ const prepareExportCanvas = async (config, options = {}) => {
926
+ const { width = 4200, height = 4800 } = options;
927
+ const canvas = document.createElement("canvas");
928
+ const imageRefs = {
929
+ current: new Map(),
930
+ };
931
+ await preloadFonts(config);
932
+ await preloadImages(config, imageRefs);
933
+ renderEmbroideryCanvas(canvas, config, { width, height }, imageRefs);
934
+ if (!canvas.width || !canvas.height) {
935
+ return null;
936
+ }
937
+ return canvas;
938
+ };
939
+ const generateEmbroideryQCImageBlob = async (config, options = {}) => {
940
+ if (typeof document === "undefined") {
941
+ throw new Error("generateEmbroideryQCImageBlob requires a browser environment.");
942
+ }
943
+ const { mimeType = "image/png", quality } = options;
944
+ const canvas = await prepareExportCanvas(config, options);
945
+ if (!canvas || typeof canvas.toBlob !== "function") {
946
+ return null;
947
+ }
948
+ const blob = await new Promise((resolve) => {
949
+ canvas.toBlob((result) => resolve(result), mimeType, quality);
950
+ });
951
+ return blob;
952
+ };
953
+ const generateEmbroideryQCImageData = async (config, options = {}) => {
954
+ if (typeof document === "undefined") {
955
+ throw new Error("generateEmbroideryQCImageData requires a browser environment.");
956
+ }
957
+ const { mimeType = "image/png", quality } = options;
958
+ const canvas = await prepareExportCanvas(config, options);
959
+ if (!canvas) {
960
+ return null;
961
+ }
962
+ if (mimeType === "image/png" || typeof quality === "undefined") {
963
+ return canvas.toDataURL(mimeType);
964
+ }
965
+ return canvas.toDataURL(mimeType, quality);
966
+ };
659
967
 
660
- export { EmbroideryQCImage };
968
+ export { EmbroideryQCImage, generateEmbroideryQCImageBlob, generateEmbroideryQCImageData };
661
969
  //# sourceMappingURL=index.esm.js.map