pic-compressor 0.1.0

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.mjs ADDED
@@ -0,0 +1,536 @@
1
+ /*! pic-compressor v0.1.0 | MIT License | https://github.com/chandq/pic-compressor */
2
+ //#region src/index.ts
3
+ /**
4
+ * Browser image compression utilities.
5
+ * @packageDocumentation
6
+ */
7
+ function isObject(value) {
8
+ return value !== null && typeof value === "object";
9
+ }
10
+ const IMAGE_TYPES = [
11
+ "image/jpeg",
12
+ "image/png",
13
+ "image/webp",
14
+ "image/avif"
15
+ ];
16
+ const DEFAULT_MAX_EDGE = 2560;
17
+ const DEFAULT_MAX_PIXELS = 8388608;
18
+ const LONG_IMAGE_RATIO = 3;
19
+ const DEFAULT_MAX_CANVAS_DIMENSION = 8192;
20
+ const DEFAULT_MAX_ITERATIONS = 8;
21
+ /** Detect whether the current runtime can create a usable Canvas 2D context. */
22
+ function supportCanvas() {
23
+ if (typeof document === "undefined" || typeof document.createElement !== "function") return false;
24
+ try {
25
+ const canvas = document.createElement("canvas");
26
+ return typeof canvas.getContext === "function" && canvas.getContext("2d") !== null;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+ const IMAGE_COMPRESSION_PRESETS = {
32
+ balanced: {
33
+ quality: .82,
34
+ minQuality: .6,
35
+ maxWidth: 1920,
36
+ maxHeight: 1920,
37
+ maxPixels: 6291456,
38
+ targetFileSizeKB: 500,
39
+ preserveLongImage: true
40
+ },
41
+ social: {
42
+ quality: .82,
43
+ minQuality: .62,
44
+ maxWidth: 1280,
45
+ maxHeight: 1280,
46
+ maxPixels: 4194304,
47
+ targetFileSizeKB: 300,
48
+ preserveLongImage: true
49
+ },
50
+ "high-quality": {
51
+ quality: .88,
52
+ minQuality: .72,
53
+ maxWidth: 2560,
54
+ maxHeight: 2560,
55
+ maxPixels: DEFAULT_MAX_PIXELS,
56
+ targetFileSizeKB: 1024,
57
+ preserveLongImage: true
58
+ },
59
+ thumbnail: {
60
+ quality: .78,
61
+ minQuality: .58,
62
+ maxWidth: 400,
63
+ maxHeight: 400,
64
+ maxPixels: 16e4,
65
+ targetFileSizeKB: 30
66
+ },
67
+ "long-image": {
68
+ quality: .82,
69
+ minQuality: .65,
70
+ maxWidth: 1080,
71
+ maxHeight: DEFAULT_MAX_CANVAS_DIMENSION,
72
+ maxPixels: 12582912,
73
+ preserveLongImage: true
74
+ }
75
+ };
76
+ function isFile(value) {
77
+ return typeof File !== "undefined" && value instanceof File || Object.prototype.toString.call(value) === "[object File]";
78
+ }
79
+ function isFileList(value) {
80
+ return typeof FileList !== "undefined" && value instanceof FileList || Object.prototype.toString.call(value) === "[object FileList]";
81
+ }
82
+ function isBlob(value) {
83
+ return typeof Blob !== "undefined" && value instanceof Blob || Object.prototype.toString.call(value) === "[object Blob]";
84
+ }
85
+ function createAbortError() {
86
+ if (typeof DOMException !== "undefined") return new DOMException("Image compression aborted", "AbortError");
87
+ const error = /* @__PURE__ */ new Error("Image compression aborted");
88
+ error.name = "AbortError";
89
+ return error;
90
+ }
91
+ function throwIfAborted(signal) {
92
+ if (signal?.aborted) throw createAbortError();
93
+ }
94
+ function assertFiniteNumber(value, name, min, max = Infinity) {
95
+ if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) throw new RangeError(`${name} must be a finite number between ${min} and ${max}`);
96
+ }
97
+ function normalizeOptions(options) {
98
+ const { preset = "balanced", quality: configuredQuality, mime = "image/jpeg", maxWidth, maxHeight, maxSize, minFileSizeKB = 50, maxPixels: configuredMaxPixels, maxCanvasDimension = DEFAULT_MAX_CANVAS_DIMENSION, preserveLongImage: configuredPreserveLongImage, targetFileSizeKB: configuredTargetFileSizeKB, minQuality: configuredMinQuality, maxIterations = DEFAULT_MAX_ITERATIONS, concurrency = 2, outputMode = "legacy", keepOriginalIfLarger = true, backgroundColor = "#fff", strictMime = false, onProgress, signal, fileName = "image" } = isObject(options) ? options : {};
99
+ const presetOptions = IMAGE_COMPRESSION_PRESETS[preset];
100
+ if (!presetOptions) throw new TypeError(`Unsupported image compression preset: ${String(preset)}`);
101
+ const quality = configuredQuality ?? presetOptions.quality;
102
+ const minQuality = configuredMinQuality ?? Math.min(presetOptions.minQuality, quality);
103
+ const maxPixels = configuredMaxPixels ?? presetOptions.maxPixels;
104
+ const targetFileSizeKB = configuredTargetFileSizeKB === null ? void 0 : configuredTargetFileSizeKB ?? presetOptions.targetFileSizeKB;
105
+ const hasExplicitDimensions = maxWidth !== void 0 || maxHeight !== void 0 || maxSize !== void 0;
106
+ const resolvedMaxWidth = hasExplicitDimensions ? maxWidth ?? maxSize : presetOptions.maxWidth;
107
+ const resolvedMaxHeight = hasExplicitDimensions ? maxHeight ?? maxSize : presetOptions.maxHeight;
108
+ const preserveLongImage = configuredPreserveLongImage ?? (!hasExplicitDimensions && !!presetOptions.preserveLongImage);
109
+ assertFiniteNumber(quality, "quality", 0, 1);
110
+ assertFiniteNumber(minQuality, "minQuality", 0, 1);
111
+ if (minQuality > quality) throw new RangeError("minQuality must not be greater than quality");
112
+ if (!IMAGE_TYPES.includes(mime)) throw new TypeError(`Unsupported image mime type: ${String(mime)}`);
113
+ if (resolvedMaxWidth !== void 0) assertFiniteNumber(resolvedMaxWidth, "maxWidth", 1);
114
+ if (resolvedMaxHeight !== void 0) assertFiniteNumber(resolvedMaxHeight, "maxHeight", 1);
115
+ if (maxSize !== void 0) assertFiniteNumber(maxSize, "maxSize", 1);
116
+ assertFiniteNumber(minFileSizeKB, "minFileSizeKB", 0);
117
+ assertFiniteNumber(maxPixels, "maxPixels", 1);
118
+ assertFiniteNumber(maxCanvasDimension, "maxCanvasDimension", 1);
119
+ if (targetFileSizeKB !== void 0) assertFiniteNumber(targetFileSizeKB, "targetFileSizeKB", 1);
120
+ assertFiniteNumber(maxIterations, "maxIterations", 1);
121
+ assertFiniteNumber(concurrency, "concurrency", 1);
122
+ if (typeof keepOriginalIfLarger !== "boolean") throw new TypeError("keepOriginalIfLarger must be a boolean");
123
+ if (typeof backgroundColor !== "string") throw new TypeError("backgroundColor must be a string");
124
+ if (typeof strictMime !== "boolean") throw new TypeError("strictMime must be a boolean");
125
+ if (typeof preserveLongImage !== "boolean") throw new TypeError("preserveLongImage must be a boolean");
126
+ if (onProgress !== void 0 && typeof onProgress !== "function") throw new TypeError("onProgress must be a function");
127
+ if (typeof fileName !== "string" || fileName.length === 0) throw new TypeError("fileName must be a non-empty string");
128
+ if (outputMode !== "legacy" && outputMode !== "compact") throw new TypeError(`outputMode must be "legacy" or "compact"`);
129
+ return {
130
+ preset,
131
+ quality,
132
+ mime,
133
+ maxWidth: resolvedMaxWidth === void 0 ? void 0 : Math.floor(resolvedMaxWidth),
134
+ maxHeight: resolvedMaxHeight === void 0 ? void 0 : Math.floor(resolvedMaxHeight),
135
+ minFileSizeKB,
136
+ maxPixels: Math.floor(maxPixels),
137
+ maxCanvasDimension: Math.floor(maxCanvasDimension),
138
+ preserveLongImage,
139
+ targetFileSizeKB,
140
+ minQuality,
141
+ maxIterations: Math.max(1, Math.floor(maxIterations)),
142
+ concurrency: Math.max(1, Math.floor(concurrency)),
143
+ outputMode,
144
+ keepOriginalIfLarger,
145
+ backgroundColor,
146
+ strictMime,
147
+ onProgress,
148
+ signal,
149
+ fileName
150
+ };
151
+ }
152
+ function blobToFile(blob, fileName) {
153
+ return new File([blob], fileName, {
154
+ type: blob.type,
155
+ lastModified: Date.now()
156
+ });
157
+ }
158
+ function calculateTargetSize(maxWidth, maxHeight, maxPixels, maxCanvasDimension, preserveLongImage, originWidth, originHeight) {
159
+ let targetMaxWidth = maxWidth;
160
+ let targetMaxHeight = maxHeight;
161
+ if (targetMaxWidth === void 0 && targetMaxHeight === void 0) {
162
+ targetMaxWidth = DEFAULT_MAX_EDGE;
163
+ targetMaxHeight = DEFAULT_MAX_EDGE;
164
+ }
165
+ const aspectRatio = Math.max(originWidth / originHeight, originHeight / originWidth);
166
+ if (preserveLongImage && aspectRatio >= LONG_IMAGE_RATIO) {
167
+ if (originWidth >= originHeight) targetMaxWidth = maxCanvasDimension;
168
+ else targetMaxHeight = maxCanvasDimension;
169
+ }
170
+ targetMaxWidth = Math.min(targetMaxWidth ?? maxCanvasDimension, maxCanvasDimension);
171
+ targetMaxHeight = Math.min(targetMaxHeight ?? maxCanvasDimension, maxCanvasDimension);
172
+ const dimensionScale = Math.min(1, targetMaxWidth / originWidth, targetMaxHeight / originHeight);
173
+ const pixelScale = Math.min(1, Math.sqrt(maxPixels / (originWidth * originHeight)));
174
+ const scale = Math.min(dimensionScale, pixelScale);
175
+ return {
176
+ width: Math.max(1, Math.floor(originWidth * scale)),
177
+ height: Math.max(1, Math.floor(originHeight * scale))
178
+ };
179
+ }
180
+ function readBlob(blob, mode, signal) {
181
+ return new Promise((resolve, reject) => {
182
+ throwIfAborted(signal);
183
+ const reader = new FileReader();
184
+ let settled = false;
185
+ const cleanup = () => {
186
+ reader.onerror = null;
187
+ reader.onabort = null;
188
+ reader.onload = null;
189
+ signal?.removeEventListener("abort", abort);
190
+ };
191
+ const fail = (error) => {
192
+ if (settled) return;
193
+ settled = true;
194
+ cleanup();
195
+ reject(error);
196
+ };
197
+ const abort = () => {
198
+ try {
199
+ reader.abort();
200
+ } finally {
201
+ fail(createAbortError());
202
+ }
203
+ };
204
+ reader.onerror = () => fail(reader.error ?? /* @__PURE__ */ new Error("Failed to read image data"));
205
+ reader.onabort = () => fail(createAbortError());
206
+ reader.onload = () => {
207
+ if (settled) return;
208
+ const result = reader.result;
209
+ if (mode === "dataURL" && typeof result === "string") {
210
+ settled = true;
211
+ cleanup();
212
+ resolve(result);
213
+ } else if (mode === "arrayBuffer" && result instanceof ArrayBuffer) {
214
+ settled = true;
215
+ cleanup();
216
+ resolve(result);
217
+ } else fail(/* @__PURE__ */ new Error(`Unexpected FileReader result for ${mode}`));
218
+ };
219
+ signal?.addEventListener("abort", abort, { once: true });
220
+ if (mode === "dataURL") reader.readAsDataURL(blob);
221
+ else reader.readAsArrayBuffer(blob);
222
+ });
223
+ }
224
+ async function decodeImage(file, signal) {
225
+ throwIfAborted(signal);
226
+ if (typeof createImageBitmap === "function") try {
227
+ const bitmap = await createImageBitmap(file);
228
+ try {
229
+ throwIfAborted(signal);
230
+ if (!bitmap.width || !bitmap.height) throw new Error("Decoded image has invalid dimensions");
231
+ } catch (error) {
232
+ bitmap.close();
233
+ throw error;
234
+ }
235
+ return {
236
+ source: bitmap,
237
+ width: bitmap.width,
238
+ height: bitmap.height,
239
+ release: () => bitmap.close()
240
+ };
241
+ } catch (error) {
242
+ if (error.name === "AbortError") throw error;
243
+ }
244
+ return new Promise((resolve, reject) => {
245
+ const image = new Image();
246
+ let objectURL;
247
+ let settled = false;
248
+ const cleanup = () => {
249
+ image.onload = null;
250
+ image.onerror = null;
251
+ signal?.removeEventListener("abort", abort);
252
+ if (objectURL && typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(objectURL);
253
+ };
254
+ const fail = (error) => {
255
+ if (settled) return;
256
+ settled = true;
257
+ cleanup();
258
+ reject(error);
259
+ };
260
+ const abort = () => fail(createAbortError());
261
+ image.onload = () => {
262
+ if (settled) return;
263
+ settled = true;
264
+ const width = image.naturalWidth || image.width;
265
+ const height = image.naturalHeight || image.height;
266
+ cleanup();
267
+ if (!width || !height) reject(/* @__PURE__ */ new Error("Decoded image has invalid dimensions"));
268
+ else resolve({
269
+ source: image,
270
+ width,
271
+ height,
272
+ release: () => void 0
273
+ });
274
+ };
275
+ image.onerror = () => fail(/* @__PURE__ */ new Error(`Failed to decode image: ${file.name}`));
276
+ signal?.addEventListener("abort", abort, { once: true });
277
+ try {
278
+ if (typeof URL !== "undefined" && typeof URL.createObjectURL === "function") {
279
+ objectURL = URL.createObjectURL(file);
280
+ image.src = objectURL;
281
+ } else readBlob(file, "dataURL", signal).then((src) => image.src = src, fail);
282
+ } catch (error) {
283
+ fail(error instanceof Error ? error : new Error(String(error)));
284
+ }
285
+ });
286
+ }
287
+ function canvasToBlob(canvas, mime, quality) {
288
+ if (typeof canvas.toBlob === "function") return new Promise((resolve, reject) => {
289
+ canvas.toBlob((blob) => blob ? resolve(blob) : reject(/* @__PURE__ */ new Error("Canvas image encoding failed")), mime, quality);
290
+ });
291
+ try {
292
+ const [header, encoded = ""] = canvas.toDataURL(mime, quality).split(",");
293
+ const actualMime = /^data:([^;]+)/.exec(header ?? "")?.[1] || mime;
294
+ const binary = atob(encoded);
295
+ const bytes = new Uint8Array(binary.length);
296
+ for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
297
+ return Promise.resolve(new Blob([bytes], { type: actualMime }));
298
+ } catch (error) {
299
+ return Promise.reject(error);
300
+ }
301
+ }
302
+ function normalizeMime(mime) {
303
+ if (mime === "image/jpg") return "image/jpeg";
304
+ return IMAGE_TYPES.includes(mime) ? mime : void 0;
305
+ }
306
+ function replaceFileExtension(name, mime) {
307
+ const extension = {
308
+ "image/jpeg": "jpg",
309
+ "image/png": "png",
310
+ "image/webp": "webp",
311
+ "image/avif": "avif"
312
+ };
313
+ const dotIndex = name.lastIndexOf(".");
314
+ return `${dotIndex > 0 ? name.slice(0, dotIndex) : name || "image"}.${extension[mime]}`;
315
+ }
316
+ function reportProgress(options, progress) {
317
+ if (!options.onProgress) return;
318
+ try {
319
+ options.onProgress(Math.max(0, Math.min(100, Math.round(progress))));
320
+ } catch {}
321
+ }
322
+ function requiresCompression(file, options) {
323
+ const sizeKB = file.size / 1024;
324
+ return sizeKB >= options.minFileSizeKB || options.targetFileSizeKB !== void 0 && sizeKB > options.targetFileSizeKB;
325
+ }
326
+ function supportsLossyQuality(mime) {
327
+ return mime === "image/jpeg" || mime === "image/webp" || mime === "image/avif";
328
+ }
329
+ function getEncodedMime(blob, requestedMime, strictMime) {
330
+ const actualMime = normalizeMime(blob.type);
331
+ if (!actualMime) throw new Error(`Canvas returned unsupported image mime type: ${blob.type || "unknown"}`);
332
+ if (strictMime && actualMime !== requestedMime) throw new Error(`Current runtime does not support encoding ${requestedMime}; received ${actualMime} instead`);
333
+ return actualMime;
334
+ }
335
+ async function encodeCanvas(canvas, options, targetBytes, remainingIterations, completedIterations) {
336
+ let iterations = 0;
337
+ const encode = async (quality) => {
338
+ throwIfAborted(options.signal);
339
+ const blob = await canvasToBlob(canvas, options.mime, quality);
340
+ iterations++;
341
+ reportProgress(options, 10 + (completedIterations + iterations) / options.maxIterations * 80);
342
+ return {
343
+ blob,
344
+ mime: getEncodedMime(blob, options.mime, options.strictMime),
345
+ quality
346
+ };
347
+ };
348
+ const result = await encode(options.quality);
349
+ if (targetBytes === void 0 || result.blob.size <= targetBytes || !supportsLossyQuality(result.mime) || remainingIterations <= 1 || options.quality <= options.minQuality) return {
350
+ ...result,
351
+ iterations
352
+ };
353
+ const minimumQualityResult = await encode(options.minQuality);
354
+ if (minimumQualityResult.blob.size > targetBytes) return {
355
+ ...minimumQualityResult,
356
+ iterations
357
+ };
358
+ let bestResult = minimumQualityResult;
359
+ let lowerQuality = options.minQuality;
360
+ let upperQuality = options.quality;
361
+ const searchIterations = remainingIterations - iterations;
362
+ for (let index = 0; index < searchIterations && upperQuality - lowerQuality > .01; index++) {
363
+ const nextQuality = (lowerQuality + upperQuality) / 2;
364
+ const nextResult = await encode(nextQuality);
365
+ if (nextResult.blob.size <= targetBytes) {
366
+ bestResult = nextResult;
367
+ lowerQuality = nextQuality;
368
+ } else upperQuality = nextQuality;
369
+ }
370
+ return {
371
+ ...bestResult,
372
+ iterations
373
+ };
374
+ }
375
+ async function encodeImage(decoded, initialWidth, initialHeight, options, targetBytes) {
376
+ let width = initialWidth;
377
+ let height = initialHeight;
378
+ let totalIterations = 0;
379
+ let lastResult;
380
+ while (totalIterations < options.maxIterations) {
381
+ const canvas = document.createElement("canvas");
382
+ canvas.width = width;
383
+ canvas.height = height;
384
+ const context = canvas.getContext("2d");
385
+ if (!context) throw new Error("Failed to create Canvas 2D context");
386
+ context.imageSmoothingEnabled = true;
387
+ context.imageSmoothingQuality = "high";
388
+ if (options.mime === "image/jpeg") {
389
+ context.fillStyle = options.backgroundColor;
390
+ context.fillRect(0, 0, width, height);
391
+ }
392
+ context.drawImage(decoded.source, 0, 0, width, height);
393
+ throwIfAborted(options.signal);
394
+ try {
395
+ lastResult = await encodeCanvas(canvas, options, targetBytes, options.maxIterations - totalIterations, totalIterations);
396
+ totalIterations += lastResult.iterations;
397
+ } finally {
398
+ canvas.width = 1;
399
+ canvas.height = 1;
400
+ }
401
+ if (targetBytes === void 0 || lastResult.blob.size <= targetBytes || totalIterations >= options.maxIterations) return {
402
+ ...lastResult,
403
+ width,
404
+ height,
405
+ iterations: totalIterations
406
+ };
407
+ const targetScale = Math.min(.9, Math.sqrt(targetBytes / lastResult.blob.size) * .95);
408
+ const nextWidth = Math.max(1, Math.floor(width * targetScale));
409
+ const nextHeight = Math.max(1, Math.floor(height * targetScale));
410
+ if (nextWidth === width && nextHeight === height || width === 1 && height === 1) return {
411
+ ...lastResult,
412
+ width,
413
+ height,
414
+ iterations: totalIterations
415
+ };
416
+ width = nextWidth;
417
+ height = nextHeight;
418
+ }
419
+ if (!lastResult) throw new Error("Image encoding did not produce a result");
420
+ return {
421
+ ...lastResult,
422
+ width,
423
+ height,
424
+ iterations: totalIterations
425
+ };
426
+ }
427
+ async function compressOne(file, options, canvasSupported) {
428
+ reportProgress(options, 0);
429
+ throwIfAborted(options.signal);
430
+ if (file.type && !file.type.startsWith("image/")) throw new TypeError(`${file.name} is not an image file`);
431
+ const beforeKB = file.size / 1024;
432
+ if (!requiresCompression(file, options)) {
433
+ reportProgress(options, 100);
434
+ return { file };
435
+ }
436
+ if (!canvasSupported) throw new Error("Current runtime environment not support Canvas");
437
+ const decoded = await decodeImage(file, options.signal);
438
+ try {
439
+ reportProgress(options, 8);
440
+ throwIfAborted(options.signal);
441
+ const targetSize = calculateTargetSize(options.maxWidth, options.maxHeight, options.maxPixels, options.maxCanvasDimension, options.preserveLongImage, decoded.width, decoded.height);
442
+ const configuredTargetBytes = options.targetFileSizeKB === void 0 ? void 0 : options.targetFileSizeKB * 1024;
443
+ const sourceMime = normalizeMime(file.type);
444
+ const dimensionsInitiallyUnchanged = targetSize.width === decoded.width && targetSize.height === decoded.height;
445
+ const originalSizeTargetBytes = options.keepOriginalIfLarger && dimensionsInitiallyUnchanged && sourceMime === options.mime ? Math.max(0, file.size - 1) : void 0;
446
+ const encodingTargetBytes = configuredTargetBytes === void 0 ? originalSizeTargetBytes : originalSizeTargetBytes === void 0 ? configuredTargetBytes : Math.min(configuredTargetBytes, originalSizeTargetBytes);
447
+ const encoded = await encodeImage(decoded, targetSize.width, targetSize.height, options, encodingTargetBytes);
448
+ throwIfAborted(options.signal);
449
+ const { blob, mime: actualMime, width, height, quality, iterations } = encoded;
450
+ if (options.keepOriginalIfLarger && dimensionsInitiallyUnchanged && sourceMime === actualMime && blob.size >= file.size) {
451
+ reportProgress(options, 100);
452
+ return { file };
453
+ }
454
+ const outputFile = new File([blob], replaceFileExtension(file.name, actualMime), {
455
+ type: actualMime,
456
+ lastModified: file.lastModified
457
+ });
458
+ const result = {
459
+ file: outputFile,
460
+ beforeKB: Number(beforeKB.toFixed(2)),
461
+ afterKB: Number((outputFile.size / 1024).toFixed(2)),
462
+ width,
463
+ height,
464
+ mime: actualMime,
465
+ compressed: true,
466
+ quality,
467
+ iterations,
468
+ ...options.targetFileSizeKB === void 0 ? {} : { targetAchieved: blob.size <= options.targetFileSizeKB * 1024 }
469
+ };
470
+ if (options.outputMode === "legacy") {
471
+ const [beforeSrc, afterSrc, arrayBuffer] = await Promise.all([
472
+ readBlob(file, "dataURL", options.signal),
473
+ readBlob(blob, "dataURL", options.signal),
474
+ readBlob(blob, "arrayBuffer", options.signal)
475
+ ]);
476
+ throwIfAborted(options.signal);
477
+ result.origin = file;
478
+ result.beforeSrc = beforeSrc;
479
+ result.afterSrc = afterSrc;
480
+ result.bufferArray = new Uint8Array(arrayBuffer);
481
+ }
482
+ reportProgress(options, 100);
483
+ return result;
484
+ } finally {
485
+ decoded.release();
486
+ }
487
+ }
488
+ async function mapWithConcurrency(items, concurrency, worker) {
489
+ const results = [];
490
+ let cursor = 0;
491
+ let failed = false;
492
+ const execute = async () => {
493
+ while (!failed && cursor < items.length) {
494
+ const index = cursor++;
495
+ try {
496
+ results[index] = await worker(items[index], index);
497
+ } catch (error) {
498
+ failed = true;
499
+ throw error;
500
+ }
501
+ }
502
+ };
503
+ await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, execute));
504
+ return results;
505
+ }
506
+ function compressImage(file, options = {}) {
507
+ const singleFile = isFile(file);
508
+ const singleBlob = !singleFile && isBlob(file);
509
+ const multipleFiles = isFileList(file);
510
+ if (!singleFile && !singleBlob && !multipleFiles) throw new TypeError(`${String(file)} require be File or FileList`);
511
+ const normalizedOptions = normalizeOptions(options);
512
+ throwIfAborted(normalizedOptions.signal);
513
+ if (singleFile || singleBlob) {
514
+ const normalizedFile = singleFile ? file : blobToFile(file, normalizedOptions.fileName);
515
+ return compressOne(normalizedFile, normalizedOptions, !requiresCompression(normalizedFile, normalizedOptions) || supportCanvas());
516
+ }
517
+ const files = Array.from(file);
518
+ const canvasSupported = !files.some((item) => requiresCompression(item, normalizedOptions)) || supportCanvas();
519
+ const progresses = Array.from({ length: files.length }, () => 0);
520
+ return mapWithConcurrency(files, normalizedOptions.concurrency, (item, index) => {
521
+ return compressOne(item, normalizedOptions.onProgress ? {
522
+ ...normalizedOptions,
523
+ onProgress: (progress) => {
524
+ progresses[index] = progress;
525
+ reportProgress(normalizedOptions, progresses.reduce((sum, current) => sum + current, 0) / files.length);
526
+ }
527
+ } : normalizedOptions, canvasSupported);
528
+ });
529
+ }
530
+ var src_default = {
531
+ IMAGE_COMPRESSION_PRESETS,
532
+ supportCanvas,
533
+ compressImage
534
+ };
535
+ //#endregion
536
+ export { IMAGE_COMPRESSION_PRESETS, compressImage, src_default as default, supportCanvas };
@@ -0,0 +1,2 @@
1
+ /*! pic-compressor v0.1.0 | MIT License | https://github.com/chandq/pic-compressor */
2
+ (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.PicCompressor={}))})(this,function(e){Object.defineProperties(e,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});function t(e){return typeof e==`object`&&!!e}let n=[`image/jpeg`,`image/png`,`image/webp`,`image/avif`],r=2560,i=8192;function a(){if(typeof document>`u`||typeof document.createElement!=`function`)return!1;try{let e=document.createElement(`canvas`);return typeof e.getContext==`function`&&e.getContext(`2d`)!==null}catch{return!1}}let o={balanced:{quality:.82,minQuality:.6,maxWidth:1920,maxHeight:1920,maxPixels:6291456,targetFileSizeKB:500,preserveLongImage:!0},social:{quality:.82,minQuality:.62,maxWidth:1280,maxHeight:1280,maxPixels:4194304,targetFileSizeKB:300,preserveLongImage:!0},"high-quality":{quality:.88,minQuality:.72,maxWidth:2560,maxHeight:2560,maxPixels:8388608,targetFileSizeKB:1024,preserveLongImage:!0},thumbnail:{quality:.78,minQuality:.58,maxWidth:400,maxHeight:400,maxPixels:16e4,targetFileSizeKB:30},"long-image":{quality:.82,minQuality:.65,maxWidth:1080,maxHeight:i,maxPixels:12582912,preserveLongImage:!0}};function s(e){return typeof File<`u`&&e instanceof File||Object.prototype.toString.call(e)===`[object File]`}function c(e){return typeof FileList<`u`&&e instanceof FileList||Object.prototype.toString.call(e)===`[object FileList]`}function l(e){return typeof Blob<`u`&&e instanceof Blob||Object.prototype.toString.call(e)===`[object Blob]`}function u(){if(typeof DOMException<`u`)return new DOMException(`Image compression aborted`,`AbortError`);let e=/* @__PURE__ */ Error(`Image compression aborted`);return e.name=`AbortError`,e}function d(e){if(e?.aborted)throw u()}function f(e,t,n,r=1/0){if(typeof e!=`number`||!Number.isFinite(e)||e<n||e>r)throw RangeError(`${t} must be a finite number between ${n} and ${r}`)}function p(e){let{preset:r=`balanced`,quality:a,mime:s=`image/jpeg`,maxWidth:c,maxHeight:l,maxSize:u,minFileSizeKB:d=50,maxPixels:p,maxCanvasDimension:m=i,preserveLongImage:h,targetFileSizeKB:g,minQuality:_,maxIterations:v=8,concurrency:y=2,outputMode:b=`legacy`,keepOriginalIfLarger:x=!0,backgroundColor:S=`#fff`,strictMime:C=!1,onProgress:w,signal:T,fileName:E=`image`}=t(e)?e:{},D=o[r];if(!D)throw TypeError(`Unsupported image compression preset: ${String(r)}`);let O=a??D.quality,k=_??Math.min(D.minQuality,O),A=p??D.maxPixels,j=g===null?void 0:g??D.targetFileSizeKB,M=c!==void 0||l!==void 0||u!==void 0,N=M?c??u:D.maxWidth,P=M?l??u:D.maxHeight,F=h??(!M&&!!D.preserveLongImage);if(f(O,`quality`,0,1),f(k,`minQuality`,0,1),k>O)throw RangeError(`minQuality must not be greater than quality`);if(!n.includes(s))throw TypeError(`Unsupported image mime type: ${String(s)}`);if(N!==void 0&&f(N,`maxWidth`,1),P!==void 0&&f(P,`maxHeight`,1),u!==void 0&&f(u,`maxSize`,1),f(d,`minFileSizeKB`,0),f(A,`maxPixels`,1),f(m,`maxCanvasDimension`,1),j!==void 0&&f(j,`targetFileSizeKB`,1),f(v,`maxIterations`,1),f(y,`concurrency`,1),typeof x!=`boolean`)throw TypeError(`keepOriginalIfLarger must be a boolean`);if(typeof S!=`string`)throw TypeError(`backgroundColor must be a string`);if(typeof C!=`boolean`)throw TypeError(`strictMime must be a boolean`);if(typeof F!=`boolean`)throw TypeError(`preserveLongImage must be a boolean`);if(w!==void 0&&typeof w!=`function`)throw TypeError(`onProgress must be a function`);if(typeof E!=`string`||E.length===0)throw TypeError(`fileName must be a non-empty string`);if(b!==`legacy`&&b!==`compact`)throw TypeError(`outputMode must be "legacy" or "compact"`);return{preset:r,quality:O,mime:s,maxWidth:N===void 0?void 0:Math.floor(N),maxHeight:P===void 0?void 0:Math.floor(P),minFileSizeKB:d,maxPixels:Math.floor(A),maxCanvasDimension:Math.floor(m),preserveLongImage:F,targetFileSizeKB:j,minQuality:k,maxIterations:Math.max(1,Math.floor(v)),concurrency:Math.max(1,Math.floor(y)),outputMode:b,keepOriginalIfLarger:x,backgroundColor:S,strictMime:C,onProgress:w,signal:T,fileName:E}}function m(e,t){return new File([e],t,{type:e.type,lastModified:Date.now()})}function h(e,t,n,i,a,o,s){let c=e,l=t;c===void 0&&l===void 0&&(c=r,l=r);let u=Math.max(o/s,s/o);a&&u>=3&&(o>=s?c=i:l=i),c=Math.min(c??i,i),l=Math.min(l??i,i);let d=Math.min(1,c/o,l/s),f=Math.min(1,Math.sqrt(n/(o*s))),p=Math.min(d,f);return{width:Math.max(1,Math.floor(o*p)),height:Math.max(1,Math.floor(s*p))}}function g(e,t,n){return new Promise((r,i)=>{d(n);let a=new FileReader,o=!1,s=()=>{a.onerror=null,a.onabort=null,a.onload=null,n?.removeEventListener(`abort`,l)},c=e=>{o||(o=!0,s(),i(e))},l=()=>{try{a.abort()}finally{c(u())}};a.onerror=()=>c(a.error??/* @__PURE__ */ Error(`Failed to read image data`)),a.onabort=()=>c(u()),a.onload=()=>{if(o)return;let e=a.result;t===`dataURL`&&typeof e==`string`||t===`arrayBuffer`&&e instanceof ArrayBuffer?(o=!0,s(),r(e)):c(/* @__PURE__ */ Error(`Unexpected FileReader result for ${t}`))},n?.addEventListener(`abort`,l,{once:!0}),t===`dataURL`?a.readAsDataURL(e):a.readAsArrayBuffer(e)})}async function _(e,t){if(d(t),typeof createImageBitmap==`function`)try{let n=await createImageBitmap(e);try{if(d(t),!n.width||!n.height)throw Error(`Decoded image has invalid dimensions`)}catch(e){throw n.close(),e}return{source:n,width:n.width,height:n.height,release:()=>n.close()}}catch(e){if(e.name===`AbortError`)throw e}return new Promise((n,r)=>{let i=new Image,a,o=!1,s=()=>{i.onload=null,i.onerror=null,t?.removeEventListener(`abort`,l),a&&typeof URL.revokeObjectURL==`function`&&URL.revokeObjectURL(a)},c=e=>{o||(o=!0,s(),r(e))},l=()=>c(u());i.onload=()=>{if(o)return;o=!0;let e=i.naturalWidth||i.width,t=i.naturalHeight||i.height;s(),!e||!t?r(/* @__PURE__ */ Error(`Decoded image has invalid dimensions`)):n({source:i,width:e,height:t,release:()=>void 0})},i.onerror=()=>c(/* @__PURE__ */ Error(`Failed to decode image: ${e.name}`)),t?.addEventListener(`abort`,l,{once:!0});try{typeof URL<`u`&&typeof URL.createObjectURL==`function`?(a=URL.createObjectURL(e),i.src=a):g(e,`dataURL`,t).then(e=>i.src=e,c)}catch(e){c(e instanceof Error?e:Error(String(e)))}})}function v(e,t,n){if(typeof e.toBlob==`function`)return new Promise((r,i)=>{e.toBlob(e=>e?r(e):i(/* @__PURE__ */ Error(`Canvas image encoding failed`)),t,n)});try{let[r,i=``]=e.toDataURL(t,n).split(`,`),a=/^data:([^;]+)/.exec(r??``)?.[1]||t,o=atob(i),s=new Uint8Array(o.length);for(let e=0;e<o.length;e++)s[e]=o.charCodeAt(e);return Promise.resolve(new Blob([s],{type:a}))}catch(e){return Promise.reject(e)}}function y(e){return e===`image/jpg`?`image/jpeg`:n.includes(e)?e:void 0}function b(e,t){let n={"image/jpeg":`jpg`,"image/png":`png`,"image/webp":`webp`,"image/avif":`avif`},r=e.lastIndexOf(`.`);return`${r>0?e.slice(0,r):e||`image`}.${n[t]}`}function x(e,t){if(e.onProgress)try{e.onProgress(Math.max(0,Math.min(100,Math.round(t))))}catch{}}function S(e,t){let n=e.size/1024;return n>=t.minFileSizeKB||t.targetFileSizeKB!==void 0&&n>t.targetFileSizeKB}function C(e){return e===`image/jpeg`||e===`image/webp`||e===`image/avif`}function w(e,t,n){let r=y(e.type);if(!r)throw Error(`Canvas returned unsupported image mime type: ${e.type||`unknown`}`);if(n&&r!==t)throw Error(`Current runtime does not support encoding ${t}; received ${r} instead`);return r}async function T(e,t,n,r,i){let a=0,o=async n=>{d(t.signal);let r=await v(e,t.mime,n);return a++,x(t,10+(i+a)/t.maxIterations*80),{blob:r,mime:w(r,t.mime,t.strictMime),quality:n}},s=await o(t.quality);if(n===void 0||s.blob.size<=n||!C(s.mime)||r<=1||t.quality<=t.minQuality)return{...s,iterations:a};let c=await o(t.minQuality);if(c.blob.size>n)return{...c,iterations:a};let l=c,u=t.minQuality,f=t.quality,p=r-a;for(let e=0;e<p&&f-u>.01;e++){let e=(u+f)/2,t=await o(e);t.blob.size<=n?(l=t,u=e):f=e}return{...l,iterations:a}}async function E(e,t,n,r,i){let a=t,o=n,s=0,c;for(;s<r.maxIterations;){let t=document.createElement(`canvas`);t.width=a,t.height=o;let n=t.getContext(`2d`);if(!n)throw Error(`Failed to create Canvas 2D context`);n.imageSmoothingEnabled=!0,n.imageSmoothingQuality=`high`,r.mime===`image/jpeg`&&(n.fillStyle=r.backgroundColor,n.fillRect(0,0,a,o)),n.drawImage(e.source,0,0,a,o),d(r.signal);try{c=await T(t,r,i,r.maxIterations-s,s),s+=c.iterations}finally{t.width=1,t.height=1}if(i===void 0||c.blob.size<=i||s>=r.maxIterations)return{...c,width:a,height:o,iterations:s};let l=Math.min(.9,Math.sqrt(i/c.blob.size)*.95),u=Math.max(1,Math.floor(a*l)),f=Math.max(1,Math.floor(o*l));if(u===a&&f===o||a===1&&o===1)return{...c,width:a,height:o,iterations:s};a=u,o=f}if(!c)throw Error(`Image encoding did not produce a result`);return{...c,width:a,height:o,iterations:s}}async function D(e,t,n){if(x(t,0),d(t.signal),e.type&&!e.type.startsWith(`image/`))throw TypeError(`${e.name} is not an image file`);let r=e.size/1024;if(!S(e,t))return x(t,100),{file:e};if(!n)throw Error(`Current runtime environment not support Canvas`);let i=await _(e,t.signal);try{x(t,8),d(t.signal);let n=h(t.maxWidth,t.maxHeight,t.maxPixels,t.maxCanvasDimension,t.preserveLongImage,i.width,i.height),a=t.targetFileSizeKB===void 0?void 0:t.targetFileSizeKB*1024,o=y(e.type),s=n.width===i.width&&n.height===i.height,c=t.keepOriginalIfLarger&&s&&o===t.mime?Math.max(0,e.size-1):void 0,l=a===void 0?c:c===void 0?a:Math.min(a,c),u=await E(i,n.width,n.height,t,l);d(t.signal);let{blob:f,mime:p,width:m,height:_,quality:v,iterations:S}=u;if(t.keepOriginalIfLarger&&s&&o===p&&f.size>=e.size)return x(t,100),{file:e};let C=new File([f],b(e.name,p),{type:p,lastModified:e.lastModified}),w={file:C,beforeKB:Number(r.toFixed(2)),afterKB:Number((C.size/1024).toFixed(2)),width:m,height:_,mime:p,compressed:!0,quality:v,iterations:S,...t.targetFileSizeKB===void 0?{}:{targetAchieved:f.size<=t.targetFileSizeKB*1024}};if(t.outputMode===`legacy`){let[n,r,i]=await Promise.all([g(e,`dataURL`,t.signal),g(f,`dataURL`,t.signal),g(f,`arrayBuffer`,t.signal)]);d(t.signal),w.origin=e,w.beforeSrc=n,w.afterSrc=r,w.bufferArray=new Uint8Array(i)}return x(t,100),w}finally{i.release()}}async function O(e,t,n){let r=[],i=0,a=!1;return await Promise.all(Array.from({length:Math.min(t,e.length)},async()=>{for(;!a&&i<e.length;){let t=i++;try{r[t]=await n(e[t],t)}catch(e){throw a=!0,e}}})),r}function k(e,t={}){let n=s(e),r=!n&&l(e),i=c(e);if(!n&&!r&&!i)throw TypeError(`${String(e)} require be File or FileList`);let o=p(t);if(d(o.signal),n||r){let t=n?e:m(e,o.fileName);return D(t,o,!S(t,o)||a())}let u=Array.from(e),f=!u.some(e=>S(e,o))||a(),h=Array.from({length:u.length},()=>0);return O(u,o.concurrency,(e,t)=>D(e,o.onProgress?{...o,onProgress:e=>{h[t]=e,x(o,h.reduce((e,t)=>e+t,0)/u.length)}}:o,f))}var A={IMAGE_COMPRESSION_PRESETS:o,supportCanvas:a,compressImage:k};e.IMAGE_COMPRESSION_PRESETS=o,e.compressImage=k,e.default=A,e.supportCanvas=a});
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "pic-compressor",
3
+ "version": "0.1.0",
4
+ "description": "Zero-dependency browser image compressor with presets, target-size iteration, long-image support, and batch processing.",
5
+ "keywords": [
6
+ "avif",
7
+ "compress",
8
+ "compressImage",
9
+ "compressImg",
10
+ "compressor",
11
+ "image",
12
+ "jpeg",
13
+ "pic-compressor",
14
+ "png",
15
+ "webp"
16
+ ],
17
+ "homepage": "https://github.com/chandq/pic-compressor#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/chandq/pic-compressor/issues"
20
+ },
21
+ "license": "MIT",
22
+ "author": "chandq",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/chandq/pic-compressor.git"
26
+ },
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "type": "module",
31
+ "sideEffects": false,
32
+ "main": "./dist/index.cjs",
33
+ "module": "./dist/index.mjs",
34
+ "browser": "./dist/index.umd.js",
35
+ "types": "./dist/index.d.mts",
36
+ "unpkg": "./dist/index.umd.js",
37
+ "jsdelivr": "./dist/index.umd.js",
38
+ "exports": {
39
+ ".": {
40
+ "import": {
41
+ "types": "./dist/index.d.mts",
42
+ "default": "./dist/index.mjs"
43
+ },
44
+ "require": {
45
+ "types": "./dist/index.d.cts",
46
+ "default": "./dist/index.cjs"
47
+ },
48
+ "default": "./dist/index.mjs"
49
+ },
50
+ "./umd": "./dist/index.umd.js",
51
+ "./package.json": "./package.json"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public",
55
+ "registry": "https://registry.npmjs.org/"
56
+ },
57
+ "scripts": {
58
+ "build": "tsdown",
59
+ "dev": "tsdown --watch",
60
+ "typecheck": "tsc --noEmit",
61
+ "lint": "oxlint .",
62
+ "format": "oxfmt --write .",
63
+ "format:check": "oxfmt --check .",
64
+ "test": "vitest run",
65
+ "test:watch": "vitest",
66
+ "test:coverage": "vitest run --coverage",
67
+ "verify": "npm run lint && npm run format:check && npm run typecheck && npm run test && npm run build && npm run publint",
68
+ "publint": "publint --pack npm",
69
+ "prepublishOnly": "npm run verify"
70
+ },
71
+ "devDependencies": {
72
+ "@types/node": "^26.0.0",
73
+ "@vitest/coverage-v8": "^5.0.1",
74
+ "jsdom": "^30.1.0",
75
+ "oxfmt": "^0.68.0",
76
+ "oxlint": "^1.83.0",
77
+ "publint": "^0.3.15",
78
+ "tsdown": "^0.23.0",
79
+ "typescript": "^7.0.2",
80
+ "vitest": "^5.0.1"
81
+ },
82
+ "engines": {
83
+ "node": ">=16"
84
+ }
85
+ }