├── .browserslistrc
├── .gitignore
├── .gitattributes
├── .husky
├── pre-commit
└── commit-msg
├── docs
├── images
│ ├── picture.jpg
│ └── picture.png
├── css
│ └── main.css
├── examples
│ ├── grayscale.html
│ ├── watermark.html
│ └── work-with-promise.html
├── js
│ └── main.js
└── index.html
├── commitlint.config.js
├── lint-staged.config.js
├── src
├── constants.js
├── defaults.js
├── utilities.js
└── index.js
├── .editorconfig
├── .stylelintrc
├── test
├── .eslintrc
├── helpers.js
├── specs
│ ├── methods
│ │ ├── noConflict.spec.js
│ │ ├── setDefaults.spec.js
│ │ └── abort.spec.js
│ ├── options
│ │ ├── checkOrientation.spec.js
│ │ ├── mimeType.spec.js
│ │ ├── success.spec.js
│ │ ├── drew.spec.js
│ │ ├── error.spec.js
│ │ ├── beforeDraw.spec.js
│ │ ├── convertSize.spec.js
│ │ ├── convertTypes.spec.js
│ │ ├── retainExif.spec.js
│ │ ├── quality.spec.js
│ │ ├── maxWidth.spec.js
│ │ ├── maxHeight.spec.js
│ │ ├── minWidth.spec.js
│ │ ├── minHeight.spec.js
│ │ ├── strict.spec.js
│ │ ├── width.spec.js
│ │ ├── height.spec.js
│ │ └── resize.spec.js
│ └── Compressor.spec.js
└── karma.conf.js
├── .eslintrc
├── .github
├── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── feature_request.md
│ └── bug_report.yml
├── workflows
│ └── ci.yml
├── CODE_OF_CONDUCT.md
├── PULL_REQUEST_TEMPLATE.md
└── CONTRIBUTING.md
├── babel.config.js
├── types
└── index.d.ts
├── LICENSE
├── rollup.config.js
├── CHANGELOG.md
├── package.json
├── dist
├── compressor.min.js
└── compressor.esm.js
└── README.md
/.browserslistrc:
--------------------------------------------------------------------------------
1 | defaults
2 | ie >= 10
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.map
2 | coverage
3 | node_modules
4 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
--------------------------------------------------------------------------------
/docs/images/picture.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fengyuanchen/compressorjs/HEAD/docs/images/picture.jpg
--------------------------------------------------------------------------------
/docs/images/picture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fengyuanchen/compressorjs/HEAD/docs/images/picture.png
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx --no-install commitlint --edit $1
5 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | '@commitlint/config-conventional',
4 | ],
5 | };
6 |
--------------------------------------------------------------------------------
/lint-staged.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | '{src,test}/**/*.js|*.conf*.js': 'eslint --fix',
3 | '{src,docs}/**/*.css': 'stylelint --fix',
4 | };
5 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | export const IS_BROWSER = typeof window !== 'undefined' && typeof window.document !== 'undefined';
2 | export const WINDOW = IS_BROWSER ? window : {};
3 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 2
7 | indent_style = space
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "stylelint-config-standard",
3 | "plugins": [
4 | "stylelint-order"
5 | ],
6 | "rules": {
7 | "no-descending-specificity": null,
8 | "order/properties-alphabetical-order": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | },
5 | "globals": {
6 | "Compressor": true,
7 | "expect": true
8 | },
9 | "rules": {
10 | "no-new": "off",
11 | "no-unused-expressions": "off"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb-base",
3 | "env": {
4 | "browser": true
5 | },
6 | "root": true,
7 | "rules": {
8 | "no-param-reassign": "off",
9 | "valid-jsdoc": ["error", {
10 | "requireReturn": false
11 | }]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: Questions & Discussions
4 | url: https://github.com/fengyuanchen/compressorjs/discussions
5 | about: Use GitHub discussions for message-board style questions and discussions.
6 |
--------------------------------------------------------------------------------
/test/helpers.js:
--------------------------------------------------------------------------------
1 | window.loadImageAsBlob = (url, done) => {
2 | const xhr = new XMLHttpRequest();
3 |
4 | xhr.onload = () => {
5 | const blob = xhr.response;
6 |
7 | blob.name = url.replace(/^.*?(\w+\.\w+)$/, '$1');
8 | done(blob);
9 | };
10 | xhr.open('GET', url);
11 | xhr.responseType = 'blob';
12 | xhr.send();
13 | };
14 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | [
4 | '@babel/preset-env',
5 | {
6 | modules: false,
7 | },
8 | ],
9 | ],
10 | plugins: [
11 | '@babel/plugin-transform-object-assign',
12 | ],
13 | env: {
14 | test: {
15 | plugins: [
16 | 'istanbul',
17 | ],
18 | },
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/test/specs/methods/noConflict.spec.js:
--------------------------------------------------------------------------------
1 | describe('noConflict', () => {
2 | it('should be a static method', () => {
3 | expect(Compressor.noConflict).to.be.a('function');
4 | });
5 |
6 | it('should return the Compressor class itself', () => {
7 | const { Compressor } = window;
8 | const ImageCompressor = Compressor.noConflict();
9 |
10 | expect(ImageCompressor).to.equal(Compressor);
11 | expect(window.Compressor).to.be.undefined;
12 |
13 | // Reverts it for the rest test suites
14 | window.Compressor = ImageCompressor;
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v4
15 | - uses: actions/setup-node@v4
16 | with:
17 | node-version: 18
18 | - run: npm install
19 | - run: npm run lint
20 | - run: npm run build
21 | - run: npm test
22 | - uses: codecov/codecov-action@v5
23 | env:
24 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
25 |
--------------------------------------------------------------------------------
/test/specs/options/checkOrientation.spec.js:
--------------------------------------------------------------------------------
1 | describe('checkOrientation', () => {
2 | it('should check orientation by default', (done) => {
3 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
4 | const compressor = new Compressor(image, {
5 | success(result) {
6 | const newImage = new Image();
7 |
8 | newImage.onload = () => {
9 | expect(newImage.naturalWidth).to.below(newImage.naturalHeight);
10 | done();
11 | };
12 | newImage.src = URL.createObjectURL(result);
13 | },
14 | });
15 |
16 | expect(compressor.options.checkOrientation).to.be.true;
17 | });
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/test/specs/methods/setDefaults.spec.js:
--------------------------------------------------------------------------------
1 | describe('setDefaults', () => {
2 | it('should be a static method', () => {
3 | expect(Compressor.setDefaults).to.be.a('function');
4 | });
5 |
6 | it('should change the global default options', (done) => {
7 | Compressor.setDefaults({
8 | strict: false,
9 | });
10 |
11 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
12 | new Compressor(image, {
13 | quality: 1,
14 | success(result) {
15 | expect(result).to.not.equal(image);
16 |
17 | // Reverts it for the rest test suites
18 | Compressor.setDefaults({
19 | strict: true,
20 | });
21 | done();
22 | },
23 | });
24 | });
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/test/specs/options/mimeType.spec.js:
--------------------------------------------------------------------------------
1 | describe('mimeType', () => {
2 | it('should be `auto` by default', (done) => {
3 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
4 | const compressor = new Compressor(image);
5 |
6 | expect(compressor.options.mimeType).to.equal('auto');
7 | done();
8 | });
9 | });
10 |
11 | it('should match the given mime type', (done) => {
12 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
13 | image.name = 'picture.jpg';
14 |
15 | const mimeType = 'image/webp';
16 | const compressor = new Compressor(image, {
17 | mimeType,
18 | success(result) {
19 | expect(result.type).to.equal(mimeType);
20 | done();
21 | },
22 | });
23 |
24 | expect(compressor.options.mimeType).to.equal(mimeType);
25 | });
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/test/specs/options/success.spec.js:
--------------------------------------------------------------------------------
1 | describe('success', () => {
2 | it('should be `null` be default', (done) => {
3 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
4 | const compressor = new Compressor(image);
5 |
6 | expect(compressor.options.success).to.be.null;
7 | done();
8 | });
9 | });
10 |
11 | it('should execute the `success` hook function', (done) => {
12 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
13 | const compressor = new Compressor(image, {
14 | success(result) {
15 | expect(this).to.equal(compressor);
16 | expect(result).to.be.an.instanceOf(Blob);
17 | expect(result.name).to.equal(image.name);
18 | done();
19 | },
20 | });
21 |
22 | expect(compressor.options.success).to.be.a('function');
23 | });
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/test/specs/options/drew.spec.js:
--------------------------------------------------------------------------------
1 | describe('drew', () => {
2 | it('should be `null` be default', (done) => {
3 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
4 | const compressor = new Compressor(image);
5 |
6 | expect(compressor.options.drew).to.be.null;
7 | done();
8 | });
9 | });
10 |
11 | it('should execute the `drew` hook function', (done) => {
12 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
13 | const compressor = new Compressor(image, {
14 | drew(context, canvas) {
15 | expect(this).to.equal(compressor);
16 | expect(context).to.be.an.instanceOf(CanvasRenderingContext2D);
17 | expect(canvas).to.be.an.instanceOf(HTMLCanvasElement);
18 | done();
19 | },
20 | });
21 |
22 | expect(compressor.options.drew).to.be.a('function');
23 | });
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/test/specs/options/error.spec.js:
--------------------------------------------------------------------------------
1 | describe('error', () => {
2 | it('should be `null` be default', (done) => {
3 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
4 | const compressor = new Compressor(image);
5 |
6 | expect(compressor.options.error).to.be.null;
7 | done();
8 | });
9 | });
10 |
11 | it('should execute the `error` hook function', (done) => {
12 | const compressor = new Compressor(null, {
13 | error(error) {
14 | expect(error).to.be.an.instanceOf(Error);
15 | setTimeout(() => {
16 | expect(this).to.equal(compressor);
17 | done();
18 | });
19 | },
20 | });
21 |
22 | expect(compressor.options.error).to.be.a('function');
23 | });
24 |
25 | it('should throw error directly without a `error` hook function', () => {
26 | expect(() => {
27 | new Compressor(null);
28 | }).to.throw();
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/test/specs/options/beforeDraw.spec.js:
--------------------------------------------------------------------------------
1 | describe('beforeDraw', () => {
2 | it('should be `null` be default', (done) => {
3 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
4 | const compressor = new Compressor(image);
5 |
6 | expect(compressor.options.beforeDraw).to.be.null;
7 | done();
8 | });
9 | });
10 |
11 | it('should execute the `beforeDraw` hook function', (done) => {
12 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
13 | const compressor = new Compressor(image, {
14 | beforeDraw(context, canvas) {
15 | expect(this).to.equal(compressor);
16 | expect(context).to.be.an.instanceOf(CanvasRenderingContext2D);
17 | expect(canvas).to.be.an.instanceOf(HTMLCanvasElement);
18 | done();
19 | },
20 | });
21 |
22 | expect(compressor.options.beforeDraw).to.be.a('function');
23 | });
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/test/specs/options/convertSize.spec.js:
--------------------------------------------------------------------------------
1 | describe('convertSize', () => {
2 | it('should not convert the image from PNG to JPEG', (done) => {
3 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
4 | const compressor = new Compressor(image, {
5 | success(result) {
6 | expect(image.type).to.equal('image/png');
7 | expect(result.type).to.equal('image/png');
8 | done();
9 | },
10 | });
11 |
12 | expect(compressor.options.convertSize).to.equal(5000000);
13 | });
14 | });
15 |
16 | it('should convert the image from PNG to JPEG', (done) => {
17 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
18 | const compressor = new Compressor(image, {
19 | convertSize: 0,
20 | success(result) {
21 | expect(image.type).to.equal('image/png');
22 | expect(result.type).to.equal('image/jpeg');
23 | done();
24 | },
25 | });
26 |
27 | expect(compressor.options.convertSize).to.equal(0);
28 | });
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/types/index.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace Compressor {
2 | export interface Options {
3 | strict?: boolean;
4 | checkOrientation?: boolean;
5 | retainExif?: boolean;
6 | maxWidth?: number;
7 | maxHeight?: number;
8 | minWidth?: number;
9 | minHeight?: number;
10 | width?: number;
11 | height?: number;
12 | resize?: 'contain' | 'cover' | 'none';
13 | quality?: number;
14 | mimeType?: string;
15 | convertTypes?: string | string[];
16 | convertSize?: number;
17 | beforeDraw?(context: CanvasRenderingContext2D, canvas: HTMLCanvasElement): void;
18 | drew?(context: CanvasRenderingContext2D, canvas: HTMLCanvasElement): void;
19 | success?(file: File | Blob): void;
20 | error?(error: Error): void;
21 | }
22 | }
23 |
24 | declare class Compressor {
25 | constructor(file: File | Blob, options?: Compressor.Options);
26 | abort(): void;
27 | static noConflict(): Compressor;
28 | static setDefaults(options: Compressor.Options): void;
29 | }
30 |
31 | declare module 'compressorjs' {
32 | export default Compressor;
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright 2018-present Chen Fengyuan
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/test/specs/options/convertTypes.spec.js:
--------------------------------------------------------------------------------
1 | describe('convertTypes', () => {
2 | it('should convert the image from PNG to JPEG', (done) => {
3 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
4 | const compressor = new Compressor(image, {
5 | convertSize: 0,
6 | success(result) {
7 | expect(image.type).to.equal('image/png');
8 | expect(result.type).to.equal('image/jpeg');
9 | done();
10 | },
11 | });
12 |
13 | expect(compressor.options.convertTypes).to.deep.equal(['image/png']);
14 | });
15 | });
16 |
17 | it('should not convert the image from PNG to JPEG', (done) => {
18 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
19 | const compressor = new Compressor(image, {
20 | convertTypes: [],
21 | convertSize: 0,
22 | success(result) {
23 | expect(image.type).to.equal('image/png');
24 | expect(result.type).to.equal('image/png');
25 | done();
26 | },
27 | });
28 |
29 | expect(compressor.options.convertTypes).to.deep.equal([]);
30 | });
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/test/specs/options/retainExif.spec.js:
--------------------------------------------------------------------------------
1 | import { getExif } from '../../../src/utilities';
2 |
3 | function blobToArrayBuffer(blob, callback) {
4 | const reader = new FileReader();
5 |
6 | reader.onload = ({ target }) => {
7 | callback(target.result);
8 | };
9 | reader.readAsArrayBuffer(blob);
10 | }
11 |
12 | describe('retainExif', () => {
13 | it('should not retain the Exif information', (done) => {
14 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
15 | new Compressor(image, {
16 | success(result) {
17 | blobToArrayBuffer(result, (arrayBuffer) => {
18 | expect(getExif(arrayBuffer)).to.be.empty;
19 | done();
20 | });
21 | },
22 | });
23 | });
24 | });
25 |
26 | it('should retain the Exif information', (done) => {
27 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
28 | new Compressor(image, {
29 | retainExif: true,
30 | success(result) {
31 | blobToArrayBuffer(result, (arrayBuffer) => {
32 | expect(getExif(arrayBuffer)).not.to.be.empty;
33 | done();
34 | });
35 | },
36 | });
37 | });
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/test/karma.conf.js:
--------------------------------------------------------------------------------
1 | const puppeteer = require('puppeteer');
2 | const rollupConfig = require('../rollup.config');
3 |
4 | process.env.CHROME_BIN = puppeteer.executablePath();
5 |
6 | module.exports = (config) => {
7 | config.set({
8 | autoWatch: false,
9 | basePath: '..',
10 | browsers: ['ChromeHeadless'],
11 | client: {
12 | mocha: {
13 | timeout: 10000,
14 | },
15 | },
16 | coverageIstanbulReporter: {
17 | reports: ['html', 'lcovonly', 'text-summary'],
18 | },
19 | files: [
20 | 'src/index.js',
21 | 'test/helpers.js',
22 | 'test/specs/**/*.spec.js',
23 | {
24 | pattern: 'docs/images/*',
25 | included: false,
26 | },
27 | ],
28 | frameworks: ['mocha', 'chai'],
29 | preprocessors: {
30 | 'src/index.js': ['rollup'],
31 | 'test/helpers.js': ['rollup'],
32 | 'test/specs/**/*.spec.js': ['rollup'],
33 | },
34 | reporters: ['mocha', 'coverage-istanbul'],
35 | rollupPreprocessor: {
36 | plugins: rollupConfig.plugins,
37 | output: {
38 | format: 'iife',
39 | name: rollupConfig.output[0].name,
40 | sourcemap: 'inline',
41 | },
42 | },
43 | singleRun: true,
44 | });
45 | };
46 |
--------------------------------------------------------------------------------
/test/specs/options/quality.spec.js:
--------------------------------------------------------------------------------
1 | describe('quality', () => {
2 | it('should be `0.8` by default', (done) => {
3 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
4 | const compressor = new Compressor(image);
5 |
6 | expect(compressor.options.quality).to.equal(0.8);
7 | done();
8 | });
9 | });
10 |
11 | it('should change the size of the output image', (done) => {
12 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
13 | const compressor = new Compressor(image, {
14 | quality: 0.6,
15 | success(result) {
16 | expect(result.size).to.below(image.size);
17 |
18 | new Compressor(image, {
19 | quality: 0.4,
20 | success(result1) {
21 | expect(result1.size).to.below(result.size);
22 |
23 | new Compressor(image, {
24 | quality: 0.2,
25 | success(result2) {
26 | expect(result2.size).to.below(result1.size);
27 | done();
28 | },
29 | });
30 | },
31 | });
32 | },
33 | });
34 |
35 | expect(compressor.options.quality).to.equal(0.6);
36 | });
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Code of Conduct
2 |
3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4 |
5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of the level of experience, gender, gender identity, and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
6 |
7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
8 |
9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
10 |
11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
12 |
13 | This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at https://www.contributor-covenant.org/version/1/0/0/code-of-conduct.html
14 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | **Summary**
7 |
8 | **What kind of change does this PR introduce?** (check at least one)
9 |
10 | - [ ] Bugfix
11 | - [ ] Feature
12 | - [ ] Code style update
13 | - [ ] Refactor
14 | - [ ] Docs
15 | - [ ] Build-related changes
16 | - [ ] Other, please describe:
17 |
18 | If changing the UI of the default theme, please provide the **before/after** screenshot:
19 |
20 | **Does this PR introduce a breaking change?** (check one)
21 |
22 | - [ ] Yes
23 | - [ ] No
24 |
25 | If yes, please describe the impact and migration path for existing applications:
26 |
27 | **The PR fulfills these requirements:**
28 |
29 | - [ ] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number)
30 |
31 | You have tested in the following browsers: (Providing a detailed version will be better.)
32 |
33 | - [ ] Chrome
34 | - [ ] Firefox
35 | - [ ] Safari
36 | - [ ] Edge
37 | - [ ] IE
38 |
39 | If adding a **new feature**, the PR's description includes:
40 |
41 | - [ ] A convincing reason for adding this feature
42 | - [ ] Related documents have been updated
43 | - [ ] Related tests have been updated
44 |
45 | To avoid wasting your time, it's best to open a **feature request issue** first and wait for approval before working on it.
46 |
47 | **Other information:**
48 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | const { babel } = require('@rollup/plugin-babel');
2 | const changeCase = require('change-case');
3 | const commonjs = require('@rollup/plugin-commonjs');
4 | const createBanner = require('create-banner');
5 | const { nodeResolve } = require('@rollup/plugin-node-resolve');
6 | const replace = require('@rollup/plugin-replace');
7 | const pkg = require('./package.json');
8 |
9 | pkg.name = pkg.name.replace('js', '');
10 |
11 | const name = changeCase.pascalCase(pkg.name);
12 | const banner = createBanner({
13 | data: {
14 | name: `${name}.js`,
15 | year: '2018-present',
16 | },
17 | });
18 |
19 | module.exports = {
20 | input: 'src/index.js',
21 | output: [
22 | {
23 | banner,
24 | name,
25 | file: `dist/${pkg.name}.js`,
26 | format: 'umd',
27 | },
28 | {
29 | banner,
30 | file: `dist/${pkg.name}.common.js`,
31 | format: 'cjs',
32 | exports: 'auto',
33 | },
34 | {
35 | banner,
36 | file: `dist/${pkg.name}.esm.js`,
37 | format: 'esm',
38 | },
39 | {
40 | banner,
41 | name,
42 | file: `docs/js/${pkg.name}.js`,
43 | format: 'umd',
44 | },
45 | ],
46 | plugins: [
47 | nodeResolve(),
48 | commonjs(),
49 | babel({
50 | babelHelpers: 'bundled',
51 | }),
52 | replace({
53 | delimiters: ['', ''],
54 | exclude: ['node_modules/**'],
55 | preventAssignment: true,
56 | '(function (module) {': `(function (module) {
57 | if (typeof window === 'undefined') {
58 | return;
59 | }`,
60 | }),
61 | ],
62 | };
63 |
--------------------------------------------------------------------------------
/docs/css/main.css:
--------------------------------------------------------------------------------
1 | [v-cloak] {
2 | display: none;
3 | }
4 |
5 | .carbonads {
6 | border: 1px solid #ccc;
7 | border-radius: 0.25rem;
8 | font-size: 0.875rem;
9 | overflow: hidden;
10 | padding: 1rem;
11 | }
12 |
13 | .carbon-wrap {
14 | overflow: hidden;
15 | }
16 |
17 | .carbon-img {
18 | clear: left;
19 | display: block;
20 | float: left;
21 | }
22 |
23 | .carbon-text,
24 | .carbon-poweredby {
25 | display: block;
26 | margin-left: 140px;
27 | }
28 |
29 | .carbon-text,
30 | .carbon-text:hover,
31 | .carbon-text:focus {
32 | color: #fff;
33 | text-decoration: none;
34 | }
35 |
36 | .carbon-poweredby,
37 | .carbon-poweredby:hover,
38 | .carbon-poweredby:focus {
39 | color: #ddd;
40 | text-decoration: none;
41 | }
42 |
43 | @media (min-width: 768px) {
44 | .carbonads {
45 | float: right;
46 | margin-bottom: -1rem;
47 | margin-top: -1rem;
48 | max-width: 360px;
49 | }
50 | }
51 |
52 | .footer {
53 | font-size: 0.875rem;
54 | }
55 |
56 | .heart {
57 | color: #ddd;
58 | display: block;
59 | height: 2rem;
60 | line-height: 2rem;
61 | margin-bottom: 0;
62 | margin-top: 1rem;
63 | position: relative;
64 | text-align: center;
65 | width: 100%;
66 | }
67 |
68 | .heart:hover {
69 | color: #ff4136;
70 | }
71 |
72 | .heart::before {
73 | border-top: 1px solid #eee;
74 | content: " ";
75 | display: block;
76 | height: 0;
77 | left: 0;
78 | position: absolute;
79 | right: 0;
80 | top: 50%;
81 | }
82 |
83 | .heart::after {
84 | background-color: #fff;
85 | content: "♥";
86 | padding-left: 0.5rem;
87 | padding-right: 0.5rem;
88 | position: relative;
89 | z-index: 1;
90 | }
91 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 1.2.1 (Feb 28, 2023)
4 |
5 | - Fix incompatible syntax in the bundled files (#170).
6 |
7 | ## 1.2.0 (Feb 25, 2023)
8 |
9 | - Add a new option: `retainExif` (#159).
10 |
11 | ## 1.1.1 (Oct 5, 2021)
12 |
13 | - Fix loading error in Node.js (#137).
14 |
15 | ## 1.1.0 (Oct 1, 2021)
16 |
17 | - Add 2 new options: `convertTypes` (#123) and `resize` (#130).
18 | - Ignore the `strict` option when the `maxWidth/Height` option is set and its value is less than the natural width/height of the image (#134).
19 | .
20 |
21 | ## 1.0.7 (Nov 28, 2020)
22 |
23 | - Update the built-in dependencies for better adaptability.
24 |
25 | ## 1.0.6 (Nov 23, 2019)
26 |
27 | - Fix the `The operation is insecure` error (#57).
28 |
29 | ## 1.0.5 (Jan 23, 2019)
30 |
31 | - Fix the wrong generated URL when the given image's orientation is 1 (#64).
32 |
33 | ## 1.0.4 (Jan 19, 2019)
34 |
35 | - Regenerate the initial URL only when the orientation was reset for better performance (#63).
36 |
37 | ## 1.0.3 (Dec 18, 2018)
38 |
39 | - Convert `TypedArray` to `Array` manually instead of using Babel helpers for better browser compatibility (#60).
40 |
41 | ## 1.0.2 (Dec 10, 2018)
42 |
43 | - Upgrade `is-blob` to v2.
44 | - Move `examples` folder to `docs` folder.
45 |
46 | ## 1.0.1 (Oct 24, 2018)
47 |
48 | - Simplify the state of canvas for the `beforeDraw` option.
49 | - Ignore range error when the image does not have correct Exif information.
50 |
51 | ## 1.0.0 (Oct 15, 2018)
52 |
53 | - Supports 15 options: `beforeDraw`, `checkOrientation`, `convertSize`, `drew`, `error`, `height`, `maxHeight`, `maxWidth`, `mimeType`, `minHeight`, `minWidth`, `quality`, `strict`, `success` and `width`.
54 | - Support 1 method: `abort`.
55 | - Support to compress images of `File` or `Blob` object.
56 | - Supports to translate Exif Orientation information.
57 |
--------------------------------------------------------------------------------
/docs/examples/grayscale.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Compressor.js
8 |
9 |
14 |
15 |
16 |
17 |
Grayscale
18 |
19 |
31 |
32 |
33 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/docs/examples/watermark.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Compressor.js
8 |
9 |
14 |
15 |
16 |
17 |
Watermark
18 |
19 |
31 |
32 |
33 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/docs/examples/work-with-promise.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Compressor.js
8 |
9 |
14 |
15 |
16 |
17 |
Work with promise
18 |
Easy to emulate a complete hook function with Promise.prototype.finally()
19 |
20 |
32 |
33 |
34 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/test/specs/options/maxWidth.spec.js:
--------------------------------------------------------------------------------
1 | describe('maxWidth', () => {
2 | it('should be `Infinity` by default', (done) => {
3 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
4 | const compressor = new Compressor(image);
5 |
6 | expect(compressor.options.maxWidth).to.equal(Infinity);
7 | done();
8 | });
9 | });
10 |
11 | it('should not greater than the given maximum width', (done) => {
12 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
13 | const compressor = new Compressor(image, {
14 | maxWidth: 100,
15 | success(result) {
16 | const newImage = new Image();
17 |
18 | newImage.onload = () => {
19 | expect(newImage.naturalWidth).to.equal(100);
20 | done();
21 | };
22 | newImage.src = URL.createObjectURL(result);
23 | },
24 | });
25 |
26 | expect(compressor.options.maxWidth).to.equal(100);
27 | });
28 | });
29 |
30 | it('should not greater than the given maximum width even it is rotated', (done) => {
31 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
32 | new Compressor(image, {
33 | maxWidth: 100,
34 | success(result) {
35 | const newImage = new Image();
36 |
37 | newImage.onload = () => {
38 | expect(newImage.naturalWidth).to.equal(100);
39 | done();
40 | };
41 | newImage.src = URL.createObjectURL(result);
42 | },
43 | });
44 | });
45 | });
46 |
47 | it('should be ignored when the given maximum width does not greater than 0', (done) => {
48 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
49 | new Compressor(image, {
50 | maxWidth: -100,
51 | success(result) {
52 | const newImage = new Image();
53 |
54 | newImage.onload = () => {
55 | expect(newImage.naturalWidth).to.above(0);
56 | done();
57 | };
58 | newImage.src = URL.createObjectURL(result);
59 | },
60 | });
61 | });
62 | });
63 |
64 | it('should be resized to fit the aspect ratio of the original image when the `maxHeight` option is set', (done) => {
65 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
66 | new Compressor(image, {
67 | maxWidth: 100,
68 | maxHeight: 60,
69 | success(result) {
70 | const newImage = new Image();
71 |
72 | newImage.onload = () => {
73 | expect(newImage.naturalWidth).to.equal(50);
74 | expect(newImage.naturalHeight).to.equal(60);
75 | done();
76 | };
77 | newImage.src = URL.createObjectURL(result);
78 | },
79 | });
80 | });
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/test/specs/options/maxHeight.spec.js:
--------------------------------------------------------------------------------
1 | describe('maxHeight', () => {
2 | it('should be `Infinity` by default', (done) => {
3 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
4 | const compressor = new Compressor(image);
5 |
6 | expect(compressor.options.maxHeight).to.equal(Infinity);
7 | done();
8 | });
9 | });
10 |
11 | it('should not greater than the given maximum height', (done) => {
12 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
13 | const compressor = new Compressor(image, {
14 | maxHeight: 100,
15 | success(result) {
16 | const newImage = new Image();
17 |
18 | newImage.onload = () => {
19 | expect(newImage.naturalHeight).to.equal(100);
20 | done();
21 | };
22 | newImage.src = URL.createObjectURL(result);
23 | },
24 | });
25 |
26 | expect(compressor.options.maxHeight).to.equal(100);
27 | });
28 | });
29 |
30 | it('should not greater than the given maximum height even it is rotated', (done) => {
31 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
32 | new Compressor(image, {
33 | maxHeight: 100,
34 | success(result) {
35 | const newImage = new Image();
36 |
37 | newImage.onload = () => {
38 | expect(newImage.naturalHeight).to.equal(100);
39 | done();
40 | };
41 | newImage.src = URL.createObjectURL(result);
42 | },
43 | });
44 | });
45 | });
46 |
47 | it('should be ignored when the given maximum height does not greater than 0', (done) => {
48 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
49 | new Compressor(image, {
50 | maxHeight: -100,
51 | success(result) {
52 | const newImage = new Image();
53 |
54 | newImage.onload = () => {
55 | expect(newImage.naturalHeight).to.above(0);
56 | done();
57 | };
58 | newImage.src = URL.createObjectURL(result);
59 | },
60 | });
61 | });
62 | });
63 |
64 | it('should be resized to fit the aspect ratio of the original image when the `maxWidth` option is set', (done) => {
65 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
66 | new Compressor(image, {
67 | maxWidth: 50,
68 | maxHeight: 100,
69 | success(result) {
70 | const newImage = new Image();
71 |
72 | newImage.onload = () => {
73 | expect(newImage.naturalWidth).to.equal(50);
74 | expect(newImage.naturalHeight).to.equal(60);
75 | done();
76 | };
77 | newImage.src = URL.createObjectURL(result);
78 | },
79 | });
80 | });
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/test/specs/Compressor.spec.js:
--------------------------------------------------------------------------------
1 | describe('Compressor', () => {
2 | it('should be a class (function)', () => {
3 | expect(Compressor).to.be.a('function');
4 | });
5 |
6 | it('should throw error when the first argument is not an image File or Blob object', () => {
7 | new Compressor(new Blob(['Hello, World!']), {
8 | error(err) {
9 | expect(err.message).to.equal('The first argument must be an image File or Blob object.');
10 | },
11 | });
12 | });
13 |
14 | it('should throw error when the first argument is not an valid image File or Blob object', () => {
15 | new Compressor(new Blob(['Hello, World!'], {
16 | type: 'image/jpeg',
17 | }), {
18 | error(err) {
19 | expect(err.message).to.equal('Failed to load the image.');
20 | },
21 | });
22 | });
23 |
24 | it('should throw error when failed to read the image with FileReader', (done) => {
25 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
26 | const compressor = new Compressor(image, {
27 | error(err) {
28 | expect(err.message).to.equal('Failed to read the image with FileReader.');
29 | done();
30 | },
31 | });
32 |
33 | // The first asynchronous process will be image reading when enable orientation checking
34 | compressor.reader.onload = null;
35 | compressor.reader.onerror();
36 | });
37 | });
38 |
39 | it('should throw error when failed to load the image', (done) => {
40 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
41 | const compressor = new Compressor(image, {
42 | checkOrientation: false,
43 | error(err) {
44 | expect(err.message).to.equal('Failed to load the image.');
45 | done();
46 | },
47 | });
48 |
49 | // The first asynchronous process will be image loading when disable orientation checking
50 | compressor.image.onload = null;
51 | compressor.image.onerror();
52 | });
53 | });
54 |
55 | it('should work with polyfill library when `canvas.toBlob` is not supported', (done) => {
56 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
57 | new Compressor(image, {
58 | drew(context, canvas) {
59 | canvas.toBlob = null;
60 | },
61 | success(result) {
62 | expect(result).to.be.a('blob');
63 | done();
64 | },
65 | });
66 | });
67 | });
68 |
69 | it('should work when the `canvas.toBlob` function does not output any blob object', (done) => {
70 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
71 | new Compressor(image, {
72 | drew(context, canvas) {
73 | canvas.toBlob = (callback) => {
74 | callback(null);
75 | };
76 | },
77 | success(result) {
78 | expect(result).to.equal(image);
79 | done();
80 | },
81 | });
82 | });
83 | });
84 | });
85 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "compressorjs",
3 | "version": "1.2.1",
4 | "description": "JavaScript image compressor.",
5 | "main": "dist/compressor.common.js",
6 | "module": "dist/compressor.esm.js",
7 | "browser": "dist/compressor.js",
8 | "types": "types/index.d.ts",
9 | "files": [
10 | "src",
11 | "dist",
12 | "types"
13 | ],
14 | "scripts": {
15 | "build": "rollup -c",
16 | "clean": "del-cli dist",
17 | "compress": "uglifyjs dist/compressor.js -o dist/compressor.min.js -c -m --comments /^!/",
18 | "dev": "rollup -c -m -w",
19 | "lint": "npm run lint:js && npm run lint:css",
20 | "lint:css": "stylelint docs/**/*.css --fix",
21 | "lint:js": "eslint src test *.js --fix",
22 | "prepare": "husky install",
23 | "release": "npm run clean && npm run lint && npm run build && npm run compress && npm test",
24 | "start": "npm run dev",
25 | "test": "cross-env NODE_ENV=test karma start test/karma.conf.js"
26 | },
27 | "repository": {
28 | "type": "git",
29 | "url": "git+https://github.com/fengyuanchen/compressorjs.git"
30 | },
31 | "keywords": [
32 | "image",
33 | "compress",
34 | "compressor",
35 | "compressor.js",
36 | "image-compressor",
37 | "lossy-compression",
38 | "javascript",
39 | "front-end",
40 | "web"
41 | ],
42 | "author": {
43 | "name": "Chen Fengyuan",
44 | "url": "https://chenfengyuan.com/"
45 | },
46 | "license": "MIT",
47 | "bugs": {
48 | "url": "https://github.com/fengyuanchen/compressorjs/issues"
49 | },
50 | "homepage": "https://fengyuanchen.github.io/compressorjs",
51 | "dependencies": {
52 | "blueimp-canvas-to-blob": "^3.29.0",
53 | "is-blob": "^2.1.0"
54 | },
55 | "devDependencies": {
56 | "@babel/core": "^7.26.0",
57 | "@babel/plugin-transform-object-assign": "^7.25.9",
58 | "@babel/preset-env": "^7.26.0",
59 | "@commitlint/cli": "^17.8.1",
60 | "@commitlint/config-conventional": "^17.8.1",
61 | "@rollup/plugin-babel": "^5.3.1",
62 | "@rollup/plugin-commonjs": "^21.1.0",
63 | "@rollup/plugin-node-resolve": "^13.3.0",
64 | "@rollup/plugin-replace": "^3.1.0",
65 | "babel-plugin-istanbul": "^6.1.1",
66 | "chai": "^4.5.0",
67 | "change-case": "^4.1.2",
68 | "codecov": "^3.8.3",
69 | "create-banner": "^2.0.0",
70 | "cross-env": "^7.0.3",
71 | "del-cli": "^5.1.0",
72 | "eslint": "^8.35.0",
73 | "eslint-config-airbnb-base": "^15.0.0",
74 | "eslint-plugin-import": "^2.31.0",
75 | "husky": "^8.0.3",
76 | "karma": "^6.4.4",
77 | "karma-chai": "^0.1.0",
78 | "karma-chrome-launcher": "^3.2.0",
79 | "karma-coverage-istanbul-reporter": "^3.0.3",
80 | "karma-mocha": "^2.0.1",
81 | "karma-mocha-reporter": "^2.2.5",
82 | "karma-rollup-preprocessor": "^7.0.8",
83 | "lint-staged": "^13.3.0",
84 | "mocha": "^10.8.2",
85 | "puppeteer": "^19.7.2",
86 | "rollup": "^2.79.2",
87 | "stylelint": "^15.11.0",
88 | "stylelint-config-standard": "^30.0.1",
89 | "stylelint-order": "^6.0.4",
90 | "uglify-js": "^3.19.3"
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: "\U0001F41E Bug report"
2 | description: Create a report to help us improve
3 | body:
4 | - type: markdown
5 | attributes:
6 | value: |
7 | **Before You Start...**
8 |
9 | This form is only for submitting bug reports. If you have a usage question
10 | or are unsure if this is really a bug, make sure to:
11 |
12 | - Read the [docs](https://fengyuanchen.github.io/compressorjs)
13 | - Ask on [GitHub Discussions](https://github.com/fengyuanchen/compressorjs/discussions)
14 | - Look for / ask questions on [Stack Overflow](https://stackoverflow.com/questions/ask?tags=compressorjs)
15 |
16 | Also try to search for your issue - it may have already been answered or even fixed in the development branch.
17 | However, if you find that an old, closed issue still persists in the latest version,
18 | you should open a new issue using the form below instead of commenting on the old issue.
19 | - type: input
20 | id: version
21 | attributes:
22 | label: Compressor version
23 | validations:
24 | required: true
25 | - type: input
26 | id: reproduction-link
27 | attributes:
28 | label: Link to minimal reproduction
29 | description: |
30 | The easiest way to provide a reproduction is by showing the bug in the [Playground](https://fengyuanchen.github.io/compressorjs).
31 | If it cannot be reproduced in the playground and requires a proper build setup, try [StackBlitz](https://stackblitz.com/?starters=frontend).
32 | If neither of these are suitable, you can always provide a GitHub repository.
33 |
34 | The reproduction should be **minimal** - i.e. it should contain only the bare minimum amount of code needed
35 | to show the bug.
36 |
37 | Please do not just fill in a random link. The issue will be closed if no valid reproduction is provided.
38 | placeholder: Reproduction Link
39 | validations:
40 | required: true
41 | - type: textarea
42 | id: steps-to-reproduce
43 | attributes:
44 | label: Steps to reproduce
45 | description: |
46 | What do we need to do after opening your repro in order to make the bug happen? Clear and concise reproduction instructions are important for us to be able to triage your issue in a timely manner. Note that you can use [Markdown](https://guides.github.com/features/mastering-markdown/) to format lists and code.
47 | placeholder: Steps to reproduce
48 | validations:
49 | required: true
50 | - type: textarea
51 | id: expected
52 | attributes:
53 | label: What is expected?
54 | validations:
55 | required: true
56 | - type: textarea
57 | id: actually-happening
58 | attributes:
59 | label: What is actually happening?
60 | validations:
61 | required: true
62 | - type: textarea
63 | id: system-info
64 | attributes:
65 | label: System Info
66 | description: Output of `npx envinfo --system --npmPackages compressorjs --binaries --browsers`
67 | render: shell
68 | placeholder: System, Binaries, Browsers
69 | - type: textarea
70 | id: additional-comments
71 | attributes:
72 | label: Any additional comments?
73 | description: e.g. some background/context of how you ran into this bug.
74 |
--------------------------------------------------------------------------------
/test/specs/methods/abort.spec.js:
--------------------------------------------------------------------------------
1 | describe('abort', () => {
2 | it('should abort the compressing process before read', (done) => {
3 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
4 | const compressor = new Compressor(image, {
5 | success() {
6 | expect.fail(1, 0);
7 | },
8 | error(err) {
9 | expect(err.message).to.equal('Aborted to read the image with FileReader.');
10 | },
11 | });
12 |
13 | // The first asynchronous process will be image reading when enable orientation checking
14 | compressor.abort();
15 | done();
16 | });
17 | });
18 |
19 | it('should abort the compressing process before load', (done) => {
20 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
21 | const compressor = new Compressor(image, {
22 | checkOrientation: false,
23 | success() {
24 | expect.fail(1, 0);
25 | },
26 | error(err) {
27 | expect(err.message).to.equal('Aborted to load the image.');
28 | },
29 | });
30 |
31 | // The first asynchronous process will be image loading when disable orientation checking
32 | compressor.abort();
33 | done();
34 | });
35 | });
36 |
37 | it('should abort the compressing process before draw', (done) => {
38 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
39 | new Compressor(image, {
40 | beforeDraw() {
41 | this.abort();
42 | },
43 | drew() {
44 | expect.fail(1, 0);
45 | },
46 | error(err) {
47 | expect(err.message).to.equal('The compression process has been aborted.');
48 | done();
49 | },
50 | });
51 | });
52 | });
53 |
54 | it('should abort the compressing process after drew', (done) => {
55 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
56 | new Compressor(image, {
57 | drew() {
58 | this.abort();
59 | },
60 | success() {
61 | expect.fail(1, 0);
62 | },
63 | error(err) {
64 | expect(err.message).to.equal('The compression process has been aborted.');
65 | done();
66 | },
67 | });
68 | });
69 | });
70 |
71 | it('should abort the compressing process before output', (done) => {
72 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
73 | new Compressor(image, {
74 | drew() {
75 | setTimeout(() => {
76 | this.abort();
77 | }, 0);
78 | },
79 | success() {
80 | expect.fail(1, 0);
81 | },
82 | error(err) {
83 | expect(err.message).to.equal('The compression process has been aborted.');
84 | done();
85 | },
86 | });
87 | });
88 | });
89 |
90 | it('should only can be aborted once', (done) => {
91 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
92 | let count = 0;
93 | const compressor = new Compressor(image, {
94 | error() {
95 | count += 1;
96 | compressor.abort();
97 | },
98 | });
99 |
100 | compressor.abort();
101 | setTimeout(() => {
102 | expect(count).to.equal(1);
103 | done();
104 | }, 500);
105 | });
106 | });
107 | });
108 |
--------------------------------------------------------------------------------
/test/specs/options/minWidth.spec.js:
--------------------------------------------------------------------------------
1 | describe('minWidth', () => {
2 | it('should be `0` by default', (done) => {
3 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
4 | const compressor = new Compressor(image);
5 |
6 | expect(compressor.options.minWidth).to.equal(0);
7 | done();
8 | });
9 | });
10 |
11 | it('should not less than the given minimum width', (done) => {
12 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
13 | const compressor = new Compressor(image, {
14 | minWidth: 1000,
15 | success(result) {
16 | const newImage = new Image();
17 |
18 | newImage.onload = () => {
19 | expect(newImage.naturalWidth).to.equal(1000);
20 | done();
21 | };
22 | newImage.src = URL.createObjectURL(result);
23 | },
24 | });
25 |
26 | expect(compressor.options.minWidth).to.equal(1000);
27 | });
28 | });
29 |
30 | it('should not less than the given minimum width even it is rotated', (done) => {
31 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
32 | new Compressor(image, {
33 | minWidth: 1000,
34 | success(result) {
35 | const newImage = new Image();
36 |
37 | newImage.onload = () => {
38 | expect(newImage.naturalWidth).to.equal(1000);
39 | done();
40 | };
41 | newImage.src = URL.createObjectURL(result);
42 | },
43 | });
44 | });
45 | });
46 |
47 | it('should be ignored when the given minimum width does not greater than 0', (done) => {
48 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
49 | new Compressor(image, {
50 | minWidth: -1000,
51 | success(result) {
52 | const newImage = new Image();
53 |
54 | newImage.onload = () => {
55 | expect(newImage.naturalWidth).to.above(0);
56 | done();
57 | };
58 | newImage.src = URL.createObjectURL(result);
59 | },
60 | });
61 | });
62 | });
63 |
64 | it('should be resized to fit the aspect ratio of the original image when the `minHeight` option is set', (done) => {
65 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
66 | new Compressor(image, {
67 | minWidth: 1000,
68 | minHeight: 900,
69 | success(result) {
70 | const newImage = new Image();
71 |
72 | newImage.onload = () => {
73 | expect(newImage.naturalWidth).to.equal(1000);
74 | expect(newImage.naturalHeight).to.equal(1200);
75 | done();
76 | };
77 | newImage.src = URL.createObjectURL(result);
78 | },
79 | });
80 | });
81 | });
82 |
83 | it('should be ignored when the `maxWidth` is set and its value is more lesser', (done) => {
84 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
85 | new Compressor(image, {
86 | minWidth: 1000,
87 | maxWidth: 100,
88 | success(result) {
89 | const newImage = new Image();
90 |
91 | newImage.onload = () => {
92 | expect(newImage.naturalWidth).to.equal(100);
93 | done();
94 | };
95 | newImage.src = URL.createObjectURL(result);
96 | },
97 | });
98 | });
99 | });
100 | });
101 |
--------------------------------------------------------------------------------
/test/specs/options/minHeight.spec.js:
--------------------------------------------------------------------------------
1 | describe('minHeight', () => {
2 | it('should be `0` by default', (done) => {
3 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
4 | const compressor = new Compressor(image);
5 |
6 | expect(compressor.options.minHeight).to.equal(0);
7 | done();
8 | });
9 | });
10 |
11 | it('should not less than the given minimum height', (done) => {
12 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
13 | const compressor = new Compressor(image, {
14 | minHeight: 1000,
15 | success(result) {
16 | const newImage = new Image();
17 |
18 | newImage.onload = () => {
19 | expect(newImage.naturalHeight).to.equal(1000);
20 | done();
21 | };
22 | newImage.src = URL.createObjectURL(result);
23 | },
24 | });
25 |
26 | expect(compressor.options.minHeight).to.equal(1000);
27 | });
28 | });
29 |
30 | it('should not less than the given minimum height even it is rotated', (done) => {
31 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
32 | new Compressor(image, {
33 | minHeight: 1000,
34 | success(result) {
35 | const newImage = new Image();
36 |
37 | newImage.onload = () => {
38 | expect(newImage.naturalHeight).to.equal(1000);
39 | done();
40 | };
41 | newImage.src = URL.createObjectURL(result);
42 | },
43 | });
44 | });
45 | });
46 |
47 | it('should be ignored when the given minimum height does not greater than 0', (done) => {
48 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
49 | new Compressor(image, {
50 | minHeight: -1000,
51 | success(result) {
52 | const newImage = new Image();
53 |
54 | newImage.onload = () => {
55 | expect(newImage.naturalHeight).to.above(0);
56 | done();
57 | };
58 | newImage.src = URL.createObjectURL(result);
59 | },
60 | });
61 | });
62 | });
63 |
64 | it('should be resized to fit the aspect ratio of the original image when the `minWidth` option is set', (done) => {
65 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
66 | new Compressor(image, {
67 | minWidth: 750,
68 | minHeight: 1000,
69 | success(result) {
70 | const newImage = new Image();
71 |
72 | newImage.onload = () => {
73 | expect(newImage.naturalWidth).to.equal(833);
74 | expect(newImage.naturalHeight).to.equal(1000);
75 | done();
76 | };
77 | newImage.src = URL.createObjectURL(result);
78 | },
79 | });
80 | });
81 | });
82 |
83 | it('should be ignored when the `maxHeight` is set and its value is more lesser', (done) => {
84 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
85 | new Compressor(image, {
86 | minHeight: 1000,
87 | maxHeight: 100,
88 | success(result) {
89 | const newImage = new Image();
90 |
91 | newImage.onload = () => {
92 | expect(newImage.naturalHeight).to.equal(100);
93 | done();
94 | };
95 | newImage.src = URL.createObjectURL(result);
96 | },
97 | });
98 | });
99 | });
100 | });
101 |
--------------------------------------------------------------------------------
/docs/js/main.js:
--------------------------------------------------------------------------------
1 | window.addEventListener('DOMContentLoaded', function () {
2 | var Vue = window.Vue;
3 | var URL = window.URL || window.webkitURL;
4 | var XMLHttpRequest = window.XMLHttpRequest;
5 | var Compressor = window.Compressor;
6 |
7 | Vue.component('VueCompareImage', window.vueCompareImage);
8 |
9 | new Vue({
10 | el: '#app',
11 |
12 | data: function () {
13 | var vm = this;
14 |
15 | return {
16 | options: {
17 | strict: true,
18 | checkOrientation: true,
19 | retainExif: false,
20 | maxWidth: undefined,
21 | maxHeight: undefined,
22 | minWidth: 0,
23 | minHeight: 0,
24 | width: undefined,
25 | height: undefined,
26 | resize: 'none',
27 | quality: 0.8,
28 | mimeType: '',
29 | convertTypes: 'image/png',
30 | convertSize: 5000000,
31 | success: function (result) {
32 | console.log('Output: ', result);
33 |
34 | if (URL) {
35 | vm.outputURL = URL.createObjectURL(result);
36 | }
37 |
38 | vm.output = result;
39 | vm.$refs.input.value = '';
40 | },
41 | error: function (err) {
42 | window.alert(err.message);
43 | },
44 | },
45 | inputURL: '',
46 | outputURL: '',
47 | input: {},
48 | output: {},
49 | };
50 | },
51 |
52 | filters: {
53 | prettySize: function (size) {
54 | var kilobyte = 1024;
55 | var megabyte = kilobyte * kilobyte;
56 |
57 | if (size > megabyte) {
58 | return (size / megabyte).toFixed(2) + ' MB';
59 | } else if (size > kilobyte) {
60 | return (size / kilobyte).toFixed(2) + ' KB';
61 | } else if (size >= 0) {
62 | return size + ' B';
63 | }
64 |
65 | return 'N/A';
66 | },
67 | },
68 |
69 | methods: {
70 | compress: function (file) {
71 | if (!file) {
72 | return;
73 | }
74 |
75 | console.log('Input: ', file);
76 |
77 | if (URL) {
78 | this.inputURL = URL.createObjectURL(file);
79 | }
80 |
81 | this.input = file;
82 | new Compressor(file, this.options);
83 | },
84 |
85 | change: function (e) {
86 | this.compress(e.target.files ? e.target.files[0] : null);
87 | },
88 |
89 | dragover: function(e) {
90 | e.preventDefault();
91 | },
92 |
93 | drop: function(e) {
94 | e.preventDefault();
95 | this.compress(e.dataTransfer.files ? e.dataTransfer.files[0] : null);
96 | },
97 | },
98 |
99 | watch: {
100 | options: {
101 | deep: true,
102 | handler: function () {
103 | this.compress(this.input);
104 | },
105 | },
106 | },
107 |
108 | mounted: function () {
109 | if (!XMLHttpRequest) {
110 | return;
111 | }
112 |
113 | var vm = this;
114 | var xhr = new XMLHttpRequest();
115 |
116 | xhr.onload = function () {
117 | var blob = xhr.response;
118 | var date = new Date();
119 |
120 | blob.lastModified = date.getTime();
121 | blob.lastModifiedDate = date;
122 | blob.name = 'picture.jpg';
123 | vm.compress(blob);
124 | };
125 | xhr.open('GET', 'images/picture.jpg');
126 | xhr.responseType = 'blob';
127 | xhr.send();
128 | },
129 | });
130 | });
131 |
--------------------------------------------------------------------------------
/test/specs/options/strict.spec.js:
--------------------------------------------------------------------------------
1 | describe('strict', () => {
2 | it('should be `true` by default', (done) => {
3 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
4 | const compressor = new Compressor(image);
5 |
6 | expect(compressor.options.strict).to.be.true;
7 | done();
8 | });
9 | });
10 |
11 | it('should output the original image as the result', (done) => {
12 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
13 | new Compressor(image, {
14 | quality: 1,
15 | success(result) {
16 | expect(result).to.equal(image);
17 | done();
18 | },
19 | });
20 | });
21 | });
22 |
23 | it('should be ignored when the `mimeType` option is set and its value is different from the mime type of the image', (done) => {
24 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
25 | new Compressor(image, {
26 | mimeType: 'image/png',
27 | success(result) {
28 | expect(result).to.not.equal(image);
29 | done();
30 | },
31 | });
32 | });
33 | });
34 |
35 | it('should be ignored when the `width` option is set and its value is greater than the natural width of the image', (done) => {
36 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
37 | new Compressor(image, {
38 | width: 501,
39 | success(result) {
40 | expect(result).to.not.equal(image);
41 | done();
42 | },
43 | });
44 | });
45 | });
46 |
47 | it('should be ignored when the `height` option is set and its value is greater than the natural height of the image', (done) => {
48 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
49 | new Compressor(image, {
50 | height: 601,
51 | success(result) {
52 | expect(result).to.not.equal(image);
53 | done();
54 | },
55 | });
56 | });
57 | });
58 |
59 | it('should be ignored when the `minWidth` option is set and its value is greater than the natural width of the image', (done) => {
60 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
61 | new Compressor(image, {
62 | minWidth: 501,
63 | success(result) {
64 | expect(result).to.not.equal(image);
65 | done();
66 | },
67 | });
68 | });
69 | });
70 |
71 | it('should be ignored when the `minHeight` option is set and its value is greater than the natural height of the image', (done) => {
72 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
73 | new Compressor(image, {
74 | minHeight: 601,
75 | success(result) {
76 | expect(result).to.not.equal(image);
77 | done();
78 | },
79 | });
80 | });
81 | });
82 |
83 | it('should be ignored when the `maxWidth` option is set and its value is less than the natural width of the image', (done) => {
84 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
85 | new Compressor(image, {
86 | maxWidth: 499,
87 | success(result) {
88 | expect(result).to.not.equal(image);
89 | done();
90 | },
91 | });
92 | });
93 | });
94 |
95 | it('should be ignored when the `maxHeight` option is set and its value is less than the natural height of the image', (done) => {
96 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
97 | new Compressor(image, {
98 | maxHeight: 599,
99 | success(result) {
100 | expect(result).to.not.equal(image);
101 | done();
102 | },
103 | });
104 | });
105 | });
106 | });
107 |
--------------------------------------------------------------------------------
/src/defaults.js:
--------------------------------------------------------------------------------
1 | export default {
2 | /**
3 | * Indicates if output the original image instead of the compressed one
4 | * when the size of the compressed image is greater than the original one's
5 | * @type {boolean}
6 | */
7 | strict: true,
8 |
9 | /**
10 | * Indicates if read the image's Exif Orientation information,
11 | * and then rotate or flip the image automatically.
12 | * @type {boolean}
13 | */
14 | checkOrientation: true,
15 |
16 | /**
17 | * Indicates if retain the image's Exif information after compressed.
18 | * @type {boolean}
19 | */
20 | retainExif: false,
21 |
22 | /**
23 | * The max width of the output image.
24 | * @type {number}
25 | */
26 | maxWidth: Infinity,
27 |
28 | /**
29 | * The max height of the output image.
30 | * @type {number}
31 | */
32 | maxHeight: Infinity,
33 |
34 | /**
35 | * The min width of the output image.
36 | * @type {number}
37 | */
38 | minWidth: 0,
39 |
40 | /**
41 | * The min height of the output image.
42 | * @type {number}
43 | */
44 | minHeight: 0,
45 |
46 | /**
47 | * The width of the output image.
48 | * If not specified, the natural width of the source image will be used.
49 | * @type {number}
50 | */
51 | width: undefined,
52 |
53 | /**
54 | * The height of the output image.
55 | * If not specified, the natural height of the source image will be used.
56 | * @type {number}
57 | */
58 | height: undefined,
59 |
60 | /**
61 | * Sets how the size of the image should be resized to the container
62 | * specified by the `width` and `height` options.
63 | * @type {string}
64 | */
65 | resize: 'none',
66 |
67 | /**
68 | * The quality of the output image.
69 | * It must be a number between `0` and `1`,
70 | * and only available for `image/jpeg` and `image/webp` images.
71 | * Check out {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob canvas.toBlob}.
72 | * @type {number}
73 | */
74 | quality: 0.8,
75 |
76 | /**
77 | * The mime type of the output image.
78 | * By default, the original mime type of the source image file will be used.
79 | * @type {string}
80 | */
81 | mimeType: 'auto',
82 |
83 | /**
84 | * Files whose file type is included in this list,
85 | * and whose file size exceeds the `convertSize` value will be converted to JPEGs.
86 | * @type {string|Array}
87 | */
88 | convertTypes: ['image/png'],
89 |
90 | /**
91 | * PNG files over this size (5 MB by default) will be converted to JPEGs.
92 | * To disable this, just set the value to `Infinity`.
93 | * @type {number}
94 | */
95 | convertSize: 5000000,
96 |
97 | /**
98 | * The hook function to execute before draw the image into the canvas for compression.
99 | * @type {Function}
100 | * @param {CanvasRenderingContext2D} context - The 2d rendering context of the canvas.
101 | * @param {HTMLCanvasElement} canvas - The canvas for compression.
102 | * @example
103 | * function (context, canvas) {
104 | * context.fillStyle = '#fff';
105 | * }
106 | */
107 | beforeDraw: null,
108 |
109 | /**
110 | * The hook function to execute after drew the image into the canvas for compression.
111 | * @type {Function}
112 | * @param {CanvasRenderingContext2D} context - The 2d rendering context of the canvas.
113 | * @param {HTMLCanvasElement} canvas - The canvas for compression.
114 | * @example
115 | * function (context, canvas) {
116 | * context.filter = 'grayscale(100%)';
117 | * }
118 | */
119 | drew: null,
120 |
121 | /**
122 | * The hook function to execute when success to compress the image.
123 | * @type {Function}
124 | * @param {File} file - The compressed image File object.
125 | * @example
126 | * function (file) {
127 | * console.log(file);
128 | * }
129 | */
130 | success: null,
131 |
132 | /**
133 | * The hook function to execute when fail to compress the image.
134 | * @type {Function}
135 | * @param {Error} err - An Error object.
136 | * @example
137 | * function (err) {
138 | * console.log(err.message);
139 | * }
140 | */
141 | error: null,
142 | };
143 |
--------------------------------------------------------------------------------
/test/specs/options/width.spec.js:
--------------------------------------------------------------------------------
1 | describe('width', () => {
2 | it('should be `undefined` by default', (done) => {
3 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
4 | const compressor = new Compressor(image);
5 |
6 | expect(compressor.options.width).to.be.undefined;
7 | done();
8 | });
9 | });
10 |
11 | it('should equal to the given width', (done) => {
12 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
13 | const compressor = new Compressor(image, {
14 | width: 100,
15 | success(result) {
16 | const newImage = new Image();
17 |
18 | newImage.onload = () => {
19 | expect(newImage.naturalWidth).to.equal(100);
20 | done();
21 | };
22 | newImage.src = URL.createObjectURL(result);
23 | },
24 | });
25 |
26 | expect(compressor.options.width).to.equal(100);
27 | });
28 | });
29 |
30 | it('should equal to the given width even it is rotated', (done) => {
31 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
32 | new Compressor(image, {
33 | width: 100,
34 | success(result) {
35 | const newImage = new Image();
36 |
37 | newImage.onload = () => {
38 | expect(newImage.naturalWidth).to.equal(100);
39 | done();
40 | };
41 | newImage.src = URL.createObjectURL(result);
42 | },
43 | });
44 | });
45 | });
46 |
47 | it('should be ignored when the given width does not greater than 0', (done) => {
48 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
49 | new Compressor(image, {
50 | width: -100,
51 | success(result) {
52 | const newImage = new Image();
53 |
54 | newImage.onload = () => {
55 | expect(newImage.naturalWidth).to.above(0);
56 | done();
57 | };
58 | newImage.src = URL.createObjectURL(result);
59 | },
60 | });
61 | });
62 | });
63 |
64 | it('should be floored when it contains decimal number', (done) => {
65 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
66 | new Compressor(image, {
67 | // eslint-disable-next-line no-loss-of-precision
68 | width: 100.30000000000000004,
69 | success(result) {
70 | const newImage = new Image();
71 |
72 | newImage.onload = () => {
73 | expect(newImage.naturalWidth).to.equal(100);
74 | done();
75 | };
76 | newImage.src = URL.createObjectURL(result);
77 | },
78 | });
79 | });
80 | });
81 |
82 | it('should be resized to fit the aspect ratio of the original image when the `height` option is set', (done) => {
83 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
84 | new Compressor(image, {
85 | width: 100,
86 | height: 60,
87 | success(result) {
88 | const newImage = new Image();
89 |
90 | newImage.onload = () => {
91 | expect(newImage.naturalWidth).to.equal(50);
92 | expect(newImage.naturalHeight).to.equal(60);
93 | done();
94 | };
95 | newImage.src = URL.createObjectURL(result);
96 | },
97 | });
98 | });
99 | });
100 |
101 | it('should be resized to fit the aspect ratio of the original image when the `height` option is set even it is rotated', (done) => {
102 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
103 | new Compressor(image, {
104 | width: 100,
105 | height: 60,
106 | success(result) {
107 | const newImage = new Image();
108 |
109 | newImage.onload = () => {
110 | expect(newImage.naturalWidth).to.equal(50);
111 | expect(newImage.naturalHeight).to.equal(60);
112 | done();
113 | };
114 | newImage.src = URL.createObjectURL(result);
115 | },
116 | });
117 | });
118 | });
119 |
120 | it('should be ignored when the `minWidth` option is set and its value is more greater', (done) => {
121 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
122 | new Compressor(image, {
123 | width: 100,
124 | minWidth: 200,
125 | success(result) {
126 | const newImage = new Image();
127 |
128 | newImage.onload = () => {
129 | expect(newImage.naturalWidth).to.equal(200);
130 | done();
131 | };
132 | newImage.src = URL.createObjectURL(result);
133 | },
134 | });
135 | });
136 | });
137 |
138 | it('should be ignored when the `maxWidth` option is set and its value is lesser', (done) => {
139 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
140 | new Compressor(image, {
141 | width: 200,
142 | maxWidth: 100,
143 | success(result) {
144 | const newImage = new Image();
145 |
146 | newImage.onload = () => {
147 | expect(newImage.naturalWidth).to.equal(100);
148 | done();
149 | };
150 | newImage.src = URL.createObjectURL(result);
151 | },
152 | });
153 | });
154 | });
155 | });
156 |
--------------------------------------------------------------------------------
/test/specs/options/height.spec.js:
--------------------------------------------------------------------------------
1 | describe('height', () => {
2 | it('should be `undefined` by default', (done) => {
3 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
4 | const compressor = new Compressor(image);
5 |
6 | expect(compressor.options.height).to.be.undefined;
7 | done();
8 | });
9 | });
10 |
11 | it('should equal to the given height', (done) => {
12 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
13 | const compressor = new Compressor(image, {
14 | height: 100,
15 | success(result) {
16 | const newImage = new Image();
17 |
18 | newImage.onload = () => {
19 | expect(newImage.naturalHeight).to.equal(100);
20 | done();
21 | };
22 | newImage.src = URL.createObjectURL(result);
23 | },
24 | });
25 |
26 | expect(compressor.options.height).to.equal(100);
27 | });
28 | });
29 |
30 | it('should equal to the given height even it is rotated', (done) => {
31 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
32 | new Compressor(image, {
33 | height: 100,
34 | success(result) {
35 | const newImage = new Image();
36 |
37 | newImage.onload = () => {
38 | expect(newImage.naturalHeight).to.equal(100);
39 | done();
40 | };
41 | newImage.src = URL.createObjectURL(result);
42 | },
43 | });
44 | });
45 | });
46 |
47 | it('should be ignored when the given height does not greater than 0', (done) => {
48 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
49 | new Compressor(image, {
50 | height: -100,
51 | success(result) {
52 | const newImage = new Image();
53 |
54 | newImage.onload = () => {
55 | expect(newImage.naturalHeight).to.above(0);
56 | done();
57 | };
58 | newImage.src = URL.createObjectURL(result);
59 | },
60 | });
61 | });
62 | });
63 |
64 | it('should be floored when it contains decimal number', (done) => {
65 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
66 | new Compressor(image, {
67 | // eslint-disable-next-line no-loss-of-precision
68 | height: 100.30000000000000004,
69 | success(result) {
70 | const newImage = new Image();
71 |
72 | newImage.onload = () => {
73 | expect(newImage.naturalHeight).to.equal(100);
74 | done();
75 | };
76 | newImage.src = URL.createObjectURL(result);
77 | },
78 | });
79 | });
80 | });
81 |
82 | it('should be resized to fit the aspect ratio of the original image when the `width` option is set', (done) => {
83 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
84 | new Compressor(image, {
85 | width: 50,
86 | height: 100,
87 | success(result) {
88 | const newImage = new Image();
89 |
90 | newImage.onload = () => {
91 | expect(newImage.naturalWidth).to.equal(50);
92 | expect(newImage.naturalHeight).to.equal(60);
93 | done();
94 | };
95 | newImage.src = URL.createObjectURL(result);
96 | },
97 | });
98 | });
99 | });
100 |
101 | it('should be resized to fit the aspect ratio of the original image when the `width` option is set even it is rotated', (done) => {
102 | window.loadImageAsBlob('/base/docs/images/picture.jpg', (image) => {
103 | new Compressor(image, {
104 | width: 50,
105 | height: 100,
106 | success(result) {
107 | const newImage = new Image();
108 |
109 | newImage.onload = () => {
110 | expect(newImage.naturalWidth).to.equal(50);
111 | expect(newImage.naturalHeight).to.equal(60);
112 | done();
113 | };
114 | newImage.src = URL.createObjectURL(result);
115 | },
116 | });
117 | });
118 | });
119 |
120 | it('should be ignored when the `minHeight` option is set and its value is more greater', (done) => {
121 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
122 | new Compressor(image, {
123 | height: 100,
124 | minHeight: 200,
125 | success(result) {
126 | const newImage = new Image();
127 |
128 | newImage.onload = () => {
129 | expect(newImage.naturalHeight).to.equal(200);
130 | done();
131 | };
132 | newImage.src = URL.createObjectURL(result);
133 | },
134 | });
135 | });
136 | });
137 |
138 | it('should be ignored when the `maxHeight` option is set and its value is lesser', (done) => {
139 | window.loadImageAsBlob('/base/docs/images/picture.png', (image) => {
140 | new Compressor(image, {
141 | height: 200,
142 | maxHeight: 100,
143 | success(result) {
144 | const newImage = new Image();
145 |
146 | newImage.onload = () => {
147 | expect(newImage.naturalHeight).to.equal(100);
148 | done();
149 | };
150 | newImage.src = URL.createObjectURL(result);
151 | },
152 | });
153 | });
154 | });
155 | });
156 |
--------------------------------------------------------------------------------
/src/utilities.js:
--------------------------------------------------------------------------------
1 | import {
2 | WINDOW,
3 | } from './constants';
4 |
5 | /**
6 | * Check if the given value is a positive number.
7 | * @param {*} value - The value to check.
8 | * @returns {boolean} Returns `true` if the given value is a positive number, else `false`.
9 | */
10 | export const isPositiveNumber = (value) => value > 0 && value < Infinity;
11 |
12 | const { slice } = Array.prototype;
13 |
14 | /**
15 | * Convert array-like or iterable object to an array.
16 | * @param {*} value - The value to convert.
17 | * @returns {Array} Returns a new array.
18 | */
19 | export function toArray(value) {
20 | return Array.from ? Array.from(value) : slice.call(value);
21 | }
22 |
23 | const REGEXP_IMAGE_TYPE = /^image\/.+$/;
24 |
25 | /**
26 | * Check if the given value is a mime type of image.
27 | * @param {*} value - The value to check.
28 | * @returns {boolean} Returns `true` if the given is a mime type of image, else `false`.
29 | */
30 | export function isImageType(value) {
31 | return REGEXP_IMAGE_TYPE.test(value);
32 | }
33 |
34 | /**
35 | * Convert image type to extension.
36 | * @param {string} value - The image type to convert.
37 | * @returns {boolean} Returns the image extension.
38 | */
39 | export function imageTypeToExtension(value) {
40 | let extension = isImageType(value) ? value.substr(6) : '';
41 |
42 | if (extension === 'jpeg') {
43 | extension = 'jpg';
44 | }
45 |
46 | return `.${extension}`;
47 | }
48 |
49 | const { fromCharCode } = String;
50 |
51 | /**
52 | * Get string from char code in data view.
53 | * @param {DataView} dataView - The data view for read.
54 | * @param {number} start - The start index.
55 | * @param {number} length - The read length.
56 | * @returns {string} The read result.
57 | */
58 | export function getStringFromCharCode(dataView, start, length) {
59 | let str = '';
60 | let i;
61 |
62 | length += start;
63 |
64 | for (i = start; i < length; i += 1) {
65 | str += fromCharCode(dataView.getUint8(i));
66 | }
67 |
68 | return str;
69 | }
70 |
71 | const { btoa } = WINDOW;
72 |
73 | /**
74 | * Transform array buffer to Data URL.
75 | * @param {ArrayBuffer} arrayBuffer - The array buffer to transform.
76 | * @param {string} mimeType - The mime type of the Data URL.
77 | * @returns {string} The result Data URL.
78 | */
79 | export function arrayBufferToDataURL(arrayBuffer, mimeType) {
80 | const chunks = [];
81 | const chunkSize = 8192;
82 | let uint8 = new Uint8Array(arrayBuffer);
83 |
84 | while (uint8.length > 0) {
85 | // XXX: Babel's `toConsumableArray` helper will throw error in IE or Safari 9
86 | // eslint-disable-next-line prefer-spread
87 | chunks.push(fromCharCode.apply(null, toArray(uint8.subarray(0, chunkSize))));
88 | uint8 = uint8.subarray(chunkSize);
89 | }
90 |
91 | return `data:${mimeType};base64,${btoa(chunks.join(''))}`;
92 | }
93 |
94 | /**
95 | * Get orientation value from given array buffer.
96 | * @param {ArrayBuffer} arrayBuffer - The array buffer to read.
97 | * @returns {number} The read orientation value.
98 | */
99 | export function resetAndGetOrientation(arrayBuffer) {
100 | const dataView = new DataView(arrayBuffer);
101 | let orientation;
102 |
103 | // Ignores range error when the image does not have correct Exif information
104 | try {
105 | let littleEndian;
106 | let app1Start;
107 | let ifdStart;
108 |
109 | // Only handle JPEG image (start by 0xFFD8)
110 | if (dataView.getUint8(0) === 0xFF && dataView.getUint8(1) === 0xD8) {
111 | const length = dataView.byteLength;
112 | let offset = 2;
113 |
114 | while (offset + 1 < length) {
115 | if (dataView.getUint8(offset) === 0xFF && dataView.getUint8(offset + 1) === 0xE1) {
116 | app1Start = offset;
117 | break;
118 | }
119 |
120 | offset += 1;
121 | }
122 | }
123 |
124 | if (app1Start) {
125 | const exifIDCode = app1Start + 4;
126 | const tiffOffset = app1Start + 10;
127 |
128 | if (getStringFromCharCode(dataView, exifIDCode, 4) === 'Exif') {
129 | const endianness = dataView.getUint16(tiffOffset);
130 |
131 | littleEndian = endianness === 0x4949;
132 |
133 | if (littleEndian || endianness === 0x4D4D /* bigEndian */) {
134 | if (dataView.getUint16(tiffOffset + 2, littleEndian) === 0x002A) {
135 | const firstIFDOffset = dataView.getUint32(tiffOffset + 4, littleEndian);
136 |
137 | if (firstIFDOffset >= 0x00000008) {
138 | ifdStart = tiffOffset + firstIFDOffset;
139 | }
140 | }
141 | }
142 | }
143 | }
144 |
145 | if (ifdStart) {
146 | const length = dataView.getUint16(ifdStart, littleEndian);
147 | let offset;
148 | let i;
149 |
150 | for (i = 0; i < length; i += 1) {
151 | offset = ifdStart + (i * 12) + 2;
152 |
153 | if (dataView.getUint16(offset, littleEndian) === 0x0112 /* Orientation */) {
154 | // 8 is the offset of the current tag's value
155 | offset += 8;
156 |
157 | // Get the original orientation value
158 | orientation = dataView.getUint16(offset, littleEndian);
159 |
160 | // Override the orientation with its default value
161 | dataView.setUint16(offset, 1, littleEndian);
162 | break;
163 | }
164 | }
165 | }
166 | } catch (e) {
167 | orientation = 1;
168 | }
169 |
170 | return orientation;
171 | }
172 |
173 | /**
174 | * Parse Exif Orientation value.
175 | * @param {number} orientation - The orientation to parse.
176 | * @returns {Object} The parsed result.
177 | */
178 | export function parseOrientation(orientation) {
179 | let rotate = 0;
180 | let scaleX = 1;
181 | let scaleY = 1;
182 |
183 | switch (orientation) {
184 | // Flip horizontal
185 | case 2:
186 | scaleX = -1;
187 | break;
188 |
189 | // Rotate left 180°
190 | case 3:
191 | rotate = -180;
192 | break;
193 |
194 | // Flip vertical
195 | case 4:
196 | scaleY = -1;
197 | break;
198 |
199 | // Flip vertical and rotate right 90°
200 | case 5:
201 | rotate = 90;
202 | scaleY = -1;
203 | break;
204 |
205 | // Rotate right 90°
206 | case 6:
207 | rotate = 90;
208 | break;
209 |
210 | // Flip horizontal and rotate right 90°
211 | case 7:
212 | rotate = 90;
213 | scaleX = -1;
214 | break;
215 |
216 | // Rotate left 90°
217 | case 8:
218 | rotate = -90;
219 | break;
220 |
221 | default:
222 | }
223 |
224 | return {
225 | rotate,
226 | scaleX,
227 | scaleY,
228 | };
229 | }
230 |
231 | const REGEXP_DECIMALS = /\.\d*(?:0|9){12}\d*$/;
232 |
233 | /**
234 | * Normalize decimal number.
235 | * Check out {@link https://0.30000000000000004.com/}
236 | * @param {number} value - The value to normalize.
237 | * @param {number} [times=100000000000] - The times for normalizing.
238 | * @returns {number} Returns the normalized number.
239 | */
240 | export function normalizeDecimalNumber(value, times = 100000000000) {
241 | return REGEXP_DECIMALS.test(value) ? (Math.round(value * times) / times) : value;
242 | }
243 |
244 | /**
245 | * Get the max sizes in a rectangle under the given aspect ratio.
246 | * @param {Object} data - The original sizes.
247 | * @param {string} [type='contain'] - The adjust type.
248 | * @returns {Object} The result sizes.
249 | */
250 | export function getAdjustedSizes(
251 | {
252 | aspectRatio,
253 | height,
254 | width,
255 | },
256 |
257 | // 'none' | 'contain' | 'cover'
258 | type = 'none',
259 | ) {
260 | const isValidWidth = isPositiveNumber(width);
261 | const isValidHeight = isPositiveNumber(height);
262 |
263 | if (isValidWidth && isValidHeight) {
264 | const adjustedWidth = height * aspectRatio;
265 |
266 | if (((type === 'contain' || type === 'none') && adjustedWidth > width) || (type === 'cover' && adjustedWidth < width)) {
267 | height = width / aspectRatio;
268 | } else {
269 | width = height * aspectRatio;
270 | }
271 | } else if (isValidWidth) {
272 | height = width / aspectRatio;
273 | } else if (isValidHeight) {
274 | width = height * aspectRatio;
275 | }
276 |
277 | return {
278 | width,
279 | height,
280 | };
281 | }
282 |
283 | /**
284 | * Get Exif information from the given array buffer.
285 | * @param {ArrayBuffer} arrayBuffer - The array buffer to read.
286 | * @returns {Array} The read Exif information.
287 | */
288 | export function getExif(arrayBuffer) {
289 | const array = toArray(new Uint8Array(arrayBuffer));
290 | const { length } = array;
291 | const segments = [];
292 | let start = 0;
293 |
294 | while (start + 3 < length) {
295 | const value = array[start];
296 | const next = array[start + 1];
297 |
298 | // SOS (Start of Scan)
299 | if (value === 0xFF && next === 0xDA) {
300 | break;
301 | }
302 |
303 | // SOI (Start of Image)
304 | if (value === 0xFF && next === 0xD8) {
305 | start += 2;
306 | } else {
307 | const offset = array[start + 2] * 256 + array[start + 3];
308 | const end = start + offset + 2;
309 | const segment = array.slice(start, end);
310 |
311 | segments.push(segment);
312 | start = end;
313 | }
314 | }
315 |
316 | return segments.reduce((exifArray, current) => {
317 | if (current[0] === 0xFF && current[1] === 0xE1) {
318 | return exifArray.concat(current);
319 | }
320 |
321 | return exifArray;
322 | }, []);
323 | }
324 |
325 | /**
326 | * Insert Exif information into the given array buffer.
327 | * @param {ArrayBuffer} arrayBuffer - The array buffer to transform.
328 | * @param {Array} exifArray - The Exif information to insert.
329 | * @returns {ArrayBuffer} The transformed array buffer.
330 | */
331 | export function insertExif(arrayBuffer, exifArray) {
332 | const array = toArray(new Uint8Array(arrayBuffer));
333 |
334 | if (array[2] !== 0xFF || array[3] !== 0xE0) {
335 | return arrayBuffer;
336 | }
337 |
338 | const app0Length = array[4] * 256 + array[5];
339 | const newArrayBuffer = [0xFF, 0xD8].concat(exifArray, array.slice(4 + app0Length));
340 |
341 | return new Uint8Array(newArrayBuffer);
342 | }
343 |
--------------------------------------------------------------------------------
/dist/compressor.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Compressor.js v1.2.1
3 | * https://fengyuanchen.github.io/compressorjs
4 | *
5 | * Copyright 2018-present Chen Fengyuan
6 | * Released under the MIT license
7 | *
8 | * Date: 2023-02-28T14:09:41.732Z
9 | */
10 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Compressor=t()}(this,function(){"use strict";function t(t,e){var r,i=Object.keys(t);return Object.getOwnPropertySymbols&&(r=Object.getOwnPropertySymbols(t),e&&(r=r.filter(function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable})),i.push.apply(i,r)),i}function n(i){for(var e=1;es.convertSize&&0<=s.convertTypes.indexOf(s.mimeType)&&(s.mimeType="image/jpeg"),"image/jpeg"===s.mimeType);h.fillStyle=m=U?"#fff":m,h.fillRect(0,0,y,w),s.beforeDraw&&s.beforeDraw.call(this,h,c),this.aborted||(h.save(),h.translate(y/2,w/2),h.rotate(t*Math.PI/180),h.scale(r,e),h.drawImage.apply(h,[l].concat(p)),h.restore(),s.drew&&s.drew.call(this,h,c),this.aborted)||(f=function(e){var i,t,r;n.aborted||(i=function(e){return n.done({naturalWidth:a,naturalHeight:o,result:e})},e&&U&&s.retainExif&&n.exif&&0i.size&&a.mimeType===i.type&&!(a.width>t||a.height>r||a.minWidth>t||a.minHeight>r||a.maxWidth Based on [Angular's contributing guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md).
4 |
5 | We would love for you to contribute to Compressor.js and help make it even better than it is today! As a contributor, here are the guidelines we would like you to follow:
6 |
7 | - [Contributing to Compressor.js](#contributing-to-compressorjs)
8 | - [Code of Conduct](#code-of-conduct)
9 | - [Question or Problem](#question-or-problem)
10 | - [Issues and Bugs](#issues-and-bugs)
11 | - [Feature Requests](#feature-requests)
12 | - [Submission Guidelines](#submission-guidelines)
13 | - [Submitting an Issue](#submitting-an-issue)
14 | - [Submitting a Pull Request (PR)](#submitting-a-pull-request-pr)
15 | - [After your pull request is merged](#after-your-pull-request-is-merged)
16 | - [Coding Rules](#coding-rules)
17 | - [Commit Message Guidelines](#commit-message-guidelines)
18 | - [Commit Message Format](#commit-message-format)
19 | - [Revert](#revert)
20 | - [Type](#type)
21 | - [Scope](#scope)
22 | - [Subject](#subject)
23 | - [Body](#body)
24 | - [Footer](#footer)
25 |
26 | ## Code of Conduct
27 |
28 | Help us keep Compressor.js open and inclusive. Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md).
29 |
30 | ## Question or Problem
31 |
32 | Do not open issues for general support questions as we want to keep GitHub issues for bug reports and feature requests. You've got much better chances of getting your question answered on [Stack Overflow](https://stackoverflow.com/questions/tagged/compressorjs) where the questions should be tagged with tag `compressorjs`.
33 |
34 | Stack Overflow is a much better place to ask questions since:
35 |
36 | - There are thousands of people willing to help on Stack Overflow.
37 | - Questions and answers stay available for public viewing so your question/answer might help someone else.
38 | - Stack Overflow's voting system assures that the best answers are prominently visible.
39 |
40 | To save you and our time, we will systematically close all issues that are requests for general support and redirect people to Stack Overflow.
41 |
42 | ## Issues and Bugs
43 |
44 | If you find a bug in the source code, you can help us by [submitting an issue](#submitting-an-issue) to our [GitHub Repository](https://github.com/fengyuanchen/compressorjs). Even better, you can [submit a Pull Request](#submitting-a-pull-request-pr) with a fix.
45 |
46 | ## Feature Requests
47 |
48 | You can *request* a new feature by [submitting an issue](#submitting-an-issue) to our [GitHub Repository](https://github.com/fengyuanchen/compressorjs). If you would like to *implement* a new feature, please submit an issue with a proposal for your work first, to be sure that we can use it.
49 |
50 | Please consider what kind of change it is:
51 |
52 | - For a **Major Feature**, first, open an issue and outline your proposal so that it can be discussed. This will also allow us to better coordinate our efforts, prevent duplication of work, and help you to craft the change so that it is successfully accepted into the project.
53 | - **Small Features** can be crafted and directly [submitted as a Pull Request](#submitting-a-pull-request-pr).
54 |
55 | ## Submission Guidelines
56 |
57 | ### Submitting an Issue
58 |
59 | Before you submit an issue, please search the [issue tracker](https://github.com/fengyuanchen/compressorjs/issues), which may be an issue for your problem already exists and the discussion might inform you of workarounds readily available.
60 |
61 | We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. To reproduce bugs, we will systematically ask you to provide a minimal reproduction scenario using [CodePen](https://codepen.io/pen). Having a live, reproducible scenario gives us a wealth of important information without going back & forth to you with additional questions like:
62 |
63 | - version of Compressor.js used
64 | - 3rd-party libraries and their versions
65 | - and most importantly - a use-case that fails
66 |
67 | A minimal reproduction scenario using [CodePen](https://codepen.io/pen) allows us to quickly confirm a bug (or point out a coding problem) as well as confirm that we are fixing the right problem. If [CodePen](https://codepen.io/pen) is not a suitable way to demonstrate the problem (for example for issues related to our npm packaging), please create a standalone git repository demonstrating the problem.
68 |
69 | We will be insisting on a minimal reproduction scenario to save maintainers time and ultimately be able to fix more bugs. Interestingly, from our experience users often find coding problems themselves while preparing a minimal reproduction scenario. We understand that sometimes it might be hard to extract essentials bits of code from a larger code-base but we need to isolate the problem before we can fix it.
70 |
71 | Unfortunately, we are not able to investigate/fix bugs without a minimal reproduction scenario, so if we don't hear back from you we are going to close an issue that doesn't have enough info to be reproduced.
72 |
73 | You can file new issues by filling out our [new issue form](https://github.com/fengyuanchen/compressorjs/issues/new).
74 |
75 | ### Submitting a Pull Request (PR)
76 |
77 | Before you submit your Pull Request (PR) consider the following guidelines:
78 |
79 | 1. Search [GitHub](https://github.com/fengyuanchen/compressorjs/pulls) for an open or closed PR that relates to your submission. You don't want to duplicate effort.
80 | 1. Fork the **fengyuanchen/compressorjs** repo.
81 | 1. Make your changes in a new git branch:
82 |
83 | ```shell
84 | git checkout -b my-fix-branch main
85 | ```
86 |
87 | 1. Create your patch, **including appropriate test cases**.
88 | 1. Follow our [Coding Rules](#coding-rules).
89 | 1. Run the full Compressor.js test suite, and ensure that all tests pass.
90 | 1. Commit your changes using a descriptive commit message that follows our [Commit Message Guidelines](#commit-message-guidelines). Adherence to these guidelines is necessary because release notes are automatically generated from these messages.
91 |
92 | ```shell
93 | git commit -a
94 | ```
95 |
96 | Note: the optional commit `-a` command-line option will automatically "add" and "rm" edited files.
97 | 1. Push your branch to GitHub:
98 |
99 | ```shell
100 | git push origin my-fix-branch
101 | ```
102 |
103 | 1. In GitHub, send a pull request to `compressorjs:main`.
104 | 1. If we suggest changes then:
105 | - Make the required updates.
106 | - Re-run the Compressor.js test suites to ensure tests are still passing.
107 | - Rebase your branch and force push to your GitHub repository (this will update your Pull Request):
108 |
109 | ```shell
110 | git rebase main -i
111 | git push -f
112 | ```
113 |
114 | That's it! Thank you for your contribution!
115 |
116 | #### After your pull request is merged
117 |
118 | After your pull request is merged, you can safely delete your branch and pull the changes from the main (upstream) repository:
119 |
120 | 1. Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows:
121 |
122 | ```shell
123 | git push origin --delete my-fix-branch
124 | ```
125 |
126 | 1. Check out the main branch:
127 |
128 | ```shell
129 | git checkout main -f
130 | ```
131 |
132 | 1. Delete the local branch:
133 |
134 | ```shell
135 | git branch -D my-fix-branch
136 | ```
137 |
138 | 1. Update your main with the latest upstream version:
139 |
140 | ```shell
141 | git pull --ff upstream main
142 | ```
143 |
144 | ## Coding Rules
145 |
146 | To ensure consistency throughout the source code, keep these rules in mind as you are working:
147 |
148 | - All features or bug fixes **must be tested** by one or more specs (unit-tests).
149 | - All public API methods **must be documented**.
150 | - We follow [Airbnb's JavaScript Style Guide](https://github.com/airbnb/javascript).
151 |
152 | ## Commit Message Guidelines
153 |
154 | ### Commit Message Format
155 |
156 | A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**:
157 |
158 | ```
159 | ():
160 |
161 |
162 |
163 |