index.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import { BasePlugin, Uppy } from '@uppy/core'
  2. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  3. // @ts-ignore
  4. import { RateLimitedQueue } from '@uppy/utils/lib/RateLimitedQueue'
  5. import getFileNameAndExtension from '@uppy/utils/lib/getFileNameAndExtension'
  6. import prettierBytes from '@transloadit/prettier-bytes'
  7. import CompressorJS from 'compressorjs'
  8. import type { Body, Meta, UppyFile } from '@uppy/utils/lib/UppyFile'
  9. import type { PluginOpts } from '@uppy/core/lib/BasePlugin'
  10. import locale from './locale.ts'
  11. declare module '@uppy/core' {
  12. export interface UppyEventMap<M extends Meta, B extends Body> {
  13. 'compressor:complete': (file: UppyFile<M, B>[]) => void
  14. }
  15. }
  16. export interface CompressorOpts extends PluginOpts, CompressorJS.Options {
  17. quality: number
  18. limit?: number
  19. }
  20. export default class Compressor<
  21. M extends Meta,
  22. B extends Body,
  23. > extends BasePlugin<CompressorOpts, M, B> {
  24. #RateLimitedQueue
  25. constructor(uppy: Uppy<M, B>, opts: CompressorOpts) {
  26. super(uppy, opts)
  27. this.id = this.opts.id || 'Compressor'
  28. this.type = 'modifier'
  29. this.defaultLocale = locale
  30. const defaultOptions = {
  31. quality: 0.6,
  32. limit: 10,
  33. }
  34. this.opts = { ...defaultOptions, ...opts }
  35. this.#RateLimitedQueue = new RateLimitedQueue(this.opts.limit)
  36. this.i18nInit()
  37. this.prepareUpload = this.prepareUpload.bind(this)
  38. this.compress = this.compress.bind(this)
  39. }
  40. compress(blob: Blob): Promise<Blob | File> {
  41. return new Promise((resolve, reject) => {
  42. /* eslint-disable no-new */
  43. new CompressorJS(blob, {
  44. ...this.opts,
  45. success: resolve,
  46. error: reject,
  47. })
  48. })
  49. }
  50. async prepareUpload(fileIDs: string[]): Promise<void> {
  51. let totalCompressedSize = 0
  52. const compressedFiles: UppyFile<M, B>[] = []
  53. const compressAndApplyResult = this.#RateLimitedQueue.wrapPromiseFunction(
  54. async (file: UppyFile<M, B>) => {
  55. try {
  56. const compressedBlob = await this.compress(file.data)
  57. const compressedSavingsSize = file.data.size - compressedBlob.size
  58. this.uppy.log(
  59. `[Image Compressor] Image ${file.id} compressed by ${prettierBytes(compressedSavingsSize)}`,
  60. )
  61. totalCompressedSize += compressedSavingsSize
  62. const { name, type, size } = compressedBlob as File
  63. const compressedFileName = getFileNameAndExtension(name)
  64. const metaFileName = getFileNameAndExtension(file.meta.name)
  65. // Name (file.meta.name) might have been changed by user, so we update only the extension
  66. const newMetaName = `${metaFileName.name}.${compressedFileName.extension}`
  67. this.uppy.setFileState(file.id, {
  68. ...(name && { name }),
  69. ...(compressedFileName.extension && {
  70. extension: compressedFileName.extension,
  71. }),
  72. ...(type && { type }),
  73. ...(size && { size }),
  74. data: compressedBlob,
  75. meta: {
  76. ...file.meta,
  77. type,
  78. name: newMetaName,
  79. },
  80. })
  81. compressedFiles.push(file)
  82. } catch (err) {
  83. this.uppy.log(
  84. `[Image Compressor] Failed to compress ${file.id}:`,
  85. 'warning',
  86. )
  87. this.uppy.log(err, 'warning')
  88. }
  89. },
  90. )
  91. const promises = fileIDs.map((fileID) => {
  92. const file = this.uppy.getFile(fileID)
  93. this.uppy.emit('preprocess-progress', file, {
  94. mode: 'indeterminate',
  95. message: this.i18n('compressingImages'),
  96. })
  97. if (file.isRemote) {
  98. return Promise.resolve()
  99. }
  100. // Some browsers (Firefox) add blobs with empty file type, when files are
  101. // added from a folder. Uppy auto-detects type from extension, but leaves the original blob intact.
  102. // However, Compressor.js failes when file has no type, so we set it here
  103. if (!file.data.type) {
  104. file.data = file.data.slice(0, file.data.size, file.type)
  105. }
  106. if (!file.type?.startsWith('image/')) {
  107. return Promise.resolve()
  108. }
  109. return compressAndApplyResult(file)
  110. })
  111. // Why emit `preprocess-complete` for all files at once, instead of
  112. // above when each is processed?
  113. // Because it leads to StatusBar showing a weird “upload 6 files” button,
  114. // while waiting for all the files to complete pre-processing.
  115. await Promise.all(promises)
  116. this.uppy.emit('compressor:complete', compressedFiles)
  117. // Only show informer if Compressor mananged to save at least a kilobyte
  118. if (totalCompressedSize > 1024) {
  119. this.uppy.info(
  120. this.i18n('compressedX', {
  121. size: prettierBytes(totalCompressedSize),
  122. }),
  123. 'info',
  124. )
  125. }
  126. for (const fileID of fileIDs) {
  127. const file = this.uppy.getFile(fileID)
  128. this.uppy.emit('preprocess-complete', file)
  129. }
  130. }
  131. install(): void {
  132. this.uppy.addPreProcessor(this.prepareUpload)
  133. }
  134. uninstall(): void {
  135. this.uppy.removePreProcessor(this.prepareUpload)
  136. }
  137. }