import { BasePlugin, Uppy } from '@uppy/core' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import { RateLimitedQueue } from '@uppy/utils/lib/RateLimitedQueue' import getFileNameAndExtension from '@uppy/utils/lib/getFileNameAndExtension' import prettierBytes from '@transloadit/prettier-bytes' import CompressorJS from 'compressorjs' import type { Body, Meta, UppyFile } from '@uppy/utils/lib/UppyFile' import type { PluginOpts } from '@uppy/core/lib/BasePlugin' import locale from './locale.ts' declare module '@uppy/core' { export interface UppyEventMap { 'compressor:complete': (file: UppyFile[]) => void } } export interface CompressorOpts extends PluginOpts, CompressorJS.Options { quality: number limit?: number } export default class Compressor< M extends Meta, B extends Body, > extends BasePlugin { #RateLimitedQueue constructor(uppy: Uppy, opts: CompressorOpts) { super(uppy, opts) this.id = this.opts.id || 'Compressor' this.type = 'modifier' this.defaultLocale = locale const defaultOptions = { quality: 0.6, limit: 10, } this.opts = { ...defaultOptions, ...opts } this.#RateLimitedQueue = new RateLimitedQueue(this.opts.limit) this.i18nInit() this.prepareUpload = this.prepareUpload.bind(this) this.compress = this.compress.bind(this) } compress(blob: Blob): Promise { return new Promise((resolve, reject) => { /* eslint-disable no-new */ new CompressorJS(blob, { ...this.opts, success: resolve, error: reject, }) }) } async prepareUpload(fileIDs: string[]): Promise { let totalCompressedSize = 0 const compressedFiles: UppyFile[] = [] const compressAndApplyResult = this.#RateLimitedQueue.wrapPromiseFunction( async (file: UppyFile) => { try { const compressedBlob = await this.compress(file.data) const compressedSavingsSize = file.data.size - compressedBlob.size this.uppy.log( `[Image Compressor] Image ${file.id} compressed by ${prettierBytes(compressedSavingsSize)}`, ) totalCompressedSize += compressedSavingsSize const { name, type, size } = compressedBlob as File const compressedFileName = getFileNameAndExtension(name) const metaFileName = getFileNameAndExtension(file.meta.name) // Name (file.meta.name) might have been changed by user, so we update only the extension const newMetaName = `${metaFileName.name}.${compressedFileName.extension}` this.uppy.setFileState(file.id, { ...(name && { name }), ...(compressedFileName.extension && { extension: compressedFileName.extension, }), ...(type && { type }), ...(size && { size }), data: compressedBlob, meta: { ...file.meta, type, name: newMetaName, }, }) compressedFiles.push(file) } catch (err) { this.uppy.log( `[Image Compressor] Failed to compress ${file.id}:`, 'warning', ) this.uppy.log(err, 'warning') } }, ) const promises = fileIDs.map((fileID) => { const file = this.uppy.getFile(fileID) this.uppy.emit('preprocess-progress', file, { mode: 'indeterminate', message: this.i18n('compressingImages'), }) if (file.isRemote) { return Promise.resolve() } // Some browsers (Firefox) add blobs with empty file type, when files are // added from a folder. Uppy auto-detects type from extension, but leaves the original blob intact. // However, Compressor.js failes when file has no type, so we set it here if (!file.data.type) { file.data = file.data.slice(0, file.data.size, file.type) } if (!file.type?.startsWith('image/')) { return Promise.resolve() } return compressAndApplyResult(file) }) // Why emit `preprocess-complete` for all files at once, instead of // above when each is processed? // Because it leads to StatusBar showing a weird “upload 6 files” button, // while waiting for all the files to complete pre-processing. await Promise.all(promises) this.uppy.emit('compressor:complete', compressedFiles) // Only show informer if Compressor mananged to save at least a kilobyte if (totalCompressedSize > 1024) { this.uppy.info( this.i18n('compressedX', { size: prettierBytes(totalCompressedSize), }), 'info', ) } for (const fileID of fileIDs) { const file = this.uppy.getFile(fileID) this.uppy.emit('preprocess-complete', file) } } install(): void { this.uppy.addPreProcessor(this.prepareUpload) } uninstall(): void { this.uppy.removePreProcessor(this.prepareUpload) } }