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