Skip to content

Commit 7223af2

Browse files
arturiaduh95
andauthored
@uppy/compressor: Add image compressor plugin (#3471)
* Add Compressor plugin * Set type in blob if it’s missing * clear thumbnail-generator queue when cancel-all is called * Add e2e test for @uppy/compressor * Docs, types, readme, bundle, add event * Update yarn.lock * fix test * Update e2e/cypress/integration/dashboard-compressor.spec.ts Co-authored-by: Antoine du Hamel <[email protected]> * Update dashboard-compressor.spec.ts * convert compressor to ESM * Update e2e/clients/dashboard-compressor/index.html Co-authored-by: Antoine du Hamel <[email protected]> * remove console.log * uglierBytes Co-authored-by: Antoine du Hamel <[email protected]>
1 parent 8b8e6df commit 7223af2

File tree

17 files changed

+455
-3
lines changed

17 files changed

+455
-3
lines changed
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Uppy from '@uppy/core'
2+
import Dashboard from '@uppy/dashboard'
3+
import Compressor from '@uppy/compressor'
4+
5+
import '@uppy/core/dist/style.css'
6+
import '@uppy/dashboard/dist/style.css'
7+
8+
const uppy = new Uppy()
9+
.use(Dashboard, {
10+
target: document.body,
11+
inline: true,
12+
})
13+
.use(Compressor)
14+
15+
// Keep this here to access uppy in tests
16+
window.uppy = uppy
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8"/>
5+
<title>dashboard-compressor</title>
6+
<script defer type="module" src="app.js"></script>
7+
</head>
8+
<body>
9+
<div id="app"></div>
10+
</body>
11+
</html>

e2e/clients/index.html

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ <h1>Test apps</h1>
1313
<li><a href="dashboard-ui/index.html">dashboard-ui</a></li>
1414
<li><a href="dashboard-react/index.html">dashboard-react</a></li>
1515
<li><a href="dashboard-vue/index.html">dashboard-vue</a></li>
16+
<li><a href="dashboard-compressor/index.html">dashboard-compressor</a></li>
1617
</ul>
1718
<nav>
1819
</body>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
function uglierBytes (text) {
2+
const KB = 2 ** 10
3+
const MB = KB * KB
4+
5+
if (text.endsWith(' KB')) {
6+
return Number(text.slice(0, -3)) * KB
7+
}
8+
9+
if (text.endsWith(' MB')) {
10+
return Number(text.slice(0, -3)) * MB
11+
}
12+
13+
if (text.endsWith(' B')) {
14+
return Number(text.slice(0, -2))
15+
}
16+
17+
throw new Error(`Not what the computer thinks a human-readable size string look like: ${text}`)
18+
}
19+
20+
describe('dashboard-compressor', () => {
21+
beforeEach(() => {
22+
cy.visit('/dashboard-compressor')
23+
cy.get('.uppy-Dashboard-input').as('file-input')
24+
})
25+
26+
it('should compress images', () => {
27+
const sizeBeforeCompression = []
28+
29+
cy.get('@file-input').attachFile(['images/cat.jpg', 'images/traffic.jpg'])
30+
31+
cy.get('.uppy-Dashboard-Item-statusSize').each((element) => {
32+
const text = element.text()
33+
sizeBeforeCompression.push(uglierBytes(text))
34+
})
35+
36+
cy.get('.uppy-StatusBar-actionBtn--upload').click()
37+
38+
cy.get('.uppy-Informer p[role="alert"]', {
39+
timeout: 10000,
40+
}).should('be.visible')
41+
42+
cy.get('.uppy-Dashboard-Item-statusSize').should((elements) => {
43+
expect(elements).to.have.length(sizeBeforeCompression.length)
44+
45+
for (let i = 0; i < elements.length; i++) {
46+
expect(sizeBeforeCompression[i]).to.be.greaterThan(
47+
uglierBytes(elements[i].textContent),
48+
)
49+
}
50+
})
51+
})
52+
})

packages/@uppy/compressor/LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2022 Transloadit
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

packages/@uppy/compressor/README.md

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# @uppy/compressor
2+
3+
<img src="https://uppy.io/images/logos/uppy-dog-head-arrow.svg" width="120" alt="Uppy logo: a superman puppy in a pink suit" align="right">
4+
5+
<a href="https://www.npmjs.com/package/@uppy/compressor"><img src="https://img.shields.io/npm/v/@uppy/compressor.svg?style=flat-square"></a> <img src="https://github.com/transloadit/uppy/workflows/Tests/badge.svg" alt="CI status for Uppy tests"> <img src="https://github.com/transloadit/uppy/workflows/Companion/badge.svg" alt="CI status for Companion tests"> <img src="https://github.com/transloadit/uppy/workflows/End-to-end%20tests/badge.svg" alt="CI status for browser tests">
6+
7+
The Compressor plugin for Uppy optimizes images (JPEG, PNG, WEBP), saving on average up to 60% in size (roughly 18 MB for 10 images). It uses [Compressor.js](https://github.com/fengyuanchen/compressorjs).
8+
9+
Uppy is being developed by the folks at [Transloadit](https://transloadit.com), a versatile file encoding service.
10+
11+
## Example
12+
13+
```js
14+
import Uppy from '@uppy/core'
15+
import Compressor from '@uppy/compressor'
16+
17+
const uppy = new Uppy()
18+
uppy.use(Compressor)
19+
```
20+
21+
## Installation
22+
23+
```bash
24+
npm install @uppy/compressor
25+
```
26+
27+
We recommend installing from yarn or npm, and then using a module bundler such as [Parcel](https://parceljs.org/), [Vite](https://vitejs.dev/) or [Webpack](https://webpack.js.org/).
28+
29+
Alternatively, you can also use this plugin in a pre-built bundle from Transloadit’s CDN: Edgly. In that case `Uppy` will attach itself to the global `window.Uppy` object. See the [main Uppy documentation](https://uppy.io/docs/#Installation) for instructions.
30+
31+
## Documentation
32+
33+
Documentation for this plugin can be found on the [Uppy website](https://uppy.io/docs/compressor).
34+
35+
## License
36+
37+
[The MIT License](./LICENSE).
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "@uppy/compressor",
3+
"description": "Uppy plugin that compresses images before upload, saving up to 60% in size",
4+
"version": "0.2.1",
5+
"license": "MIT",
6+
"main": "lib/index.js",
7+
"style": "dist/style.min.css",
8+
"types": "types/index.d.ts",
9+
"keywords": [
10+
"file uploader",
11+
"uppy",
12+
"uppy-plugin",
13+
"compress",
14+
"image compression"
15+
],
16+
"type": "module",
17+
"homepage": "https://uppy.io",
18+
"bugs": {
19+
"url": "https://github.com/transloadit/uppy/issues"
20+
},
21+
"repository": {
22+
"type": "git",
23+
"url": "git+https://github.com/transloadit/uppy.git"
24+
},
25+
"dependencies": {
26+
"@transloadit/prettier-bytes": "^0.0.9",
27+
"@uppy/utils": "workspace:^",
28+
"compressorjs": "^1.1.1",
29+
"preact": "^10.5.13",
30+
"promise-queue": "^2.2.5"
31+
},
32+
"peerDependencies": {
33+
"@uppy/core": "workspace:^"
34+
},
35+
"publishConfig": {
36+
"access": "public"
37+
}
38+
}
+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { BasePlugin } from '@uppy/core'
2+
import { RateLimitedQueue } from '@uppy/utils/lib/RateLimitedQueue'
3+
import prettierBytes from '@transloadit/prettier-bytes'
4+
import CompressorJS from 'compressorjs/dist/compressor.common.js'
5+
import locale from './locale.js'
6+
7+
export default class Compressor extends BasePlugin {
8+
#RateLimitedQueue
9+
10+
constructor (uppy, opts) {
11+
super(uppy, opts)
12+
this.id = this.opts.id || 'Compressor'
13+
this.type = 'modifier'
14+
15+
this.defaultLocale = locale
16+
17+
const defaultOptions = {
18+
quality: 0.6,
19+
limit: 10,
20+
}
21+
22+
this.opts = { ...defaultOptions, ...opts }
23+
24+
this.#RateLimitedQueue = new RateLimitedQueue(this.opts.limit)
25+
26+
this.i18nInit()
27+
28+
this.prepareUpload = this.prepareUpload.bind(this)
29+
this.compress = this.compress.bind(this)
30+
}
31+
32+
compress (blob) {
33+
return new Promise((resolve, reject) => {
34+
/* eslint-disable no-new */
35+
new CompressorJS(blob, {
36+
...this.opts,
37+
success: resolve,
38+
error: reject,
39+
})
40+
})
41+
}
42+
43+
async prepareUpload (fileIDs) {
44+
let totalCompressedSize = 0
45+
const compressAndApplyResult = this.#RateLimitedQueue.wrapPromiseFunction(
46+
async (file) => {
47+
try {
48+
const compressedBlob = await this.compress(file.data)
49+
this.uppy.log(`[Image Compressor] Image ${file.id} size before/after compression: ${file.data.size} / ${compressedBlob.size}`)
50+
totalCompressedSize += compressedBlob.size
51+
this.uppy.setFileState(file.id, {
52+
data: compressedBlob,
53+
size: compressedBlob.size,
54+
})
55+
} catch (err) {
56+
this.uppy.log(`[Image Compressor] Failed to compress ${file.id}:`, 'warning')
57+
this.uppy.log(err, 'warning')
58+
}
59+
},
60+
)
61+
62+
const promises = fileIDs.map((fileID) => {
63+
const file = this.uppy.getFile(fileID)
64+
this.uppy.emit('preprocess-progress', file, {
65+
mode: 'indeterminate',
66+
message: this.i18n('compressingImages'),
67+
})
68+
69+
// Some browsers (Firefox) add blobs with empty file type, when files are
70+
// added from a folder. Uppy auto-detects type from extension, but leaves the original blob intact.
71+
// However, Compressor.js failes when file has no type, so we set it here
72+
if (!file.data.type) {
73+
file.data = file.data.slice(0, file.data.size, file.type)
74+
}
75+
76+
if (!file.type.startsWith('image/')) {
77+
return Promise.resolve()
78+
}
79+
80+
return compressAndApplyResult(file)
81+
})
82+
83+
// Why emit `preprocess-complete` for all files at once, instead of
84+
// above when each is processed?
85+
// Because it leads to StatusBar showing a weird “upload 6 files” button,
86+
// while waiting for all the files to complete pre-processing.
87+
await Promise.all(promises)
88+
89+
this.uppy.emit('compressor:complete')
90+
91+
// Only show informer if Compressor mananged to save at least a kilobyte
92+
if (totalCompressedSize > 1024) {
93+
this.uppy.info(
94+
this.i18n('compressedX', {
95+
size: prettierBytes(totalCompressedSize),
96+
}),
97+
'info',
98+
)
99+
}
100+
101+
for (const fileID of fileIDs) {
102+
const file = this.uppy.getFile(fileID)
103+
this.uppy.emit('preprocess-complete', file)
104+
}
105+
}
106+
107+
install () {
108+
this.uppy.addPreProcessor(this.prepareUpload)
109+
}
110+
111+
uninstall () {
112+
this.uppy.removePreProcessor(this.prepareUpload)
113+
}
114+
}
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default {
2+
strings: {
3+
// Shown in the Status Bar
4+
compressingImages: 'Compressing images...',
5+
compressedX: 'Saved %{size} by compressing images',
6+
},
7+
}
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { PluginOptions, BasePlugin } from '@uppy/core'
2+
import type CompressorLocale from './generatedLocale'
3+
4+
export interface CompressorOptions extends PluginOptions {
5+
quality?: number
6+
limit?: number
7+
locale?: CompressorLocale
8+
}
9+
10+
declare class Compressor extends BasePlugin<CompressorOptions> {}
11+
12+
export default Compressor
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import Uppy from '@uppy/core'
2+
import Compressor from '..'
3+
4+
{
5+
const uppy = new Uppy()
6+
uppy.use(Compressor)
7+
}

packages/@uppy/thumbnail-generator/src/index.js

+8
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,10 @@ module.exports = class ThumbnailGenerator extends UIPlugin {
329329
})
330330
}
331331

332+
onAllFilesRemoved = () => {
333+
this.queue = []
334+
}
335+
332336
waitUntilAllProcessed = (fileIDs) => {
333337
fileIDs.forEach((fileID) => {
334338
const file = this.uppy.getFile(fileID)
@@ -360,6 +364,8 @@ module.exports = class ThumbnailGenerator extends UIPlugin {
360364

361365
install () {
362366
this.uppy.on('file-removed', this.onFileRemoved)
367+
this.uppy.on('cancel-all', this.onAllFilesRemoved)
368+
363369
if (this.opts.lazy) {
364370
this.uppy.on('thumbnail:request', this.onFileAdded)
365371
this.uppy.on('thumbnail:cancel', this.onCancelRequest)
@@ -375,6 +381,8 @@ module.exports = class ThumbnailGenerator extends UIPlugin {
375381

376382
uninstall () {
377383
this.uppy.off('file-removed', this.onFileRemoved)
384+
this.uppy.off('cancel-all', this.onAllFilesRemoved)
385+
378386
if (this.opts.lazy) {
379387
this.uppy.off('thumbnail:request', this.onFileAdded)
380388
this.uppy.off('thumbnail:cancel', this.onCancelRequest)

packages/@uppy/thumbnail-generator/src/index.test.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
5151
plugin.addToQueue = jest.fn()
5252
plugin.install()
5353

54-
expect(core.on).toHaveBeenCalledTimes(3)
54+
expect(core.on).toHaveBeenCalledTimes(4)
5555
expect(core.on).toHaveBeenCalledWith('file-added', plugin.onFileAdded)
5656
})
5757
})
@@ -67,11 +67,11 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
6767
plugin.addToQueue = jest.fn()
6868
plugin.install()
6969

70-
expect(core.on).toHaveBeenCalledTimes(3)
70+
expect(core.on).toHaveBeenCalledTimes(4)
7171

7272
plugin.uninstall()
7373

74-
expect(core.off).toHaveBeenCalledTimes(3)
74+
expect(core.off).toHaveBeenCalledTimes(4)
7575
expect(core.off).toHaveBeenCalledWith('file-added', plugin.onFileAdded)
7676
})
7777
})

packages/uppy/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,6 @@ exports.Form = require('@uppy/form')
4949
exports.GoldenRetriever = require('@uppy/golden-retriever')
5050
exports.ReduxDevTools = require('@uppy/redux-dev-tools')
5151
exports.ThumbnailGenerator = require('@uppy/thumbnail-generator')
52+
exports.Compressor = require('@uppy/compressor')
5253

5354
exports.locales = {}

packages/uppy/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"@uppy/aws-s3-multipart": "workspace:^",
3737
"@uppy/box": "workspace:^",
3838
"@uppy/companion-client": "workspace:^",
39+
"@uppy/compressor": "workspace:^",
3940
"@uppy/core": "workspace:^",
4041
"@uppy/dashboard": "workspace:^",
4142
"@uppy/drag-drop": "workspace:^",

0 commit comments

Comments
 (0)