sprintify-ui 0.11.28 → 0.11.29

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.
@@ -1,8 +1,9 @@
1
1
  import toHumanList from './toHumanList';
2
2
  import fileSizeFormat from './fileSizeFormat';
3
3
  import resizeImageFromURI from './resizeImageFromURI';
4
+ import resizeImageForUpload from './resizeImageForUpload';
4
5
  import { disableScroll, enableScroll } from './scrollPreventer';
5
6
  import { blobToBase64, validateBase64, base64ToBlob } from './blob';
6
7
  import { getColorConfig } from './colors';
7
8
  import { getInitials, getAvatarColor } from './avatar';
8
- export { toHumanList, fileSizeFormat, disableScroll, enableScroll, resizeImageFromURI, blobToBase64, validateBase64, base64ToBlob, getColorConfig, getInitials, getAvatarColor, };
9
+ export { toHumanList, fileSizeFormat, disableScroll, enableScroll, resizeImageFromURI, resizeImageForUpload, blobToBase64, validateBase64, base64ToBlob, getColorConfig, getInitials, getAvatarColor, };
@@ -0,0 +1,7 @@
1
+ type ResizeImageForUploadOptions = {
2
+ maxBytes: number;
3
+ outputMimeType?: string;
4
+ quality?: number;
5
+ };
6
+ export default function resizeImageForUpload(file: Blob, options: ResizeImageForUploadOptions): Promise<Blob>;
7
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sprintify-ui",
3
- "version": "0.11.28",
3
+ "version": "0.11.29",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "generate-llm-txt": "node scripts/generate-llm-txt.js",
@@ -35,7 +35,7 @@ import { UploadedFile } from '@/types/UploadedFile';
35
35
  import { useSnackbarsStore } from '@/stores/snackbars';
36
36
  import BaseLoadingCover from '@/components/BaseLoadingCover.vue';
37
37
  import { BaseCropperConfig } from '@/types';
38
- import { base64ToBlob, blobToBase64, resizeImageFromURI } from '@/utils';
38
+ import { resizeImageForUpload } from '@/utils';
39
39
  import BaseFilePicker from './BaseFilePicker.vue';
40
40
  import BaseFilePickerCrop from './BaseFilePickerCrop.vue';
41
41
  import { t } from '@/i18n';
@@ -220,49 +220,21 @@ async function autoResizeImage(file: File): Promise<File> {
220
220
  }
221
221
 
222
222
  try {
223
- const source = await blobToBase64(file);
224
- const dimensions = await getImageDimensions(source);
225
-
226
- const resizeRatio = Math.sqrt(props.maxImageSizeBeforeResize / file.size);
227
-
228
- let targetWidth = Math.max(1, Math.floor(dimensions.width * resizeRatio));
229
- let targetHeight = Math.max(1, Math.floor(dimensions.height * resizeRatio));
230
-
231
- let resizedBlob = await createResizedBlob(
232
- source,
233
- targetHeight,
234
- targetWidth,
235
- file.type
236
- );
237
-
238
- let attempts = 0;
239
-
240
- while (
241
- resizedBlob.size > props.maxImageSizeBeforeResize &&
242
- attempts < 5 &&
243
- targetWidth > 1 &&
244
- targetHeight > 1
245
- ) {
246
- targetWidth = Math.max(1, Math.floor(targetWidth * 0.8));
247
- targetHeight = Math.max(1, Math.floor(targetHeight * 0.8));
248
- resizedBlob = await createResizedBlob(
249
- source,
250
- targetHeight,
251
- targetWidth,
252
- file.type
253
- );
254
- attempts++;
255
- }
223
+ const resizedBlob = await resizeImageForUpload(file, {
224
+ maxBytes: props.maxImageSizeBeforeResize,
225
+ });
256
226
 
257
227
  if (resizedBlob.size >= file.size) {
258
228
  return file;
259
229
  }
260
230
 
231
+ const mimeType = resizedBlob.type || file.type;
232
+
261
233
  return new File(
262
234
  [resizedBlob],
263
- getResizedFileName(file.name, resizedBlob.type),
235
+ getResizedFileName(file.name, mimeType),
264
236
  {
265
- type: resizedBlob.type || file.type,
237
+ type: mimeType,
266
238
  lastModified: file.lastModified,
267
239
  }
268
240
  );
@@ -272,34 +244,6 @@ async function autoResizeImage(file: File): Promise<File> {
272
244
  }
273
245
  }
274
246
 
275
- async function createResizedBlob(
276
- source: string,
277
- height: number,
278
- width: number,
279
- mimeType: string
280
- ): Promise<Blob> {
281
- const resizedSource = await resizeImageFromURI(source, height, width);
282
- return await base64ToBlob(resizedSource, `data:${mimeType}`);
283
- }
284
-
285
- async function getImageDimensions(
286
- source: string
287
- ): Promise<{ width: number; height: number }> {
288
- return await new Promise((resolve, reject) => {
289
- const image = new Image();
290
- image.onload = () => {
291
- resolve({
292
- width: image.width,
293
- height: image.height,
294
- });
295
- };
296
- image.onerror = () => {
297
- reject(new Error('Failed to load image dimensions'));
298
- };
299
- image.src = source;
300
- });
301
- }
302
-
303
247
  function getResizedFileName(filename: string, mimeType: string): string {
304
248
  if (mimeType != 'image/jpeg') {
305
249
  return filename;
@@ -1,6 +1,7 @@
1
1
  import toHumanList from './toHumanList';
2
2
  import fileSizeFormat from './fileSizeFormat';
3
3
  import resizeImageFromURI from './resizeImageFromURI';
4
+ import resizeImageForUpload from './resizeImageForUpload';
4
5
  import { disableScroll, enableScroll } from './scrollPreventer';
5
6
  import { blobToBase64, validateBase64, base64ToBlob } from './blob';
6
7
  import { getColorConfig } from './colors';
@@ -12,6 +13,7 @@ export {
12
13
  disableScroll,
13
14
  enableScroll,
14
15
  resizeImageFromURI,
16
+ resizeImageForUpload,
15
17
  blobToBase64,
16
18
  validateBase64,
17
19
  base64ToBlob,
@@ -0,0 +1,181 @@
1
+ type ResizeImageForUploadOptions = {
2
+ maxBytes: number;
3
+ outputMimeType?: string;
4
+ quality?: number;
5
+ };
6
+
7
+ type DecodedImage = {
8
+ source: CanvasImageSource;
9
+ width: number;
10
+ height: number;
11
+ cleanup: () => void;
12
+ };
13
+
14
+ const DEFAULT_OUTPUT_QUALITY = 0.9;
15
+
16
+ export default async function resizeImageForUpload(
17
+ file: Blob,
18
+ options: ResizeImageForUploadOptions
19
+ ): Promise<Blob> {
20
+ if (options.maxBytes <= 0 || file.size <= options.maxBytes) {
21
+ return file;
22
+ }
23
+
24
+ const decodedImage = await decodeImage(file);
25
+ const outputMimeType =
26
+ options.outputMimeType ?? getOutputMimeType((file as File).type);
27
+ const quality = clamp(options.quality ?? DEFAULT_OUTPUT_QUALITY, 0.1, 1);
28
+
29
+ let canvas: HTMLCanvasElement | null = null;
30
+
31
+ try {
32
+ const resizeRatio = Math.min(1, Math.sqrt(options.maxBytes / file.size));
33
+ const targetWidth = Math.max(1, Math.floor(decodedImage.width * resizeRatio));
34
+ const targetHeight = Math.max(
35
+ 1,
36
+ Math.floor(decodedImage.height * resizeRatio)
37
+ );
38
+
39
+ canvas = createScaledCanvas(
40
+ decodedImage.source,
41
+ decodedImage.width,
42
+ decodedImage.height,
43
+ targetWidth,
44
+ targetHeight
45
+ );
46
+
47
+ if (!supportsQualityEncoding(outputMimeType)) {
48
+ return await canvasToBlob(canvas, outputMimeType);
49
+ }
50
+
51
+ return await canvasToBlob(canvas, outputMimeType, quality);
52
+ } finally {
53
+ decodedImage.cleanup();
54
+
55
+ if (canvas) {
56
+ releaseCanvas(canvas);
57
+ }
58
+ }
59
+ }
60
+
61
+ function getOutputMimeType(inputMimeType: string): string {
62
+ if (inputMimeType === 'image/webp') {
63
+ return 'image/webp';
64
+ }
65
+
66
+ return 'image/jpeg';
67
+ }
68
+
69
+ async function decodeImage(file: Blob): Promise<DecodedImage> {
70
+ if (typeof createImageBitmap === 'function') {
71
+ try {
72
+ const bitmap = await createImageBitmap(file);
73
+
74
+ return {
75
+ source: bitmap,
76
+ width: bitmap.width,
77
+ height: bitmap.height,
78
+ cleanup: () => bitmap.close(),
79
+ };
80
+ } catch {
81
+ // Fallback to Image decode when createImageBitmap is not supported.
82
+ }
83
+ }
84
+
85
+ const objectUrl = URL.createObjectURL(file);
86
+
87
+ try {
88
+ const image = await loadImage(objectUrl);
89
+
90
+ return {
91
+ source: image,
92
+ width: image.naturalWidth || image.width,
93
+ height: image.naturalHeight || image.height,
94
+ cleanup: () => URL.revokeObjectURL(objectUrl),
95
+ };
96
+ } catch (error) {
97
+ URL.revokeObjectURL(objectUrl);
98
+ throw error;
99
+ }
100
+ }
101
+
102
+ async function loadImage(src: string): Promise<HTMLImageElement> {
103
+ const image = new Image();
104
+ image.decoding = 'async';
105
+ image.src = src;
106
+
107
+ if (typeof image.decode === 'function') {
108
+ try {
109
+ await image.decode();
110
+ return image;
111
+ } catch {
112
+ // decode() may reject if called before enough data is available.
113
+ }
114
+ }
115
+
116
+ return await new Promise((resolve, reject) => {
117
+ image.onload = () => resolve(image);
118
+ image.onerror = () => reject(new Error('Failed to decode image'));
119
+ });
120
+ }
121
+
122
+ function createScaledCanvas(
123
+ source: CanvasImageSource,
124
+ sourceWidth: number,
125
+ sourceHeight: number,
126
+ width: number,
127
+ height: number
128
+ ): HTMLCanvasElement {
129
+ const canvas = document.createElement('canvas');
130
+ canvas.width = width;
131
+ canvas.height = height;
132
+
133
+ const context = canvas.getContext('2d', {
134
+ alpha: false,
135
+ desynchronized: true,
136
+ });
137
+
138
+ if (!context) {
139
+ throw new Error('Could not acquire context 2d');
140
+ }
141
+
142
+ context.imageSmoothingEnabled = true;
143
+ context.imageSmoothingQuality = 'high';
144
+ context.drawImage(source, 0, 0, sourceWidth, sourceHeight, 0, 0, width, height);
145
+
146
+ return canvas;
147
+ }
148
+
149
+ function supportsQualityEncoding(mimeType: string): boolean {
150
+ return mimeType === 'image/jpeg' || mimeType === 'image/webp';
151
+ }
152
+
153
+ async function canvasToBlob(
154
+ canvas: HTMLCanvasElement,
155
+ mimeType: string,
156
+ quality?: number
157
+ ): Promise<Blob> {
158
+ return await new Promise((resolve, reject) => {
159
+ canvas.toBlob(
160
+ (blob) => {
161
+ if (blob) {
162
+ resolve(blob);
163
+ return;
164
+ }
165
+
166
+ reject(new Error('Could not create resized image blob'));
167
+ },
168
+ mimeType,
169
+ quality
170
+ );
171
+ });
172
+ }
173
+
174
+ function releaseCanvas(canvas: HTMLCanvasElement) {
175
+ canvas.width = 0;
176
+ canvas.height = 0;
177
+ }
178
+
179
+ function clamp(value: number, min: number, max: number): number {
180
+ return Math.min(max, Math.max(min, value));
181
+ }