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.
- package/dist/sprintify-ui.es.js +3607 -3535
- package/dist/types/utils/index.d.ts +2 -1
- package/dist/types/utils/resizeImageForUpload.d.ts +7 -0
- package/package.json +1 -1
- package/src/components/BaseFileUploader.vue +8 -64
- package/src/utils/index.ts +2 -0
- package/src/utils/resizeImageForUpload.ts +181 -0
|
@@ -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, };
|
package/package.json
CHANGED
|
@@ -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 {
|
|
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
|
|
224
|
-
|
|
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,
|
|
235
|
+
getResizedFileName(file.name, mimeType),
|
|
264
236
|
{
|
|
265
|
-
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;
|
package/src/utils/index.ts
CHANGED
|
@@ -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
|
+
}
|