embroidery-qc-image 1.0.24 → 1.0.25

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
@@ -48,7 +48,7 @@ const LAYOUT = {
48
48
  // Font sizes (base values, will be multiplied by scaleFactor)
49
49
  HEADER_FONT_SIZE: 220,
50
50
  TEXT_FONT_SIZE: 200,
51
- OTHER_FONT_SIZE: 160,
51
+ OTHER_FONT_SIZE: 180,
52
52
  // Colors
53
53
  HEADER_COLOR: "#000000",
54
54
  LABEL_COLOR: "#444444",
@@ -57,12 +57,12 @@ const LAYOUT = {
57
57
  TEXT_ALIGN: "left",
58
58
  TEXT_BASELINE: "top",
59
59
  // Spacing
60
- LINE_GAP: 40,
61
- PADDING: 40,
60
+ LINE_GAP: 50,
61
+ PADDING: 50,
62
62
  SECTION_SPACING: 60,
63
63
  ELEMENT_SPACING: 100,
64
64
  SWATCH_SPACING: 25,
65
- FLORAL_SPACING: 300,
65
+ FLORAL_SPACING: 100,
66
66
  // Visual styling
67
67
  SWATCH_HEIGHT_RATIO: 2.025,
68
68
  UNDERLINE_POSITION: 0.9,
@@ -105,6 +105,17 @@ const getImageUrl = (type, value) => {
105
105
  return `${BASE_URLS.THREAD_COLOR}/${value}.webp`;
106
106
  };
107
107
  const getProxyUrl = (url) => `https://proxy-img.c8p.workers.dev?url=${encodeURIComponent(url)}`;
108
+ const getIconImageUrl = (position) => {
109
+ if (position.is_delete_icon)
110
+ return null;
111
+ if (position.icon_image && position.icon_image.trim().length > 0) {
112
+ return position.icon_image;
113
+ }
114
+ if (position.icon !== 0) {
115
+ return getImageUrl("icon", position.icon);
116
+ }
117
+ return null;
118
+ };
108
119
  const ensureImage = (existing) => {
109
120
  if (existing && existing.crossOrigin === "anonymous") {
110
121
  return existing;
@@ -136,14 +147,14 @@ const loadImage = (url, imageRefs, onLoad) => {
136
147
  img.onerror = () => {
137
148
  if (!attemptedProxy) {
138
149
  attemptedProxy = true;
139
- img.src = getProxyUrl(url);
150
+ img.src = getProxyUrl(getResizeUrl(url));
140
151
  return;
141
152
  }
142
153
  img.dataset.proxyUsed = attemptedProxy ? "true" : "false";
143
154
  cleanup();
144
155
  onLoad();
145
156
  };
146
- img.src = attemptedProxy ? getProxyUrl(url) : url;
157
+ img.src = attemptedProxy ? getProxyUrl(getResizeUrl(url)) : getResizeUrl(url);
147
158
  };
148
159
  const loadImageAsync = (url, imageRefs, cacheKey) => {
149
160
  const key = cacheKey ?? url;
@@ -181,13 +192,13 @@ const loadImageAsync = (url, imageRefs, cacheKey) => {
181
192
  target.onerror = () => {
182
193
  if (!attemptedProxy) {
183
194
  attemptedProxy = true;
184
- target.src = getProxyUrl(url);
195
+ target.src = getProxyUrl(getResizeUrl(url));
185
196
  return;
186
197
  }
187
198
  target.dataset.proxyUsed = attemptedProxy ? "true" : "false";
188
199
  finalize();
189
200
  };
190
- const desiredSrc = attemptedProxy ? getProxyUrl(url) : url;
201
+ const desiredSrc = attemptedProxy ? getProxyUrl(getResizeUrl(url)) : getResizeUrl(url);
191
202
  if (target.src !== desiredSrc) {
192
203
  target.src = desiredSrc;
193
204
  }
@@ -196,6 +207,57 @@ const loadImageAsync = (url, imageRefs, cacheKey) => {
196
207
  }
197
208
  });
198
209
  };
210
+ const getResizeUrl = (url) => {
211
+ try {
212
+ const urlObj = new URL(url);
213
+ // Xử lý cdn.shopify.com
214
+ if (urlObj.hostname === 'cdn.shopify.com') {
215
+ // Set hoặc update query param width=400
216
+ urlObj.searchParams.set('width', '400');
217
+ return urlObj.toString();
218
+ }
219
+ // Xử lý m.media-amazon.com
220
+ if (urlObj.hostname === 'm.media-amazon.com') {
221
+ const pathname = urlObj.pathname;
222
+ // Split pathname theo dấu /
223
+ const pathArr = pathname.split('/');
224
+ // Lấy filename (phần cuối cùng)
225
+ const filename = pathArr[pathArr.length - 1];
226
+ // Xóa pattern ._.*_ (ví dụ: ._AC_SX569_)
227
+ const cleanedFilename = filename.replace(/\._.*_/g, '');
228
+ // Split filename đã clean theo dấu .
229
+ const parts = cleanedFilename.split('.');
230
+ if (parts.length >= 2) {
231
+ // Lấy phần đầu và phần cuối
232
+ const firstPart = parts[0];
233
+ const lastPart = parts[parts.length - 1];
234
+ // Chèn _AC_SX400_ vào giữa và join lại
235
+ const newFilename = `${firstPart}._AC_SX400_.${lastPart}`;
236
+ // Thay filename mới vào pathArr
237
+ pathArr[pathArr.length - 1] = newFilename;
238
+ // Join lại
239
+ urlObj.pathname = pathArr.join('/');
240
+ return urlObj.toString();
241
+ }
242
+ }
243
+ // Xử lý i.etsystatic.com
244
+ if (urlObj.hostname === 'i.etsystatic.com') {
245
+ const pathname = urlObj.pathname;
246
+ // Thay il_fullxfull bằng il_400x400
247
+ if (pathname.includes('il_fullxfull')) {
248
+ const newPathname = pathname.replace(/il_fullxfull/g, 'il_400x400');
249
+ urlObj.pathname = newPathname;
250
+ return urlObj.toString();
251
+ }
252
+ }
253
+ // Nếu không phải các domain cần xử lý, return URL gốc
254
+ return url;
255
+ }
256
+ catch (error) {
257
+ // Nếu URL không hợp lệ, return URL gốc
258
+ return url;
259
+ }
260
+ };
199
261
  const preloadFonts = async (config) => {
200
262
  if (config.error_message || !config.sides?.length)
201
263
  return;
@@ -225,12 +287,10 @@ const preloadImages = async (config, imageRefs) => {
225
287
  config.sides.forEach((side) => {
226
288
  side.positions.forEach((position) => {
227
289
  if (position.type === "ICON") {
228
- if (position.icon !== 0) {
229
- const iconUrl = getImageUrl("icon", position.icon);
230
- if (!seen.has(iconUrl)) {
231
- entries.push({ url: iconUrl });
232
- seen.add(iconUrl);
233
- }
290
+ const iconUrl = getIconImageUrl(position);
291
+ if (iconUrl && !seen.has(iconUrl)) {
292
+ entries.push({ url: iconUrl });
293
+ seen.add(iconUrl);
234
294
  }
235
295
  if (position.color) {
236
296
  const threadUrl = getImageUrl("threadColor", position.color);
@@ -303,23 +363,30 @@ const wrapText = (ctx, text, x, y, maxWidth, lineHeight) => {
303
363
  };
304
364
  };
305
365
  const buildWrappedLines = (ctx, text, maxWidth) => {
306
- const words = text.split(" ").filter((word) => word.length > 0);
307
- if (words.length === 0)
308
- return [""];
309
- const lines = [];
310
- let currentLine = words[0];
311
- for (let i = 1; i < words.length; i++) {
312
- const testLine = `${currentLine} ${words[i]}`;
313
- if (ctx.measureText(testLine).width > maxWidth && currentLine.length > 0) {
314
- lines.push(currentLine);
315
- currentLine = words[i];
366
+ // Mỗi '\n' tương đương với một line break giống như khi wrap tự động.
367
+ const segments = text.split("\n");
368
+ const result = [];
369
+ segments.forEach((segment) => {
370
+ const words = segment.split(" ").filter((word) => word.length > 0);
371
+ if (words.length === 0) {
372
+ // Nếu đoạn rỗng, thêm một dòng trống (break đúng 1 line)
373
+ result.push("");
374
+ return;
316
375
  }
317
- else {
318
- currentLine = testLine;
376
+ let currentLine = words[0];
377
+ for (let i = 1; i < words.length; i++) {
378
+ const testLine = `${currentLine} ${words[i]}`;
379
+ if (ctx.measureText(testLine).width > maxWidth && currentLine.length > 0) {
380
+ result.push(currentLine);
381
+ currentLine = words[i];
382
+ }
383
+ else {
384
+ currentLine = testLine;
385
+ }
319
386
  }
320
- }
321
- lines.push(currentLine);
322
- return lines;
387
+ result.push(currentLine);
388
+ });
389
+ return result.length ? result : [""];
323
390
  };
324
391
  const drawSwatches = (ctx, colors, startX, startY, swatchHeight, scaleFactor, imageRefs) => {
325
392
  let swatchX = startX;
@@ -383,8 +450,9 @@ const EmbroideryQCImage = ({ config, className = "", style = {}, }) => {
383
450
  config.sides.forEach((side) => {
384
451
  side.positions.forEach((position) => {
385
452
  if (position.type === "ICON") {
386
- if (position.icon !== 0) {
387
- loadImage(getImageUrl("icon", position.icon), imageRefs, incrementCounter);
453
+ const iconUrl = getIconImageUrl(position);
454
+ if (iconUrl) {
455
+ loadImage(iconUrl, imageRefs, incrementCounter);
388
456
  }
389
457
  position.layer_colors?.forEach((color) => {
390
458
  loadImage(getImageUrl("threadColor", color), imageRefs, incrementCounter);
@@ -435,7 +503,7 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
435
503
  return;
436
504
  ctx.textAlign = LAYOUT.TEXT_ALIGN;
437
505
  ctx.textBaseline = LAYOUT.TEXT_BASELINE;
438
- // Calculate warning height (with scaleFactor = 1 for measurement)
506
+ // Calculate warning & message height (with scaleFactor = 1 for measurement)
439
507
  let warningHeight = 0;
440
508
  let warningLineHeight = 0;
441
509
  let warningLineCount = 0;
@@ -454,6 +522,24 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
454
522
  warningHeight = warningLineCount * warningLineHeight + LAYOUT.PADDING;
455
523
  }
456
524
  }
525
+ let messageHeight = 0;
526
+ let messageLineHeight = 0;
527
+ let messageLineCount = 0;
528
+ if (config.message) {
529
+ const measureMessageCanvas = document.createElement("canvas");
530
+ measureMessageCanvas.width = canvas.width;
531
+ measureMessageCanvas.height = canvas.height;
532
+ const measureMessageCtx = measureMessageCanvas.getContext("2d");
533
+ if (measureMessageCtx) {
534
+ measureMessageCtx.textAlign = "left";
535
+ measureMessageCtx.textBaseline = "top";
536
+ measureMessageCtx.font = `${LAYOUT.HEADER_FONT_SIZE * 0.7}px ${LAYOUT.FONT_FAMILY}`;
537
+ const messageLines = buildWrappedLines(measureMessageCtx, config.message.trim(), canvas.width - LAYOUT.PADDING * 4);
538
+ messageLineCount = messageLines.length;
539
+ messageLineHeight = LAYOUT.HEADER_FONT_SIZE * 0.7 + LAYOUT.LINE_GAP;
540
+ messageHeight = messageLineCount * messageLineHeight + LAYOUT.PADDING;
541
+ }
542
+ }
457
543
  if (config.image_url) {
458
544
  const mockupImage = imageRefs.current.get(config.image_url);
459
545
  if (mockupImage) {
@@ -486,27 +572,40 @@ const renderEmbroideryCanvas = (canvas, config, canvasSize, imageRefs) => {
486
572
  measureCtx.textBaseline = LAYOUT.TEXT_BASELINE;
487
573
  let measureY = LAYOUT.PADDING;
488
574
  const measureSpacing = LAYOUT.ELEMENT_SPACING;
489
- // Add warning text height (without bottom padding, no spacing)
575
+ // Add warning & message text height (without bottom padding, no spacing)
490
576
  if (config.warning_message) {
491
577
  const warningTextHeight = warningHeight - LAYOUT.PADDING;
492
578
  measureY += warningTextHeight;
493
579
  }
580
+ if (config.message) {
581
+ const messageTextHeight = messageHeight - LAYOUT.PADDING;
582
+ measureY += messageTextHeight;
583
+ }
494
584
  config.sides.forEach((side) => {
495
585
  const sideHeight = renderSide(measureCtx, side, measureY, canvas.width, 1, imageRefs);
496
586
  measureY += sideHeight + measureSpacing;
497
587
  });
498
- const scaleFactor = Math.max(0.5, Math.min(1, (canvas.height - LAYOUT.PADDING - warningHeight) / measureY));
588
+ const scaleFactor = Math.max(0.5, Math.min(1, (canvas.height - LAYOUT.PADDING - warningHeight - messageHeight) / measureY));
499
589
  drawMockupAndFlorals(ctx, canvas, floralAssets, imageRefs);
500
- // Render warning with scaleFactor and get actual height
590
+ // Render warning & message with scaleFactor and get actual heights
501
591
  let actualWarningHeight = 0;
592
+ let actualMessageHeight = 0;
502
593
  if (config.warning_message) {
503
594
  actualWarningHeight = renderWarning(ctx, canvas, config.warning_message, scaleFactor);
504
595
  }
505
- // Calculate currentY: padding top + actual warning height (no spacing)
596
+ if (config.message) {
597
+ actualMessageHeight = renderWarning(ctx, canvas, config.message, scaleFactor, actualWarningHeight, "", // message: không cần prefix "Note"
598
+ DEFAULT_ERROR_COLOR // message: hiển thị màu đỏ
599
+ );
600
+ }
601
+ // Calculate currentY: padding top + actual warning & message height (no spacing)
506
602
  let currentY = LAYOUT.PADDING * scaleFactor;
507
603
  if (config.warning_message && actualWarningHeight > 0) {
508
604
  currentY += actualWarningHeight;
509
605
  }
606
+ if (config.message && actualMessageHeight > 0) {
607
+ currentY += actualMessageHeight;
608
+ }
510
609
  config.sides.forEach((side) => {
511
610
  const sideHeight = renderSide(ctx, side, currentY, canvas.width, scaleFactor, imageRefs);
512
611
  currentY += sideHeight + measureSpacing * scaleFactor;
@@ -548,8 +647,8 @@ const renderErrorState = (ctx, canvas, message) => {
548
647
  });
549
648
  ctx.restore();
550
649
  };
551
- const renderWarning = (ctx, canvas, message, scaleFactor = 1) => {
552
- const sanitizedMessage = `Note: ${message.trim()}`;
650
+ const renderWarning = (ctx, canvas, message, scaleFactor = 1, offsetY = 0, prefix = "Note: ", color = DEFAULT_WARNING_COLOR) => {
651
+ const sanitizedMessage = `${prefix}${message.trim()}`;
553
652
  const horizontalPadding = LAYOUT.PADDING * 2 * scaleFactor;
554
653
  const maxWidth = canvas.width - horizontalPadding * 2;
555
654
  const baseFontSize = LAYOUT.HEADER_FONT_SIZE * 0.7 * scaleFactor;
@@ -558,7 +657,7 @@ const renderWarning = (ctx, canvas, message, scaleFactor = 1) => {
558
657
  ctx.save();
559
658
  ctx.textAlign = "left";
560
659
  ctx.textBaseline = "top";
561
- ctx.fillStyle = DEFAULT_WARNING_COLOR;
660
+ ctx.fillStyle = color;
562
661
  ctx.font = `${baseFontSize}px ${LAYOUT.FONT_FAMILY}`;
563
662
  let fontSize = baseFontSize;
564
663
  let lineGap = LAYOUT.LINE_GAP * scaleFactor;
@@ -577,7 +676,7 @@ const renderWarning = (ctx, canvas, message, scaleFactor = 1) => {
577
676
  lines = buildWrappedLines(ctx, sanitizedMessage, maxWidth);
578
677
  longestLineWidth = Math.max(...lines.map((line) => ctx.measureText(line).width));
579
678
  }
580
- const startY = LAYOUT.PADDING * scaleFactor;
679
+ const startY = LAYOUT.PADDING * scaleFactor + offsetY;
581
680
  lines.forEach((line, index) => {
582
681
  const y = startY + index * lineHeight;
583
682
  ctx.fillText(line, leftX, y);
@@ -601,7 +700,7 @@ const drawMockupAndFlorals = (ctx, canvas, floralAssets, imageRefs) => {
601
700
  ctx.drawImage(mockupImg, x, y, width, height);
602
701
  // Draw florals
603
702
  if (floralAssets.length > 0) {
604
- const floralH = Math.min(900, height);
703
+ const floralH = Math.min(500, height);
605
704
  let currentX = x - LAYOUT.FLORAL_SPACING;
606
705
  for (let i = floralAssets.length - 1; i >= 0; i--) {
607
706
  const img = floralAssets[i];
@@ -930,21 +1029,56 @@ const renderTextPosition = (ctx, position, x, y, maxWidth, displayIndex, showLab
930
1029
  return drawnHeight;
931
1030
  };
932
1031
  const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRefs, options) => {
933
- const iconFontSize = LAYOUT.OTHER_FONT_SIZE * scaleFactor;
1032
+ // Dùng cùng font size với Text cho label và value icon
1033
+ const iconFontSize = LAYOUT.TEXT_FONT_SIZE * scaleFactor;
934
1034
  const lineGap = LAYOUT.LINE_GAP * scaleFactor;
935
1035
  ctx.save();
936
- ctx.font = `${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
937
1036
  ctx.fillStyle = LAYOUT.LABEL_COLOR;
938
1037
  let cursorY = y;
939
- const iconText = position.note ? `Icon: ${position.note}` :
940
- position.icon === 0
941
- ? `Icon: (icon mặc định theo file thêu)`
942
- : `Icon: ${position.icon}`;
943
- const iconResult = wrapText(ctx, iconText, x, cursorY, maxWidth, iconFontSize + lineGap);
1038
+ // Tách label "Icon:" (in đậm) và phần value (thường)
1039
+ const iconLabel = "Icon:";
1040
+ let iconValue;
1041
+ if (position.is_delete_icon) {
1042
+ // Ưu tiên hiển thị không icon nếu được đánh dấu xóa
1043
+ iconValue = "(không có icon)";
1044
+ }
1045
+ else if (position.note) {
1046
+ iconValue = position.note;
1047
+ }
1048
+ else if (position.icon_name && position.icon_name.trim().length > 0) {
1049
+ // Nếu có icon_name thì hiển thị tên đó
1050
+ iconValue = position.icon_name;
1051
+ }
1052
+ else if (position.icon === 0) {
1053
+ // Icon mặc định theo file thêu
1054
+ iconValue = "(icon mặc định theo file thêu)";
1055
+ }
1056
+ else {
1057
+ // Fallback: hiển thị mã icon (ép sang string)
1058
+ iconValue = String(position.icon);
1059
+ }
1060
+ // Vẽ label Icon: (bold, màu label mặc định)
1061
+ ctx.font = `bold ${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
1062
+ ctx.fillText(iconLabel, x, cursorY);
1063
+ const labelWidth = ctx.measureText(iconLabel).width;
1064
+ // Vẽ value kế bên, font thường, màu đỏ giống text value
1065
+ ctx.font = `${iconFontSize}px ${LAYOUT.FONT_FAMILY}`;
1066
+ ctx.fillStyle = DEFAULT_ERROR_COLOR;
1067
+ const valueText = ` ${iconValue}`;
1068
+ ctx.fillText(valueText, x + labelWidth, cursorY);
1069
+ // Reset lại màu về label color cho các phần text tiếp theo (màu chỉ, v.v.)
1070
+ ctx.fillStyle = LAYOUT.LABEL_COLOR;
1071
+ const valueWidth = ctx.measureText(valueText).width;
1072
+ const iconResult = {
1073
+ height: iconFontSize + lineGap,
1074
+ // tổng width của cả label + value, dùng để canh icon image lệch sang phải
1075
+ lastLineWidth: labelWidth + valueWidth,
1076
+ lastLineY: cursorY,
1077
+ };
944
1078
  // Draw icon image
945
- if (position.icon !== 0) {
946
- const url = getImageUrl("icon", position.icon);
947
- const img = imageRefs.current.get(url);
1079
+ const iconUrl = getIconImageUrl(position);
1080
+ if (iconUrl) {
1081
+ const img = imageRefs.current.get(iconUrl);
948
1082
  if (img?.complete && img.naturalHeight > 0) {
949
1083
  const ratio = img.naturalWidth / img.naturalHeight;
950
1084
  const swatchW = Math.max(1, Math.floor(iconFontSize * ratio));
@@ -957,11 +1091,14 @@ const renderIconPosition = (ctx, position, x, y, maxWidth, scaleFactor, imageRef
957
1091
  }
958
1092
  cursorY += iconResult.height;
959
1093
  // Draw color swatches (prefer layer_colors, fallback to single color)
960
- const iconColors = position.layer_colors?.length
961
- ? position.layer_colors
962
- : position.color
963
- ? [position.color]
964
- : null;
1094
+ // Nếu icon đã bị xóa thì không cần hiển thị màu chỉ nữa
1095
+ const iconColors = position.is_delete_icon
1096
+ ? null
1097
+ : position.layer_colors?.length
1098
+ ? position.layer_colors
1099
+ : position.color
1100
+ ? [position.color]
1101
+ : null;
965
1102
  const layerCount = position.layer_colors?.length ?? 0;
966
1103
  const hasMultiLayerColors = layerCount > 1;
967
1104
  const shouldSkipColorSection = options?.hideColor && !hasMultiLayerColors;