embroidery-qc-image 1.0.7 → 1.0.8

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 || !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,25 @@ 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
+ };
168
374
  const wrapTextMultiColor = (ctx, text, colors, x, y, maxWidth, lineHeight) => {
169
375
  const words = text.split(" ");
170
376
  const lines = [];
@@ -227,7 +433,7 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
227
433
  // Load fonts
228
434
  react.useEffect(() => {
229
435
  const loadFonts = async () => {
230
- if (!config.sides?.length)
436
+ if (config.error || !config.sides?.length)
231
437
  return;
232
438
  const fontsToLoad = new Set();
233
439
  config.sides.forEach((side) => {
@@ -253,23 +459,12 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
253
459
  }, [config.sides, loadedFonts]);
254
460
  // Load images
255
461
  react.useEffect(() => {
256
- if (!config.sides?.length)
462
+ if (config.error || !config.sides?.length)
257
463
  return;
258
464
  const incrementCounter = () => setImagesLoaded((prev) => prev + 1);
259
465
  // Load mockup
260
466
  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);
467
+ loadImage(config.image_url, imageRefs, incrementCounter);
273
468
  }
274
469
  // Load all other images
275
470
  config.sides.forEach((side) => {
@@ -299,61 +494,9 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
299
494
  // Render canvas
300
495
  react.useEffect(() => {
301
496
  const renderCanvas = () => {
302
- if (!canvasRef.current || !config.sides?.length)
497
+ if (!canvasRef.current)
303
498
  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)
339
- 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
- });
499
+ renderEmbroideryCanvas(canvasRef.current, config, canvasSize, imageRefs);
357
500
  };
358
501
  const timer = setTimeout(renderCanvas, 100);
359
502
  return () => clearTimeout(timer);
@@ -363,6 +506,102 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
363
506
  // ============================================================================
364
507
  // RENDERING FUNCTIONS
365
508
  // ============================================================================
509
+ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
510
+ const ctx = canvas.getContext("2d");
511
+ if (!ctx)
512
+ return;
513
+ canvas.width = canvasSize.width;
514
+ canvas.height = canvasSize.height;
515
+ ctx.fillStyle = LAYOUT.BACKGROUND_COLOR;
516
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
517
+ if (config.error) {
518
+ renderErrorState(ctx, canvas, config.error);
519
+ return;
520
+ }
521
+ if (!config.sides?.length)
522
+ return;
523
+ ctx.textAlign = LAYOUT.TEXT_ALIGN;
524
+ ctx.textBaseline = LAYOUT.TEXT_BASELINE;
525
+ if (config.image_url) {
526
+ const mockupImage = imageRefs.current.get(config.image_url);
527
+ if (mockupImage) {
528
+ imageRefs.current.set("mockup", mockupImage);
529
+ }
530
+ }
531
+ const floralAssets = [];
532
+ const seenFlorals = new Set();
533
+ config.sides.forEach((side) => {
534
+ side.positions.forEach((position) => {
535
+ if (position.type === "TEXT" && position.floral_pattern) {
536
+ const url = getImageUrl("floral", position.floral_pattern);
537
+ if (!seenFlorals.has(url)) {
538
+ const img = imageRefs.current.get(url);
539
+ if (img?.complete && img.naturalWidth > 0) {
540
+ floralAssets.push(img);
541
+ seenFlorals.add(url);
542
+ }
543
+ }
544
+ }
545
+ });
546
+ });
547
+ const measureCanvas = document.createElement("canvas");
548
+ measureCanvas.width = canvas.width;
549
+ measureCanvas.height = canvas.height;
550
+ const measureCtx = measureCanvas.getContext("2d");
551
+ if (!measureCtx)
552
+ return;
553
+ measureCtx.textAlign = LAYOUT.TEXT_ALIGN;
554
+ measureCtx.textBaseline = LAYOUT.TEXT_BASELINE;
555
+ let measureY = LAYOUT.PADDING;
556
+ const measureSpacing = LAYOUT.ELEMENT_SPACING;
557
+ config.sides.forEach((side) => {
558
+ const sideHeight = renderSide(measureCtx, side, measureY, canvas.width, 1, imageRefs);
559
+ measureY += sideHeight + measureSpacing;
560
+ });
561
+ const scaleFactor = Math.max(0.5, Math.min(1, (canvas.height - LAYOUT.PADDING) / measureY));
562
+ drawMockupAndFlorals(ctx, canvas, floralAssets, imageRefs);
563
+ let currentY = LAYOUT.PADDING * scaleFactor;
564
+ config.sides.forEach((side) => {
565
+ const sideHeight = renderSide(ctx, side, currentY, canvas.width, scaleFactor, imageRefs);
566
+ currentY += sideHeight + measureSpacing * scaleFactor;
567
+ });
568
+ };
569
+ const renderErrorState = (ctx, canvas, message) => {
570
+ const sanitizedMessage = message.trim() || "Đã xảy ra lỗi";
571
+ const horizontalPadding = LAYOUT.PADDING * 3;
572
+ const maxWidth = canvas.width - horizontalPadding * 2;
573
+ const baseFontSize = LAYOUT.HEADER_FONT_SIZE;
574
+ const minFontSize = 60;
575
+ const centerX = canvas.width / 2;
576
+ ctx.save();
577
+ ctx.textAlign = "center";
578
+ ctx.textBaseline = "top";
579
+ ctx.fillStyle = DEFAULT_ERROR_COLOR;
580
+ let fontSize = baseFontSize;
581
+ let lineGap = LAYOUT.LINE_GAP;
582
+ let lineHeight = fontSize + lineGap;
583
+ const adjustMetrics = () => {
584
+ ctx.font = `bold ${fontSize}px ${LAYOUT.FONT_FAMILY}`;
585
+ lineGap = LAYOUT.LINE_GAP * (fontSize / baseFontSize);
586
+ lineHeight = fontSize + lineGap;
587
+ };
588
+ adjustMetrics();
589
+ let lines = buildWrappedLines(ctx, sanitizedMessage, maxWidth);
590
+ let longestLineWidth = Math.max(...lines.map((line) => ctx.measureText(line).width));
591
+ while (longestLineWidth > maxWidth && fontSize > minFontSize) {
592
+ fontSize = Math.max(minFontSize, Math.floor(fontSize * 0.9));
593
+ adjustMetrics();
594
+ lines = buildWrappedLines(ctx, sanitizedMessage, maxWidth);
595
+ longestLineWidth = Math.max(...lines.map((line) => ctx.measureText(line).width));
596
+ }
597
+ const totalHeight = lines.length * lineHeight;
598
+ const startY = Math.max(LAYOUT.PADDING * 2, canvas.height / 2 - totalHeight / 2);
599
+ lines.forEach((line, index) => {
600
+ const y = startY + index * lineHeight;
601
+ ctx.fillText(line, centerX, y);
602
+ });
603
+ ctx.restore();
604
+ };
366
605
  const drawMockupAndFlorals = (ctx, canvas, floralAssets, imageRefs) => {
367
606
  const mockupImg = imageRefs.current.get("mockup");
368
607
  if (!mockupImg?.complete || !mockupImg.naturalWidth)
@@ -448,7 +687,7 @@ const renderSide = (ctx, side, startY, width, scaleFactor, imageRefs) => {
448
687
  side.positions.forEach((position) => {
449
688
  if (position.type === "ICON") {
450
689
  currentY += renderIconPosition(ctx, position, padding, currentY, sideWidth, scaleFactor, imageRefs);
451
- currentY += LAYOUT.LINE_GAP / 3 * scaleFactor;
690
+ currentY += (LAYOUT.LINE_GAP / 3) * scaleFactor;
452
691
  }
453
692
  });
454
693
  return currentY - startY;
@@ -494,7 +733,9 @@ const computeUniformProperties = (textPositions) => {
494
733
  const fonts = new Set(textPositions.map((p) => p.font));
495
734
  const shapes = new Set(textPositions.map((p) => p.text_shape));
496
735
  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"));
736
+ const colors = new Set(textPositions.map((p) => p.character_colors?.length
737
+ ? p.character_colors.join(",")
738
+ : p.color ?? "None"));
498
739
  return {
499
740
  values: {
500
741
  font: fonts.size === 1 ? [...fonts][0] : null,
@@ -533,9 +774,13 @@ const renderUniformLabels = (ctx, uniformProps, x, y, maxWidth, scaleFactor, ima
533
774
  const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
534
775
  const result = wrapText(ctx, `Màu chỉ: ${values.color}`, x, cursorY, textMaxWidth, fontSize + lineGap);
535
776
  const swatchH = Math.floor(fontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
536
- const swatchX = x + Math.ceil(result.lastLineWidth) + LAYOUT.ELEMENT_SPACING * scaleFactor;
777
+ const swatchX = x +
778
+ Math.ceil(result.lastLineWidth) +
779
+ LAYOUT.ELEMENT_SPACING * scaleFactor;
537
780
  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];
781
+ const colors = values.color.includes(",")
782
+ ? values.color.split(",").map((s) => s.trim())
783
+ : [values.color];
539
784
  drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
540
785
  cursorY += result.height;
541
786
  rendered++;
@@ -607,7 +852,9 @@ const renderTextPosition = (ctx, position, x, y, maxWidth, displayIndex, showLab
607
852
  const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
608
853
  const result = wrapText(ctx, `Màu chỉ: ${colorValue}`, x, currentY, textMaxWidth, otherFontSize + lineGap);
609
854
  const swatchH = Math.floor(otherFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
610
- const swatchX = x + Math.ceil(result.lastLineWidth) + LAYOUT.ELEMENT_SPACING * scaleFactor;
855
+ const swatchX = x +
856
+ Math.ceil(result.lastLineWidth) +
857
+ LAYOUT.ELEMENT_SPACING * scaleFactor;
611
858
  const swatchY = result.lastLineY + Math.floor(otherFontSize / 2 - swatchH / 2);
612
859
  const colors = position.character_colors || [position.color];
613
860
  drawSwatches(ctx, colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
@@ -630,7 +877,9 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
630
877
  ctx.font = `${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
631
878
  ctx.fillStyle = LAYOUT.LABEL_COLOR;
632
879
  let cursorY = y;
633
- const iconText = position.icon === 0 ? `Icon: icon mặc định theo file thêu` : `Icon: ${position.icon}`;
880
+ const iconText = position.icon === 0
881
+ ? `Icon: icon mặc định theo file thêu`
882
+ : `Icon: ${position.icon}`;
634
883
  const iconResult = wrapText(ctx, iconText, x, cursorY, maxWidth, iconFontSize + lineGap);
635
884
  // Draw icon image
636
885
  if (position.icon !== 0) {
@@ -639,7 +888,9 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
639
888
  if (img?.complete && img.naturalHeight > 0) {
640
889
  const ratio = img.naturalWidth / img.naturalHeight;
641
890
  const swatchW = Math.max(1, Math.floor(iconFontSize * ratio));
642
- const iconX = x + Math.ceil(iconResult.lastLineWidth) + LAYOUT.ELEMENT_SPACING * scaleFactor;
891
+ const iconX = x +
892
+ Math.ceil(iconResult.lastLineWidth) +
893
+ LAYOUT.ELEMENT_SPACING * scaleFactor;
643
894
  const iconY = iconResult.lastLineY + Math.floor(iconFontSize / 2 - iconFontSize / 2);
644
895
  ctx.drawImage(img, iconX, iconY, swatchW, iconFontSize);
645
896
  }
@@ -650,7 +901,9 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
650
901
  const textMaxWidth = Math.max(LAYOUT.MIN_TEXT_WIDTH * scaleFactor, maxWidth - LAYOUT.SWATCH_RESERVED_SPACE * scaleFactor);
651
902
  const colorResult = wrapText(ctx, `Màu chỉ: ${position.layer_colors.join(", ")}`, x, cursorY, textMaxWidth, iconFontSize + lineGap);
652
903
  const swatchH = Math.floor(iconFontSize * LAYOUT.SWATCH_HEIGHT_RATIO);
653
- const swatchX = x + Math.ceil(colorResult.lastLineWidth) + LAYOUT.ELEMENT_SPACING * scaleFactor;
904
+ const swatchX = x +
905
+ Math.ceil(colorResult.lastLineWidth) +
906
+ LAYOUT.ELEMENT_SPACING * scaleFactor;
654
907
  const swatchY = colorResult.lastLineY + Math.floor(iconFontSize / 2 - swatchH / 2);
655
908
  drawSwatches(ctx, position.layer_colors, swatchX, swatchY, swatchH, scaleFactor, imageRefs);
656
909
  cursorY += colorResult.height;
@@ -658,6 +911,50 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
658
911
  ctx.restore();
659
912
  return cursorY - y;
660
913
  };
914
+ const prepareExportCanvas = async (config, options = {}) => {
915
+ const { width = 4200, height = 4800 } = options;
916
+ const canvas = document.createElement("canvas");
917
+ const imageRefs = {
918
+ current: new Map(),
919
+ };
920
+ await preloadFonts(config);
921
+ await preloadImages(config, imageRefs);
922
+ renderEmbroideryCanvas(canvas, config, { width, height }, imageRefs);
923
+ if (!canvas.width || !canvas.height) {
924
+ return null;
925
+ }
926
+ return canvas;
927
+ };
928
+ const generateEmbroideryQCImageBlob = async (config, options = {}) => {
929
+ if (typeof document === "undefined") {
930
+ throw new Error("generateEmbroideryQCImageBlob requires a browser environment.");
931
+ }
932
+ const { mimeType = "image/png", quality } = options;
933
+ const canvas = await prepareExportCanvas(config, options);
934
+ if (!canvas || typeof canvas.toBlob !== "function") {
935
+ return null;
936
+ }
937
+ const blob = await new Promise((resolve) => {
938
+ canvas.toBlob((result) => resolve(result), mimeType, quality);
939
+ });
940
+ return blob;
941
+ };
942
+ const generateEmbroideryQCImageData = async (config, options = {}) => {
943
+ if (typeof document === "undefined") {
944
+ throw new Error("generateEmbroideryQCImageData requires a browser environment.");
945
+ }
946
+ const { mimeType = "image/png", quality } = options;
947
+ const canvas = await prepareExportCanvas(config, options);
948
+ if (!canvas) {
949
+ return null;
950
+ }
951
+ if (mimeType === "image/png" || typeof quality === "undefined") {
952
+ return canvas.toDataURL(mimeType);
953
+ }
954
+ return canvas.toDataURL(mimeType, quality);
955
+ };
661
956
 
662
957
  exports.EmbroideryQCImage = EmbroideryQCImage;
958
+ exports.generateEmbroideryQCImageBlob = generateEmbroideryQCImageBlob;
959
+ exports.generateEmbroideryQCImageData = generateEmbroideryQCImageData;
663
960
  //# sourceMappingURL=index.js.map