@akinon/pz-virtual-try-on 2.0.40-rc.0 → 2.0.40

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/CHANGELOG.md CHANGED
@@ -1,36 +1,11 @@
1
1
  # @akinon/pz-virtual-try-on
2
2
 
3
- ## 2.0.40-rc.0
4
-
5
- ### Patch Changes
6
-
7
- - 9db81a71: ZERO-4365: Remove brand `@theme/*` alias imports from library packages
8
- - Updated dependencies [0cf9ea23]
9
- - Updated dependencies [324f97d5]
10
- - Updated dependencies [51ea0688]
11
- - Updated dependencies
12
- - Updated dependencies [2fea4143]
13
- - Updated dependencies [b55acb768]
14
- - Updated dependencies [760258c1]
15
- - Updated dependencies [143be2b9d]
16
- - Updated dependencies [7889b08f]
17
- - Updated dependencies [9f8cd3bc5]
18
- - Updated dependencies [bfafa3f4]
19
- - Updated dependencies [57d7eb30]
20
- - Updated dependencies [d99a6a7d5]
21
- - Updated dependencies [9db81a71]
22
- - Updated dependencies [591e345e1]
23
- - Updated dependencies [4de5303c5]
24
- - Updated dependencies [95b139dc1]
25
- - Updated dependencies [1d00f2d0]
26
- - Updated dependencies [4ac7b2a1]
27
- - Updated dependencies [e9598c71]
28
- - Updated dependencies [4998a963]
29
- - Updated dependencies [804d2bd6]
30
- - Updated dependencies [3909d3224]
31
- - Updated dependencies [6a3d8a63]
32
- - Updated dependencies [e18836b2]
33
- - @akinon/next@2.0.40-rc.0
3
+ ## 2.0.40
4
+
5
+ ### Patch Changes
6
+
7
+ - 7a0e0c17: ZERO-4720: Preserve uploaded and generated image proportions during resizing and display.
8
+ - @akinon/next@2.0.40
34
9
 
35
10
  ## 2.0.39
36
11
 
package/jest.config.js ADDED
@@ -0,0 +1,7 @@
1
+ /** @type {import('ts-jest').JestConfigWithTsJest} */
2
+ module.exports = {
3
+ preset: 'ts-jest',
4
+ testEnvironment: 'jsdom',
5
+ roots: ['<rootDir>/src'],
6
+ testMatch: ['**/*.test.ts']
7
+ };
package/package.json CHANGED
@@ -1,15 +1,18 @@
1
1
  {
2
2
  "name": "@akinon/pz-virtual-try-on",
3
- "version": "2.0.40-rc.0",
3
+ "version": "2.0.40",
4
4
  "license": "MIT",
5
5
  "main": "src/index.ts",
6
+ "scripts": {
7
+ "test": "jest"
8
+ },
6
9
  "peerDependencies": {
7
10
  "next-auth": "^5.0.0-beta",
8
11
  "react": "^18.0.0 || ^19.0.0",
9
12
  "react-dom": "^18.0.0 || ^19.0.0"
10
13
  },
11
14
  "dependencies": {
12
- "@akinon/next": "2.0.40-rc.0",
15
+ "@akinon/next": "2.0.40",
13
16
  "clsx": "^2.0.0",
14
17
  "tailwind-merge": "^2.0.0",
15
18
  "react-image-crop": "^11.0.5",
@@ -0,0 +1,26 @@
1
+ import { getScaledImageDimensions } from './image-compression';
2
+
3
+ describe('getScaledImageDimensions', () => {
4
+ it.each([
5
+ [4000, 3000, 2048, 2048, 1536],
6
+ [3000, 4000, 2048, 1536, 2048],
7
+ [1200, 1600, 2048, 1200, 1600]
8
+ ])(
9
+ 'preserves the aspect ratio for %sx%s images',
10
+ (width, height, maxDimension, expectedWidth, expectedHeight) => {
11
+ const result = getScaledImageDimensions(width, height, maxDimension);
12
+
13
+ expect(result).toEqual({
14
+ width: expectedWidth,
15
+ height: expectedHeight
16
+ });
17
+ expect(result.width / result.height).toBeCloseTo(width / height, 5);
18
+ }
19
+ );
20
+
21
+ it('rejects invalid dimensions before drawing to canvas', () => {
22
+ expect(() => getScaledImageDimensions(0, 1000, 2048)).toThrow(
23
+ 'Image dimensions must be positive'
24
+ );
25
+ });
26
+ });
@@ -1,26 +1,42 @@
1
+ export const getScaledImageDimensions = (
2
+ width: number,
3
+ height: number,
4
+ maxDimension: number
5
+ ): { width: number; height: number } => {
6
+ if (width <= 0 || height <= 0 || maxDimension <= 0) {
7
+ throw new Error('Image dimensions must be positive');
8
+ }
9
+
10
+ const scale = Math.min(1, maxDimension / Math.max(width, height));
11
+
12
+ return {
13
+ width: Math.max(1, Math.round(width * scale)),
14
+ height: Math.max(1, Math.round(height * scale))
15
+ };
16
+ };
17
+
1
18
  export const compressImageToUnder1MB = (file: File): Promise<File> => {
2
19
  return new Promise((resolve, reject) => {
3
20
  const canvas = document.createElement('canvas');
4
21
  const ctx = canvas.getContext('2d');
5
22
  const img = new Image();
23
+ const imageUrl = URL.createObjectURL(file);
6
24
 
7
25
  img.onload = () => {
8
26
  const MAX_SIZE = 1048576;
27
+ URL.revokeObjectURL(imageUrl);
9
28
 
10
29
  if (!ctx) {
11
30
  reject(new Error('Canvas context not available'));
12
31
  return;
13
32
  }
14
33
 
15
- let { width, height } = img;
16
- let quality = 0.9;
17
-
18
34
  const maxDimension = 2048;
19
- if (width > maxDimension || height > maxDimension) {
20
- const scale = Math.min(maxDimension / width, maxDimension / height);
21
- width = Math.floor(width * scale);
22
- height = Math.floor(height * scale);
23
- }
35
+ const { width, height } = getScaledImageDimensions(
36
+ img.naturalWidth,
37
+ img.naturalHeight,
38
+ maxDimension
39
+ );
24
40
 
25
41
  canvas.width = width;
26
42
  canvas.height = height;
@@ -55,12 +71,14 @@ export const compressImageToUnder1MB = (file: File): Promise<File> => {
55
71
  );
56
72
  };
57
73
 
58
- attemptCompression(quality);
74
+ attemptCompression(0.9);
59
75
  };
60
76
 
61
- img.onerror = () =>
77
+ img.onerror = () => {
78
+ URL.revokeObjectURL(imageUrl);
62
79
  reject(new Error('Failed to load image for compression'));
63
- img.src = URL.createObjectURL(file);
80
+ };
81
+ img.src = imageUrl;
64
82
  });
65
83
  };
66
84
 
@@ -69,8 +87,10 @@ export const convertWebPToJPEG = (file: File): Promise<File> => {
69
87
  const canvas = document.createElement('canvas');
70
88
  const ctx = canvas.getContext('2d');
71
89
  const img = new Image();
90
+ const imageUrl = URL.createObjectURL(file);
72
91
 
73
92
  img.onload = () => {
93
+ URL.revokeObjectURL(imageUrl);
74
94
  canvas.width = img.naturalWidth;
75
95
  canvas.height = img.naturalHeight;
76
96
 
@@ -104,7 +124,10 @@ export const convertWebPToJPEG = (file: File): Promise<File> => {
104
124
  );
105
125
  };
106
126
 
107
- img.onerror = () => reject(new Error('Failed to load image'));
108
- img.src = URL.createObjectURL(file);
127
+ img.onerror = () => {
128
+ URL.revokeObjectURL(imageUrl);
129
+ reject(new Error('Failed to load image'));
130
+ };
131
+ img.src = imageUrl;
109
132
  });
110
133
  };
@@ -147,7 +147,8 @@ export const fileToBase64 = (file: File): Promise<string> => {
147
147
 
148
148
  export {
149
149
  compressImageToUnder1MB,
150
- convertWebPToJPEG
150
+ convertWebPToJPEG,
151
+ getScaledImageDimensions
151
152
  } from './image-compression';
152
153
 
153
154
  export {
@@ -276,7 +276,7 @@ export function BasketAsyncModal({
276
276
  src={hookData.uploadedImage}
277
277
  alt="Uploaded"
278
278
  className={twMerge(
279
- 'rounded-[4px] w-auto',
279
+ 'rounded-[4px] max-w-full h-auto object-contain',
280
280
  settings?.customStyles
281
281
  ?.basketAsyncModalProcessingImage
282
282
  )}
@@ -350,7 +350,7 @@ export function BasketAsyncModal({
350
350
  src={hookData.uploadedImage}
351
351
  alt="Uploaded"
352
352
  className={twMerge(
353
- 'w-full rounded-[4px]',
353
+ 'w-full h-auto object-contain rounded-[4px]',
354
354
  settings?.customStyles?.basketAsyncModalErrorImage
355
355
  )}
356
356
  />
@@ -515,7 +515,7 @@ export function BasketAsyncModal({
515
515
  src={result.generated}
516
516
  alt={`Result ${index + 1}`}
517
517
  className={twMerge(
518
- 'w-full h-full group-hover:scale-105 transition-transform duration-200',
518
+ 'w-full h-full object-contain group-hover:scale-105 transition-transform duration-200',
519
519
  settings?.customStyles
520
520
  ?.basketAsyncModalResultImage
521
521
  )}
@@ -9,7 +9,6 @@ import type {
9
9
  VirtualTryOnPluginSettings,
10
10
  VirtualTryOnResponse
11
11
  } from '../types';
12
- import { Image } from '@akinon/next/components/image';
13
12
  import { useSubmitVirtualTryOnFeedbackMutation } from '../data/endpoints';
14
13
  import { parseVirtualTryOnError } from '../utils';
15
14
  import { useLocalization } from '@akinon/next/hooks';
@@ -398,12 +397,10 @@ export function VirtualTryOnMultipleResultModal({
398
397
  </p>
399
398
  </div>
400
399
  ) : (
401
- <Image
400
+ <img
402
401
  src={result.generated}
403
402
  alt={`Try-on result for ${product.name}`}
404
- fill
405
- aspectRatio={3 / 4}
406
- sizes="(max-width: 768px) 100vw, 33vw"
403
+ className="absolute inset-0 w-full h-full object-contain"
407
404
  />
408
405
  )}
409
406
  </div>
@@ -408,7 +408,7 @@ export function VirtualTryOnResultModal({
408
408
  imageUrl: tryOnResult.generated,
409
409
  alt: 'Virtual try-on result',
410
410
  className: twMerge(
411
- 'w-full rounded-[4px]',
411
+ 'w-full h-auto object-contain rounded-[4px]',
412
412
  settings?.customStyles?.resultImage
413
413
  )
414
414
  }
@@ -418,7 +418,7 @@ export function VirtualTryOnResultModal({
418
418
  src={tryOnResult.generated}
419
419
  alt="Virtual try-on result"
420
420
  className={twMerge(
421
- 'w-full rounded-[4px]',
421
+ 'w-full h-auto object-contain rounded-[4px]',
422
422
  settings?.customStyles?.resultImage
423
423
  )}
424
424
  />
@@ -1034,7 +1034,7 @@ export function VirtualTryOnUploadModal({
1034
1034
  src={uploadedImage || ''}
1035
1035
  alt="Uploaded image"
1036
1036
  className={twMerge(
1037
- 'rounded-[4px] w-auto pointer-events-none select-none',
1037
+ 'rounded-[4px] max-w-full h-auto object-contain pointer-events-none select-none',
1038
1038
  settings?.customStyles?.cropImage
1039
1039
  )}
1040
1040
  />