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