├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ └── node.js.yml ├── .gitignore ├── .xo-config.json ├── LICENSE ├── README.md ├── index.d.ts ├── index.js ├── package-lock.json ├── package.json └── test ├── 80x80.gif ├── 80x80.jpg ├── 80x80.png ├── README.md ├── cat_kotatsu_neko.png ├── examples.js ├── manytimes.js ├── parallel.js ├── size.js └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | indent_size = 2 7 | end_of_line = lf 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | indent_style = space 13 | indent_size = 2 14 | trim_trailing_whitespace = false 15 | 16 | [*.json] 17 | indent_style = space 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main, dev ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '26 2 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main, dev ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [14.x, 16.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: npm ci 29 | - run: npm run build --if-present 30 | - run: npm test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | test/tmp 4 | test/_* 5 | -------------------------------------------------------------------------------- /.xo-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignores": [ 3 | "**/*.d.ts" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Hideo Matsumoto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gulp-libsquoosh 2 | 3 | Minify images with [libSquoosh](https://github.com/GoogleChromeLabs/squoosh/tree/dev/libsquoosh), the [Squoosh](https://squoosh.app/) API for Node. 4 | 5 | ## Important Notice 6 | 7 | From gulp-libsquoosh version 1.1.x, support for Node.js 12.x has been stopped because using libSquoosh 0.4.x with Node.js 12.x causes a wasm out-of-memory error. 8 | If you want to use gulp-libsquoosh with node.js 12.x, you can use version 1.0.x. This version uses libSquoosh 0.3.x. 9 | 10 | ## Install 11 | 12 | ``` 13 | $ npm install --save-dev gulp-libsquoosh 14 | ``` 15 | 16 | ## Usage 17 | 18 | Detailed descriptions about options can be found in [libSquoosh README](https://github.com/GoogleChromeLabs/squoosh/tree/dev/libsquoosh). 19 | 20 | ### Basic 21 | 22 | ```js 23 | const { src, dest, series } = require('gulp'); 24 | const squoosh = require('gulp-libsquoosh'); 25 | 26 | // minify images into same format 27 | function images() { 28 | return src('src/images/**') 29 | .pipe(squoosh()) 30 | .pipe(dest('dist/images')); 31 | } 32 | 33 | exports.default = series(images); 34 | ``` 35 | 36 | ### Convert to multiple image formats 37 | 38 | ```js 39 | const { src, dest, series } = require('gulp'); 40 | const squoosh = require('gulp-libsquoosh'); 41 | 42 | // minify png into png, webp and avif format 43 | function images() { 44 | return src('src/images/**/*.png') 45 | .pipe( 46 | squoosh({ 47 | oxipng: {}, 48 | webp: {}, 49 | avif: {}, 50 | }) 51 | ) 52 | .pipe(dest('dist/images')); 53 | } 54 | 55 | exports.default = series(images); 56 | ``` 57 | 58 | ### Conversion with watching files 59 | 60 | It is useful to convert PNG files to multiple formats with `watch()` API. 61 | 62 | ```js 63 | const { src, dest, watch } = require('gulp'); 64 | const squoosh = require('gulp-libsquoosh'); 65 | 66 | // when png file dropped into images/** ... 67 | function watchTask() { 68 | watch('images/**/*.png', images); 69 | } 70 | 71 | // ...minify png into png, webp and avif format 72 | function images() { 73 | return src('images/**/*.png') 74 | .pipe( 75 | squoosh({ 76 | oxipng: {}, 77 | webp: {}, 78 | avif: {}, 79 | }) 80 | ) 81 | .pipe(dest('dist/images')); 82 | } 83 | 84 | exports.watch = watchTask; 85 | ``` 86 | 87 | You can specify each filename with `` in `` tag. 88 | 89 | ```html 90 | 91 | 92 | 93 | logo 94 | 95 | ``` 96 | 97 | ### Resizing image 98 | 99 | ```js 100 | const { src, dest, series } = require('gulp'); 101 | const squoosh = require('gulp-libsquoosh'); 102 | 103 | // resize image to width 200px with keeping aspect ratio. 104 | function images() { 105 | return src('src/thumbnail/*.png') 106 | .pipe( 107 | squoosh( 108 | null, // use default 109 | { 110 | resize: { 111 | // specify either width or height 112 | // when you specify width and height, image resized to exact size you specified 113 | width: 200, 114 | }, 115 | } 116 | ) 117 | ) 118 | .pipe(dest('dist/thumbnail')); 119 | } 120 | 121 | exports.default = series(images); 122 | ``` 123 | 124 | ### Specify encodeOptions, preprocessOptions in one object argument. 125 | 126 | ```js 127 | const { src, dest, series } = require('gulp'); 128 | const squoosh = require('gulp-libsquoosh'); 129 | 130 | // squoosh({encodeOptions:..., preprocessOptions:...}) 131 | function images() { 132 | return src('src/images/**') 133 | .pipe( 134 | squoosh({ 135 | encodeOptions: { 136 | avif: {}, 137 | webp: {}, 138 | }, 139 | preprocessOptions: { 140 | rotate: { 141 | numRotations: 2, 142 | }, 143 | }, 144 | }) 145 | ) 146 | .pipe(dest('dist/images')); 147 | } 148 | 149 | exports.default = series(images); 150 | ``` 151 | 152 | ### Resize using original image size 153 | 154 | ```js 155 | const { src, dest, series } = require('gulp'); 156 | const squoosh = require('gulp-libsquoosh'); 157 | 158 | // resize image to half size of original. 159 | function images() { 160 | return src('src/thumbnail/*.png') 161 | .pipe( 162 | squoosh((src) => ({ 163 | preprocessOptions: { 164 | resize: { 165 | width: Math.round(src.width / 2), 166 | height: Math.round(src.height / 2), 167 | }, 168 | }, 169 | })) 170 | ) 171 | .pipe(dest('dist/thumbnail')); 172 | } 173 | 174 | exports.default = series(images); 175 | ``` 176 | 177 | You can use some helper functions. It acts like as "object-fit" CSS property. 178 | 179 | - `contain(width, [height])` 180 | - `scaleDown(width, [height])` 181 | 182 | ```js 183 | const { src, dest, series } = require('gulp'); 184 | const squoosh = require('gulp-libsquoosh'); 185 | 186 | // resize image to fit inside of 200x200 box. 187 | function images() { 188 | return src('src/thumbnail/*.png') 189 | .pipe( 190 | squoosh((src) => ({ 191 | preprocessOptions: { 192 | resize: { 193 | ...src.contain(200), 194 | }, 195 | }, 196 | })) 197 | ) 198 | .pipe(dest('dist/thumbnail')); 199 | } 200 | 201 | exports.default = series(images); 202 | ``` 203 | 204 | ### Quantize, Rotate image 205 | 206 | ```js 207 | const { src, dest, series } = require('gulp'); 208 | const squoosh = require('gulp-libsquoosh'); 209 | 210 | // quantize, rotate and minify png into png, webp and avif format 211 | function images() { 212 | return src('src/images/**/*.png') 213 | .pipe( 214 | squoosh( 215 | { 216 | oxipng: { 217 | level: 6, // slower but more compression 218 | }, 219 | webp: {}, 220 | avif: {}, 221 | }, 222 | { 223 | // quantize images 224 | quant: { 225 | numColors: 128, // default=255 226 | }, 227 | // rotate images 228 | rotate: { 229 | numRotations: 1, // (numRotations * 90) degrees 230 | }, 231 | } 232 | ) 233 | ) 234 | .pipe(dest('dist/images')); 235 | } 236 | 237 | exports.default = series(images); 238 | ``` 239 | 240 | ### More complex 241 | 242 | ```js 243 | const path = require('path'); 244 | const { src, dest, series } = require('gulp'); 245 | const squoosh = require('gulp-libsquoosh'); 246 | 247 | function images() { 248 | return src(['src/images/**/*.{png,jpg,webp}']) 249 | .pipe( 250 | squoosh((src) => { 251 | const extname = path.extname(src.path); 252 | let options = { 253 | encodeOptions: squoosh.DefaultEncodeOptions[extname], 254 | }; 255 | 256 | if (extname === '.jpg') { 257 | options = { 258 | encodeOptions: { 259 | jxl: {}, 260 | mozjpeg: {}, 261 | }, 262 | }; 263 | } 264 | 265 | if (extname === '.png') { 266 | options = { 267 | encodeOptions: { 268 | avif: {}, 269 | }, 270 | preprocessOptions: { 271 | quant: { 272 | enabled: true, 273 | numColors: 16, 274 | }, 275 | }, 276 | }; 277 | } 278 | 279 | return options; 280 | }) 281 | ) 282 | .pipe(dest('dist/images')); 283 | } 284 | 285 | exports.default = series(images); 286 | ``` 287 | 288 | ## API 289 | 290 | ### squoosh(encodeOptions?, preprocessOptions?) 291 | 292 | Description of the options can be found in [libSquoosh README](https://github.com/GoogleChromeLabs/squoosh/tree/dev/libsquoosh#preprocessing-and-encoding-images). 293 | 294 | ## License 295 | 296 | MIT License 297 | 298 | Copyright (c) 2021-2022 Hideo Matsumoto 299 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export = squoosh; 2 | /** 3 | * @typedef { import('vinyl') } File 4 | */ 5 | /** 6 | * @typedef {Object} BoxSize 7 | * @property {number} width 8 | * @property {number} height 9 | */ 10 | /** 11 | * @typedef {Object} SquooshOptions 12 | * @property {EncodeOptions} encodeOptions 13 | * @property {PreprocessOptions} preprocessOptions 14 | */ 15 | /** 16 | * @callback SquooshCallback 17 | * @param {ImageSize} imageSize 18 | * @returns {BoxSize} 19 | */ 20 | /** 21 | * @typedef {Object} EncodeOptions 22 | * @property {Object} [mozjpeg] 23 | * @property {Object} [webp] 24 | * @property {Object} [avif] 25 | * @property {Object} [jxl] 26 | * @property {Object} [wp2] 27 | * @property {Object} [oxipng] 28 | */ 29 | /** 30 | * @typedef {Object} PreprocessOptions 31 | * @property {Object} [resize] 32 | * @property {Object} [quant] 33 | * @property {Object} [rotate] 34 | */ 35 | /** 36 | * Minify images with libSquoosh. 37 | * @param {(EncodeOptions|SquooshOptions|SquooshCallback)} [encodeOptions] - An object with encoders to use, and their settings. 38 | * @param {Object} [PreprocessOptions] - An object with preprocessors to use, and their settings. 39 | * @returns {NodeJS.ReadWriteStream} 40 | */ 41 | declare function squoosh(encodeOptions?: (EncodeOptions | SquooshOptions | SquooshCallback), preprocessOptions: any): NodeJS.ReadWriteStream; 42 | declare namespace squoosh { 43 | export { DefaultEncodeOptions, ImageSize, File, BoxSize, SquooshOptions, SquooshCallback, EncodeOptions, PreprocessOptions }; 44 | } 45 | type EncodeOptions = { 46 | mozjpeg?: any; 47 | webp?: any; 48 | avif?: any; 49 | jxl?: any; 50 | wp2?: any; 51 | oxipng?: any; 52 | }; 53 | type SquooshOptions = { 54 | encodeOptions: EncodeOptions; 55 | preprocessOptions: PreprocessOptions; 56 | }; 57 | type SquooshCallback = (imageSize: ImageSize) => BoxSize; 58 | /** 59 | * : Object} 60 | */ 61 | type DefaultEncodeOptions = [extension: string]; 62 | /** 63 | * By default, encode to same image type. 64 | * @typedef {[extension:string]: Object} 65 | */ 66 | declare const DefaultEncodeOptions: { 67 | [k: string]: any; 68 | }; 69 | /** 70 | * @class 71 | * @param {Object} bitmap 72 | * @param {string} path - The full path to the file. 73 | */ 74 | declare function ImageSize({ bitmap }: any, path: string): void; 75 | declare class ImageSize { 76 | /** 77 | * @class 78 | * @param {Object} bitmap 79 | * @param {string} path - The full path to the file. 80 | */ 81 | constructor({ bitmap }: any, path: string); 82 | /** @type {number} */ 83 | width: number; 84 | /** @type {number} */ 85 | height: number; 86 | path: string; 87 | /** 88 | * Scale to keep its aspect ratio while fitting within the specified bounding box. 89 | * @param {number} targetWidth 90 | * @param {number} [targetHeight] 91 | * @returns {BoxSize} 92 | */ 93 | contain(targetWidth: number, targetHeight?: number): BoxSize; 94 | /** 95 | * Acts like contain() but don't zoom if image is smaller than the specified bounding box. 96 | * @param {number} targetWidth 97 | * @param {number} [targetHeight] 98 | * @returns {BoxSize} 99 | */ 100 | scaleDown(targetWidth: number, targetHeight?: number): BoxSize; 101 | /** 102 | * Scale to keep its aspect ratio while filling the specified bounding box. 103 | * This method is not usable because libSquoosh doesn't provide crop functionality. 104 | * @param {number} targetWidth 105 | * @param {number} [targetHeight] 106 | * @returns {BoxSize} 107 | */ 108 | cover(targetWidth: number, targetHeight?: number): BoxSize; 109 | } 110 | type File = import('vinyl'); 111 | type BoxSize = { 112 | width: number; 113 | height: number; 114 | }; 115 | type PreprocessOptions = { 116 | resize?: any; 117 | quant?: any; 118 | rotate?: any; 119 | }; 120 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const os = require('os'); 4 | const through = require('through2'); 5 | const PluginError = require('plugin-error'); 6 | const libSquoosh = require('@squoosh/lib'); 7 | const debounce = require('lodash.debounce'); 8 | 9 | const PLUGIN_NAME = 'gulp-libsquoosh'; 10 | 11 | // How many parallel operations allowed while processing ImagePool. 12 | const NUM_PARALLEL = os.cpus().length > 1 ? 2 : 1; 13 | 14 | // Reuse ImagePool every 5 images. 15 | const REUSE_IMAGEPOOL = 5; 16 | 17 | /** @type {libSquoosh.ImagePool} */ 18 | let imagePool; 19 | 20 | const queue = []; 21 | let running = 0; 22 | let processed = 0; 23 | 24 | /** 25 | * By default, encode to same image type. 26 | * @typedef {[extension:string]: Object} 27 | */ 28 | const DefaultEncodeOptions = Object.fromEntries( 29 | Object.entries(libSquoosh.encoders).map(([key, encoder]) => { 30 | const extension = `.${encoder.extension}`; 31 | return [extension, Object.fromEntries([[key, {}]])]; 32 | }) 33 | ); 34 | 35 | /** 36 | * @typedef { import('vinyl') } File 37 | */ 38 | /** 39 | * @typedef {Object} BoxSize 40 | * @property {number} width 41 | * @property {number} height 42 | */ 43 | /** 44 | * @typedef {Object} SquooshOptions 45 | * @property {EncodeOptions} encodeOptions 46 | * @property {PreprocessOptions} preprocessOptions 47 | */ 48 | /** 49 | * @callback SquooshCallback 50 | * @param {ImageSize} imageSize 51 | * @returns {BoxSize} 52 | */ 53 | 54 | /* The following two options are as of libSquoosh's commit #955b2ac. */ 55 | /** 56 | * @typedef {Object} EncodeOptions 57 | * @property {Object} [mozjpeg] 58 | * @property {Object} [webp] 59 | * @property {Object} [avif] 60 | * @property {Object} [jxl] 61 | * @property {Object} [wp2] 62 | * @property {Object} [oxipng] 63 | */ 64 | /** 65 | * @typedef {Object} PreprocessOptions 66 | * @property {Object} [resize] 67 | * @property {Object} [quant] 68 | * @property {Object} [rotate] 69 | */ 70 | 71 | /** 72 | * Close ImagePool instance when idle. 73 | * If you don't close imagePool when idle, gulp should hang. 74 | */ 75 | const closeImagePoolWhenIdle = debounce(() => { 76 | (async () => { 77 | if (imagePool) { 78 | await imagePool.close(); 79 | imagePool = null; 80 | } 81 | })(); 82 | }, 500); 83 | 84 | /** 85 | * Minify images with libSquoosh. 86 | * @param {(EncodeOptions|SquooshOptions|SquooshCallback)} [encodeOptions] - An object with encoders to use, and their settings. 87 | * @param {Object} [PreprocessOptions] - An object with preprocessors to use, and their settings. 88 | * @returns {NodeJS.ReadWriteStream} 89 | */ 90 | function squoosh(encodeOptions, preprocessOptions) { 91 | if (typeof encodeOptions === 'object' && typeof preprocessOptions === 'undefined') { 92 | if (typeof encodeOptions.preprocessOptions !== 'undefined') { 93 | preprocessOptions = encodeOptions.preprocessOptions; 94 | delete encodeOptions.preprocessOptions; 95 | } 96 | 97 | if (typeof encodeOptions.encodeOptions !== 'undefined') { 98 | encodeOptions = encodeOptions.encodeOptions; 99 | } 100 | } 101 | 102 | /** 103 | * @param {File} file 104 | * @returns {File[]} 105 | */ 106 | const encode = async function (file, encodeOptions, preprocessOptions) { 107 | closeImagePoolWhenIdle.cancel(); // Stop debounce timer 108 | 109 | if (!imagePool) { 110 | imagePool = new libSquoosh.ImagePool(NUM_PARALLEL); 111 | } 112 | 113 | let currentEncodeOptions = encodeOptions; 114 | let currentPreprocessOptions = preprocessOptions; 115 | 116 | const image = imagePool.ingestImage(file.contents); 117 | const decoded = await image.decoded; 118 | 119 | if (typeof encodeOptions === 'function') { 120 | /** @type {SquooshCallback} */ 121 | const callback = encodeOptions; 122 | const result = callback(new ImageSize(decoded, file.path)); 123 | currentEncodeOptions = result.encodeOptions || null; 124 | currentPreprocessOptions = result.preprocessOptions || null; 125 | } 126 | 127 | currentEncodeOptions = (currentEncodeOptions && Object.keys(currentEncodeOptions).length > 0) ? currentEncodeOptions : DefaultEncodeOptions[file.extname]; 128 | 129 | if (currentPreprocessOptions) { 130 | await image.preprocess(currentPreprocessOptions); 131 | } 132 | 133 | await image.encode(currentEncodeOptions); 134 | 135 | const encodedFiles = []; 136 | const tasks = Object.values(image.encodedWith).map(async encoder => { 137 | const encodedImage = await encoder; 138 | const newfile = file.clone({contents: false}); 139 | newfile.contents = Buffer.from(encodedImage.binary); 140 | newfile.extname = `.${encodedImage.extension}`; 141 | encodedFiles.push(newfile); 142 | }); 143 | await Promise.all(tasks); 144 | 145 | processed++; 146 | if (processed >= REUSE_IMAGEPOOL) { 147 | await imagePool.close(); 148 | imagePool = null; 149 | processed = 0; 150 | } 151 | 152 | closeImagePoolWhenIdle(); 153 | 154 | return encodedFiles; 155 | }; 156 | 157 | /** 158 | * @type { import('through2').TransformFunction } 159 | * @param {File} file 160 | */ 161 | const transform = async function (file, enc, cb) { 162 | if (file.isNull()) { 163 | cb(null, file); 164 | return; 165 | } 166 | 167 | if (file.isStream()) { 168 | cb(new PluginError(PLUGIN_NAME, 'Streaming not supported')); 169 | return; 170 | } 171 | 172 | // Is file supported by libsquoosh? 173 | if (!Object.keys(DefaultEncodeOptions).includes(file.extname)) { 174 | cb(null, file); 175 | return; 176 | } 177 | 178 | queue.push([this, file, encodeOptions, preprocessOptions, cb]); 179 | 180 | if (running < 1) { 181 | running++; 182 | for (let args; (args = queue.shift());) { 183 | const [self, file, encodeOptions, preprocessOptions, cb] = args; 184 | try { 185 | const encoded = await encode(file, encodeOptions, preprocessOptions); // eslint-disable-line no-await-in-loop 186 | for (const f of encoded) { 187 | self.push(f); 188 | } 189 | 190 | cb(); 191 | } catch (error) { 192 | cb(new PluginError(PLUGIN_NAME, error, {filename: file.path})); 193 | } 194 | } 195 | 196 | running--; 197 | } 198 | }; 199 | 200 | return through.obj(transform); 201 | } 202 | 203 | /** 204 | * @class 205 | * @param {Object} bitmap 206 | * @param {string} path - The full path to the file. 207 | */ 208 | function ImageSize({bitmap}, path) { 209 | /** @type {number} */ 210 | this.width = bitmap.width; 211 | 212 | /** @type {number} */ 213 | this.height = bitmap.height; 214 | 215 | this.path = path; 216 | } 217 | 218 | /** 219 | * Scale to keep its aspect ratio while fitting within the specified bounding box. 220 | * @param {number} targetWidth 221 | * @param {number} [targetHeight] 222 | * @returns {BoxSize} 223 | */ 224 | ImageSize.prototype.contain = function (targetWidth, targetHeight) { 225 | if (typeof targetHeight === 'undefined') { 226 | targetHeight = targetWidth; 227 | } 228 | 229 | const {width, height} = this; 230 | 231 | const scaleW = targetWidth / width; 232 | const scaleH = targetHeight / height; 233 | const scale = (scaleW > scaleH) ? scaleH : scaleW; 234 | 235 | return { 236 | width: Math.round(width * scale), 237 | height: Math.round(height * scale) 238 | }; 239 | }; 240 | 241 | /** 242 | * Acts like contain() but don't zoom if image is smaller than the specified bounding box. 243 | * @param {number} targetWidth 244 | * @param {number} [targetHeight] 245 | * @returns {BoxSize} 246 | */ 247 | ImageSize.prototype.scaleDown = function (targetWidth, targetHeight) { 248 | if (typeof targetHeight === 'undefined') { 249 | targetHeight = targetWidth; 250 | } 251 | 252 | const {width, height} = this; 253 | 254 | if (targetWidth > width && targetHeight > height) { 255 | return {width, height}; 256 | } 257 | 258 | return this.contain(targetWidth, targetHeight); 259 | }; 260 | 261 | /** 262 | * Scale to keep its aspect ratio while filling the specified bounding box. 263 | * This method is not usable because libSquoosh doesn't provide crop functionality. 264 | * @param {number} targetWidth 265 | * @param {number} [targetHeight] 266 | * @returns {BoxSize} 267 | */ 268 | ImageSize.prototype.cover = function (targetWidth, targetHeight) { 269 | if (typeof targetHeight === 'undefined') { 270 | targetHeight = targetWidth; 271 | } 272 | 273 | const {width, height} = this; 274 | 275 | const scaleW = targetWidth / width; 276 | const scaleH = targetHeight / height; 277 | const scale = (scaleW > scaleH) ? scaleW : scaleH; 278 | 279 | return { 280 | width: Math.round(width * scale), 281 | height: Math.round(height * scale) 282 | }; 283 | }; 284 | 285 | squoosh.DefaultEncodeOptions = DefaultEncodeOptions; 286 | squoosh.ImageSize = ImageSize; 287 | 288 | module.exports = squoosh; 289 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-libsquoosh", 3 | "version": "1.1.2", 4 | "description": "Minify images with libSquoosh, the Squoosh API for Node.", 5 | "public": true, 6 | "main": "index.js", 7 | "files": [ 8 | "index.js", 9 | "index.d.ts", 10 | "LICENSE" 11 | ], 12 | "scripts": { 13 | "test": "xo && ava --verbose --no-worker-threads --timeout 5m", 14 | "types": "npx tsc index.js --declaration --allowJs --emitDeclarationOnly --outDir ." 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/pekeq/gulp-libsquoosh.git" 19 | }, 20 | "keywords": [ 21 | "gulp-libsquoosh", 22 | "gulp", 23 | "libsquoosh", 24 | "squoosh", 25 | "gulpplugin", 26 | "gulp-plugin", 27 | "avif", 28 | "jpeg-xl", 29 | "jpegxl", 30 | "mozjpeg", 31 | "oxipng", 32 | "webp", 33 | "webp2", 34 | "wp2" 35 | ], 36 | "author": "Hideo Matsumoto ", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/pekeq/gulp-libsquoosh/issues" 40 | }, 41 | "engines": { 42 | "node": " ^14.0.0 || ^16.0.0 " 43 | }, 44 | "dependencies": { 45 | "@squoosh/lib": "^0.4.0", 46 | "lodash.debounce": "^4.0.8", 47 | "plugin-error": "^2.0.1", 48 | "through2": "^4.0.2" 49 | }, 50 | "devDependencies": { 51 | "@types/through2": "^2.0.36", 52 | "@types/vinyl": "^2.0.6", 53 | "ava": "^5.1.0", 54 | "del": "^6.0.0", 55 | "gulp": "^4.0.2", 56 | "xo": "^0.39.1" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/80x80.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pekeq/gulp-libsquoosh/61e146d805f2db5c42c28dac69432e401772e9aa/test/80x80.gif -------------------------------------------------------------------------------- /test/80x80.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pekeq/gulp-libsquoosh/61e146d805f2db5c42c28dac69432e401772e9aa/test/80x80.jpg -------------------------------------------------------------------------------- /test/80x80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pekeq/gulp-libsquoosh/61e146d805f2db5c42c28dac69432e401772e9aa/test/80x80.png -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | ## About images 2 | 3 | ### [cat_kotatsu_neko.png](https://www.irasutoya.com/2021/06/blog-post_24.html) 4 | 5 | "cat_kotatsu_neko.png" is quoted work from [Irasutoya](https://www.irasutoya.com/). 6 | 7 | Copyright © いらすとや. All Rights Reserved. 8 | -------------------------------------------------------------------------------- /test/cat_kotatsu_neko.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pekeq/gulp-libsquoosh/61e146d805f2db5c42c28dac69432e401772e9aa/test/cat_kotatsu_neko.png -------------------------------------------------------------------------------- /test/examples.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable comma-dangle,arrow-parens,object-curly-spacing,capitalized-comments */ 2 | 3 | 'use strict'; 4 | 5 | const test = require('ava'); 6 | const fs = require('fs'); 7 | const del = require('del'); 8 | 9 | const basedir = '_examples'; 10 | const srcdir = `${basedir}/src`; 11 | const dstdir = `${basedir}/dst`; 12 | 13 | test.before(() => { 14 | process.chdir(__dirname); 15 | fs.mkdirSync(`${srcdir}/images`, { recursive: true }); 16 | fs.mkdirSync(`${srcdir}/thumbnail`, { recursive: true }); 17 | fs.copyFileSync('80x80.png', `${srcdir}/images/test1.png`); 18 | fs.copyFileSync('80x80.jpg', `${srcdir}/images/test2.jpg`); 19 | fs.copyFileSync('80x80.png', `${srcdir}/thumbnail/test1.png`); 20 | fs.copyFileSync('80x80.jpg', `${srcdir}/thumbnail/test2.png`); 21 | }); 22 | 23 | test.after(async () => { 24 | await del(basedir); 25 | }); 26 | 27 | test.afterEach(async () => { 28 | await del(dstdir); 29 | }); 30 | 31 | function exists(file) { 32 | return fs.existsSync(file); 33 | } 34 | 35 | test.serial('Basic', t => { 36 | return new Promise((resolve, reject) => { 37 | const { src, dest, series } = require('gulp'); 38 | const squoosh = require('..'); 39 | 40 | // minify images into same format 41 | function images() { 42 | return src(`${srcdir}/images/**`) 43 | .pipe(squoosh()) 44 | .pipe(dest(`${dstdir}/images`)); 45 | } 46 | 47 | series(images)((error) => { 48 | if (error) { 49 | reject(error); 50 | } 51 | 52 | t.true(exists(`${dstdir}/images/test1.png`)); 53 | t.true(exists(`${dstdir}/images/test2.jpg`)); 54 | resolve(); 55 | }); 56 | }); 57 | }); 58 | 59 | test.serial('Convert to multiple image formats', t => { 60 | return new Promise((resolve, reject) => { 61 | const { src, dest, series } = require('gulp'); 62 | const squoosh = require('..'); 63 | 64 | // minify png into png, webp and avif format 65 | function images() { 66 | return src(`${srcdir}/images/**/*.png`) 67 | .pipe( 68 | squoosh({ 69 | oxipng: {}, 70 | webp: {}, 71 | avif: {}, 72 | }) 73 | ) 74 | .pipe(dest(`${dstdir}/images`)); 75 | } 76 | 77 | series(images)((error) => { 78 | if (error) { 79 | reject(error); 80 | } 81 | 82 | t.true(exists(`${dstdir}/images/test1.png`)); 83 | t.true(exists(`${dstdir}/images/test1.webp`)); 84 | t.true(exists(`${dstdir}/images/test1.avif`)); 85 | resolve(); 86 | }); 87 | }); 88 | }); 89 | 90 | test.serial('Resizing image', t => { 91 | return new Promise((resolve, reject) => { 92 | const { src, dest, series } = require('gulp'); 93 | const squoosh = require('..'); 94 | 95 | // resize image to width 200px with keeping aspect ratio. 96 | function images() { 97 | return src(`${srcdir}/thumbnail/*.png`) 98 | .pipe( 99 | squoosh( 100 | null, // use default 101 | { 102 | resize: { 103 | enabled: true, 104 | // specify either width or height 105 | // when you specify width and height, image resized to exact size you specified 106 | width: 200, 107 | }, 108 | } 109 | ) 110 | ) 111 | .pipe(dest(`${dstdir}/thumbnail`)); 112 | } 113 | 114 | series(images)((error) => { 115 | if (error) { 116 | reject(error); 117 | } 118 | 119 | t.true(exists(`${dstdir}/thumbnail/test1.png`)); 120 | resolve(); 121 | }); 122 | }); 123 | }); 124 | 125 | test.serial('Specify encodeOptions, preprocessOptions in one object argument.', t => { 126 | return new Promise((resolve, reject) => { 127 | const { src, dest, series } = require('gulp'); 128 | const squoosh = require('..'); 129 | 130 | // squoosh({encodeOptions:..., preprocessOptions:...}) 131 | function images() { 132 | return src(`${srcdir}/images/**`) 133 | .pipe( 134 | squoosh({ 135 | encodeOptions: { 136 | avif: {}, 137 | webp: {}, 138 | }, 139 | preprocessOptions: { 140 | rotate: { 141 | enabled: true, 142 | numRotations: 2, 143 | }, 144 | }, 145 | }) 146 | ) 147 | .pipe(dest(`${dstdir}/images`)); 148 | } 149 | 150 | series(images)((error) => { 151 | if (error) { 152 | reject(error); 153 | } 154 | 155 | t.true(exists(`${dstdir}/images/test1.avif`)); 156 | t.true(exists(`${dstdir}/images/test1.webp`)); 157 | t.false(exists(`${dstdir}/images/test1.png`)); 158 | resolve(); 159 | }); 160 | }); 161 | }); 162 | 163 | test.serial('Resize using original image size', t => { 164 | return new Promise((resolve, reject) => { 165 | const { src, dest, series } = require('gulp'); 166 | const squoosh = require('..'); 167 | 168 | // resize image to half size of original. 169 | function images() { 170 | return src(`${srcdir}/thumbnail/*.png`) 171 | .pipe( 172 | squoosh((src) => ({ 173 | preprocessOptions: { 174 | resize: { 175 | enabled: true, 176 | width: Math.round(src.width / 2), 177 | height: Math.round(src.height / 2), 178 | }, 179 | }, 180 | })) 181 | ) 182 | .pipe(dest(`${dstdir}/thumbnail`)); 183 | } 184 | 185 | series(images)((error) => { 186 | if (error) { 187 | reject(error); 188 | } 189 | 190 | t.true(exists(`${dstdir}/thumbnail/test1.png`)); 191 | resolve(); 192 | }); 193 | }); 194 | }); 195 | 196 | test.serial('Resize using original image size (with helper function)', t => { 197 | return new Promise((resolve, reject) => { 198 | const { src, dest, series } = require('gulp'); 199 | const squoosh = require('..'); 200 | 201 | // resize image to fit inside of 200x200 box. 202 | function images() { 203 | return src(`${srcdir}/thumbnail/*.png`) 204 | .pipe( 205 | squoosh((src) => ({ 206 | preprocessOptions: { 207 | resize: { 208 | enabled: true, 209 | ...src.contain(200), 210 | }, 211 | }, 212 | })) 213 | ) 214 | .pipe(dest(`${dstdir}/thumbnail`)); 215 | } 216 | 217 | series(images)((error) => { 218 | if (error) { 219 | reject(error); 220 | } 221 | 222 | t.true(exists(`${dstdir}/thumbnail/test1.png`)); 223 | resolve(); 224 | }); 225 | }); 226 | }); 227 | 228 | test.serial('Quantize, Rotate image', t => { 229 | return new Promise((resolve, reject) => { 230 | const { src, dest, series } = require('gulp'); 231 | const squoosh = require('..'); 232 | 233 | // quantize, rotate and minify png into png, webp and avif format 234 | function images() { 235 | return src(`${srcdir}/images/**/*.png`) 236 | .pipe( 237 | squoosh( 238 | { 239 | oxipng: { 240 | level: 6, // slower but more compression 241 | }, 242 | webp: {}, 243 | avif: {}, 244 | }, 245 | { 246 | // quantize images 247 | quant: { 248 | enabled: true, 249 | numColors: 128, // default=255 250 | }, 251 | // rotate images 252 | rotate: { 253 | enabled: true, 254 | numRotations: 1, // (numRotations * 90) degrees 255 | }, 256 | } 257 | ) 258 | ) 259 | .pipe(dest(`${dstdir}/images`)); 260 | } 261 | 262 | series(images)((error) => { 263 | if (error) { 264 | reject(error); 265 | } 266 | 267 | t.true(exists(`${dstdir}/images/test1.png`)); 268 | resolve(); 269 | }); 270 | }); 271 | }); 272 | 273 | test.serial('More complex', t => { 274 | return new Promise((resolve, reject) => { 275 | const path = require('path'); 276 | const { src, dest, series } = require('gulp'); 277 | const squoosh = require('..'); 278 | 279 | function images() { 280 | return src([`${srcdir}/images/**/*.{png,jpg,webp}`]) 281 | .pipe( 282 | squoosh((src) => { 283 | const extname = path.extname(src.path); 284 | let options = { 285 | encodeOptions: squoosh.DefaultEncodeOptions[extname], 286 | }; 287 | 288 | if (extname === '.jpg') { 289 | options = { 290 | encodeOptions: { 291 | jxl: {}, 292 | mozjpeg: {}, 293 | }, 294 | }; 295 | } 296 | 297 | if (extname === '.png') { 298 | options = { 299 | encodeOptions: { 300 | avif: {}, 301 | }, 302 | preprocessOptions: { 303 | quant: { 304 | enabled: true, 305 | numColors: 16, 306 | }, 307 | }, 308 | }; 309 | } 310 | 311 | return options; 312 | }) 313 | ) 314 | .pipe(dest(`${dstdir}/images`)); 315 | } 316 | 317 | series(images)(error => { 318 | if (error) { 319 | reject(error); 320 | } 321 | 322 | t.true(exists(`${dstdir}/images/test1.avif`)); 323 | t.true(exists(`${dstdir}/images/test2.jpg`)); 324 | t.true(exists(`${dstdir}/images/test2.jxl`)); 325 | resolve(); 326 | }); 327 | }); 328 | }); 329 | -------------------------------------------------------------------------------- /test/manytimes.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | 'use strict'; 4 | 5 | const fs = require('fs'); 6 | const gulp = require('gulp'); 7 | const test = require('ava'); 8 | const del = require('del'); 9 | const squoosh = require('..'); 10 | 11 | const dirname = __dirname; 12 | const dstdir = '_manytimes'; 13 | 14 | test.before(async t => { 15 | process.chdir(dirname); 16 | await del(dstdir); 17 | }); 18 | 19 | test.after(async t => { 20 | await del(dstdir); 21 | }); 22 | 23 | test.afterEach(async t => { 24 | await del(dstdir); 25 | }); 26 | 27 | test.serial('run task many times', async t => { 28 | function image() { 29 | return gulp.src('80x80.png') 30 | .pipe(squoosh({ 31 | encodeOptions: { 32 | avif: {}, 33 | webp: {} 34 | }, 35 | preprocessOptions: { 36 | resize: { 37 | enabled: true, 38 | width: 40 39 | } 40 | } 41 | })) 42 | .pipe(gulp.dest(dstdir)); 43 | } 44 | 45 | for (let i = 0; i < 50; i++) { 46 | console.log(`manytimes: iter #${i}`); 47 | // eslint-disable-next-line no-await-in-loop 48 | await (new Promise((resolve, reject) => { 49 | gulp.series(image)(error => { 50 | if (error) { 51 | console.log('error:', error); 52 | reject(error); 53 | return; 54 | } 55 | 56 | t.true(fs.existsSync(`${dstdir}/80x80.webp`)); 57 | t.true(fs.existsSync(`${dstdir}/80x80.avif`)); 58 | resolve(); 59 | }); 60 | })); 61 | } 62 | }); 63 | -------------------------------------------------------------------------------- /test/parallel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | 'use strict'; 4 | 5 | const fs = require('fs'); 6 | const gulp = require('gulp'); 7 | const test = require('ava'); 8 | const del = require('del'); 9 | const squoosh = require('..'); 10 | 11 | const dirname = __dirname; 12 | const basedir = '_parallel'; 13 | 14 | test.before(t => { 15 | process.chdir(dirname); 16 | del.sync(basedir); 17 | fs.mkdirSync(basedir); 18 | fs.copyFileSync('80x80.jpg', `${basedir}/test1.jpg`); 19 | fs.copyFileSync('80x80.png', `${basedir}/test2.png`); 20 | }); 21 | 22 | test.after(t => { 23 | del.sync(basedir); 24 | }); 25 | 26 | test.serial('run parallel to check "out of memory" error', t => { 27 | return new Promise((resolve, reject) => { 28 | let job = 1; 29 | function images() { 30 | const thisjob = job++; 31 | return gulp.src([`${basedir}/test1.jpg`, `${basedir}/test2.png`]) 32 | .pipe(squoosh( 33 | {mozjpeg: {}, webp: {}, oxipng: {}, avif: {}}, 34 | { 35 | resize: { 36 | enabled: true, 37 | // Specify either width or height 38 | // When you specify width and height, image resized to exact size you specified 39 | width: 20 40 | } 41 | } 42 | )) 43 | .pipe(gulp.dest(`${basedir}/${thisjob}`)); 44 | } 45 | 46 | gulp.parallel( 47 | images, images, images, images, images, images, images, images, images, images, 48 | images, images, images, images, images, images, images, images, images, images 49 | )(error => { 50 | if (error) { 51 | reject(error); 52 | } 53 | 54 | t.true(fs.existsSync(`${basedir}/20/test1.jpg`)); 55 | t.true(fs.existsSync(`${basedir}/20/test1.png`)); 56 | t.true(fs.existsSync(`${basedir}/20/test1.webp`)); 57 | t.true(fs.existsSync(`${basedir}/20/test1.avif`)); 58 | t.true(fs.existsSync(`${basedir}/20/test2.jpg`)); 59 | t.true(fs.existsSync(`${basedir}/20/test2.png`)); 60 | t.true(fs.existsSync(`${basedir}/20/test2.webp`)); 61 | t.true(fs.existsSync(`${basedir}/20/test2.avif`)); 62 | resolve(); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/size.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const squoosh = require('..'); 5 | 6 | test('contain 1', t => { 7 | const image = new squoosh.ImageSize({bitmap: {width: 200, height: 200}}, '/path/to/file.jpg'); 8 | let size; 9 | 10 | size = image.contain(100, 100); 11 | t.is(size.width, 100); 12 | t.is(size.height, 100); 13 | 14 | size = image.contain(500, 500); 15 | t.is(size.width, 500); 16 | t.is(size.height, 500); 17 | 18 | size = image.contain(10, 10); 19 | t.is(size.width, 10); 20 | t.is(size.height, 10); 21 | 22 | size = image.contain(200, 100); 23 | t.is(size.width, 100); 24 | t.is(size.height, 100); 25 | 26 | size = image.contain(100, 200); 27 | t.is(size.width, 100); 28 | t.is(size.height, 100); 29 | }); 30 | 31 | test('contain 2', t => { 32 | const image = new squoosh.ImageSize({bitmap: {width: 400, height: 200}}, '/path/to/file.jpg'); 33 | let size; 34 | 35 | size = image.contain(100, 100); 36 | t.is(size.width, 100); 37 | t.is(size.height, 50); 38 | 39 | size = image.contain(800, 800); 40 | t.is(size.width, 800); 41 | t.is(size.height, 400); 42 | 43 | size = image.contain(10, 10); 44 | t.is(size.width, 10); 45 | t.is(size.height, 5); 46 | 47 | size = image.contain(200, 100); 48 | t.is(size.width, 200); 49 | t.is(size.height, 100); 50 | 51 | size = image.contain(100, 200); 52 | t.is(size.width, 100); 53 | t.is(size.height, 50); 54 | }); 55 | 56 | test('contain 3', t => { 57 | const image = new squoosh.ImageSize({bitmap: {width: 200, height: 400}}, '/path/to/file.jpg'); 58 | let size; 59 | 60 | size = image.contain(100, 100); 61 | t.is(size.width, 50); 62 | t.is(size.height, 100); 63 | 64 | size = image.contain(800, 800); 65 | t.is(size.width, 400); 66 | t.is(size.height, 800); 67 | 68 | size = image.contain(10, 10); 69 | t.is(size.width, 5); 70 | t.is(size.height, 10); 71 | 72 | size = image.contain(200, 100); 73 | t.is(size.width, 50); 74 | t.is(size.height, 100); 75 | 76 | size = image.contain(100, 200); 77 | t.is(size.width, 100); 78 | t.is(size.height, 200); 79 | }); 80 | 81 | test('scaleDown 1', t => { 82 | const image = new squoosh.ImageSize({bitmap: {width: 200, height: 400}}, '/path/to/file.jpg'); 83 | let size; 84 | 85 | size = image.scaleDown(100, 100); 86 | t.is(size.width, 50); 87 | t.is(size.height, 100); 88 | 89 | size = image.scaleDown(800, 800); 90 | t.is(size.width, 200); 91 | t.is(size.height, 400); 92 | 93 | size = image.scaleDown(10, 10); 94 | t.is(size.width, 5); 95 | t.is(size.height, 10); 96 | 97 | size = image.scaleDown(200, 100); 98 | t.is(size.width, 50); 99 | t.is(size.height, 100); 100 | 101 | size = image.scaleDown(100, 200); 102 | t.is(size.width, 100); 103 | t.is(size.height, 200); 104 | }); 105 | 106 | test('cover 1', t => { 107 | const image = new squoosh.ImageSize({bitmap: {width: 200, height: 200}}, '/path/to/file.jpg'); 108 | let size; 109 | 110 | size = image.cover(100, 100); 111 | t.is(size.width, 100); 112 | t.is(size.height, 100); 113 | 114 | size = image.cover(500, 500); 115 | t.is(size.width, 500); 116 | t.is(size.height, 500); 117 | 118 | size = image.cover(10, 10); 119 | t.is(size.width, 10); 120 | t.is(size.height, 10); 121 | 122 | size = image.cover(200, 100); 123 | t.is(size.width, 200); 124 | t.is(size.height, 200); 125 | 126 | size = image.cover(100, 200); 127 | t.is(size.width, 200); 128 | t.is(size.height, 200); 129 | }); 130 | 131 | test('cover 2', t => { 132 | const image = new squoosh.ImageSize({bitmap: {width: 400, height: 200}}, '/path/to/file.jpg'); 133 | let size; 134 | 135 | size = image.cover(100, 100); 136 | t.is(size.width, 200); 137 | t.is(size.height, 100); 138 | 139 | size = image.cover(500, 500); 140 | t.is(size.width, 1000); 141 | t.is(size.height, 500); 142 | 143 | size = image.cover(10, 10); 144 | t.is(size.width, 20); 145 | t.is(size.height, 10); 146 | 147 | size = image.cover(200, 100); 148 | t.is(size.width, 200); 149 | t.is(size.height, 100); 150 | 151 | size = image.cover(100, 200); 152 | t.is(size.width, 400); 153 | t.is(size.height, 200); 154 | }); 155 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | 'use strict'; 4 | 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const gulp = require('gulp'); 8 | const test = require('ava'); 9 | const del = require('del'); 10 | const squoosh = require('..'); 11 | 12 | const dirname = __dirname; 13 | const dstdir = '_test'; 14 | 15 | test.beforeEach(t => { 16 | process.chdir(__dirname); 17 | del.sync(dstdir); 18 | }); 19 | 20 | test.afterEach(() => { 21 | del.sync(dstdir); 22 | }); 23 | 24 | test.serial('basic usage', t => { 25 | return new Promise(resolve => { 26 | const file = '80x80.jpg'; 27 | const stream = gulp.src(file) 28 | .pipe(squoosh()) 29 | .pipe(gulp.dest(dstdir)); 30 | stream.on('finish', () => { 31 | t.true(fs.existsSync(`${dstdir}/80x80.jpg`)); 32 | t.false(fs.existsSync(`${dstdir}/80x80.webp`)); 33 | resolve(); 34 | }); 35 | }); 36 | }); 37 | 38 | test.serial('array src', t => { 39 | return new Promise(resolve => { 40 | const stream = gulp.src(['80x80.png', 'cat_kotatsu_neko.png']) 41 | .pipe(squoosh()) 42 | .pipe(gulp.dest(dstdir)); 43 | stream.on('finish', () => { 44 | t.true(fs.existsSync(`${dstdir}/80x80.png`)); 45 | t.true(fs.existsSync(`${dstdir}/cat_kotatsu_neko.png`)); 46 | resolve(); 47 | }); 48 | }); 49 | }); 50 | 51 | test.serial('wildcard src', t => { 52 | return new Promise(resolve => { 53 | const stream = gulp.src(['*.png']) 54 | .pipe(squoosh()) 55 | .pipe(gulp.dest(dstdir)); 56 | stream.on('finish', () => { 57 | t.true(fs.existsSync(`${dstdir}/80x80.png`)); 58 | t.true(fs.existsSync(`${dstdir}/cat_kotatsu_neko.png`)); 59 | resolve(); 60 | }); 61 | }); 62 | }); 63 | 64 | test.serial('squoosh to same format', t => { 65 | return new Promise(resolve => { 66 | const file = '80x80.png'; 67 | const stream = gulp.src(file) 68 | .pipe(squoosh({ 69 | oxipng: {} 70 | })) 71 | .pipe(gulp.dest(dstdir)); 72 | stream.on('finish', () => { 73 | t.true(fs.existsSync(`${dstdir}/80x80.png`)); 74 | resolve(); 75 | }); 76 | }); 77 | }); 78 | 79 | test.serial('squoosh to webp, avif', t => { 80 | return new Promise(resolve => { 81 | const file = '80x80.png'; 82 | const stream = gulp.src(file) 83 | .pipe(squoosh({ 84 | avif: {}, 85 | webp: {} 86 | })) 87 | .pipe(gulp.dest(dstdir)); 88 | stream.on('finish', () => { 89 | t.true(fs.existsSync(`${dstdir}/80x80.avif`)); 90 | t.true(fs.existsSync(`${dstdir}/80x80.webp`)); 91 | t.false(fs.existsSync(`${dstdir}/80x80.png`)); 92 | resolve(); 93 | }); 94 | }); 95 | }); 96 | 97 | test.serial('passthrough unsupported format', t => { 98 | return new Promise(resolve => { 99 | const file = '80x80.gif'; 100 | const stream = gulp.src(file) 101 | .pipe(squoosh({ 102 | avif: {}, 103 | webp: {} 104 | })) 105 | .pipe(gulp.dest(dstdir)); 106 | stream.on('finish', () => { 107 | t.true(fs.existsSync(`${dstdir}/80x80.gif`)); 108 | t.false(fs.existsSync(`${dstdir}/80x80.avif`)); 109 | t.false(fs.existsSync(`${dstdir}/80x80.webp`)); 110 | resolve(); 111 | }); 112 | }); 113 | }); 114 | 115 | test.serial('quantize and rotate image', t => { 116 | return new Promise(resolve => { 117 | const base = 'cat_kotatsu_neko'; 118 | const stream = gulp.src(`${base}.png`) 119 | .pipe(squoosh({ 120 | oxipng: { 121 | level: 6 122 | }, 123 | webp: {}, 124 | avif: {} 125 | }, { 126 | quant: { 127 | enabled: true, 128 | numColors: 256 129 | }, 130 | rotate: { 131 | enabled: true, 132 | numRotations: 1 133 | } 134 | })) 135 | .pipe(gulp.dest(dstdir)); 136 | stream.on('finish', () => { 137 | t.true(fs.existsSync(`${dstdir}/${base}.png`)); 138 | t.true(fs.existsSync(`${dstdir}/${base}.avif`)); 139 | t.true(fs.existsSync(`${dstdir}/${base}.webp`)); 140 | resolve(); 141 | }); 142 | }); 143 | }); 144 | 145 | test.serial('object argument - encodeOptions only', t => { 146 | return new Promise(resolve => { 147 | const file = '80x80.png'; 148 | const stream = gulp.src(file) 149 | .pipe(squoosh({ 150 | encodeOptions: { 151 | webp: {} 152 | } 153 | })) 154 | .pipe(gulp.dest(dstdir)); 155 | stream.on('finish', () => { 156 | t.true(fs.existsSync(`${dstdir}/80x80.webp`)); 157 | resolve(); 158 | }); 159 | }); 160 | }); 161 | 162 | test.serial('object argument - preprocessOptions only', t => { 163 | return new Promise(resolve => { 164 | const base = 'cat_kotatsu_neko'; 165 | const stream = gulp.src(`${base}.png`) 166 | .pipe(squoosh({ 167 | preprocessOptions: { 168 | rotate: { 169 | enabled: true, 170 | numRotations: 2 171 | } 172 | } 173 | })) 174 | .pipe(gulp.dest(dstdir)); 175 | stream.on('finish', () => { 176 | t.true(fs.existsSync(`${dstdir}/${base}.png`)); 177 | resolve(); 178 | }); 179 | }); 180 | }); 181 | 182 | test.serial('object argument - both encodeOptions,preprocessOptions', t => { 183 | return new Promise(resolve => { 184 | const base = '80x80'; 185 | const stream = gulp.src(`${base}.png`) 186 | .pipe(squoosh({ 187 | encodeOptions: { 188 | avif: {}, 189 | webp: {} 190 | }, 191 | preprocessOptions: { 192 | rotate: { 193 | enabled: true, 194 | numRotations: 2 195 | } 196 | } 197 | })) 198 | .pipe(gulp.dest(dstdir)); 199 | stream.on('finish', () => { 200 | t.true(fs.existsSync(`${dstdir}/${base}.avif`)); 201 | t.true(fs.existsSync(`${dstdir}/${base}.webp`)); 202 | resolve(); 203 | }); 204 | }); 205 | }); 206 | 207 | test.serial('function argument contain', t => { 208 | return new Promise(resolve => { 209 | const base = 'cat_kotatsu_neko'; 210 | const stream = gulp.src(`${base}.png`) 211 | .pipe(squoosh(src => ({ 212 | preprocessOptions: { 213 | resize: { 214 | enabled: true, 215 | ...src.contain(200) 216 | } 217 | } 218 | }))) 219 | .pipe(gulp.dest(dstdir)); 220 | stream.on('finish', () => { 221 | t.true(fs.existsSync(`${dstdir}/${base}.png`)); 222 | resolve(); 223 | }); 224 | }); 225 | }); 226 | 227 | test.serial('function argument cover', t => { 228 | return new Promise(resolve => { 229 | const base = 'cat_kotatsu_neko'; 230 | const stream = gulp.src(`${base}.png`) 231 | .pipe(squoosh(src => ({ 232 | preprocessOptions: { 233 | resize: { 234 | enabled: true, 235 | ...src.cover(200) 236 | } 237 | } 238 | }))) 239 | .pipe(gulp.dest(dstdir)); 240 | stream.on('finish', () => { 241 | t.true(fs.existsSync(`${dstdir}/${base}.png`)); 242 | resolve(); 243 | }); 244 | }); 245 | }); 246 | 247 | test.serial('more complex', t => { 248 | return new Promise(resolve => { 249 | const base = 'cat_kotatsu_neko'; 250 | const stream = gulp.src(['*.png', '*.jpg']) 251 | .pipe(squoosh(src => { 252 | const extname = path.extname(src.path); 253 | const options = { 254 | encodeOptions: squoosh.DefaultEncodeOptions[extname] 255 | }; 256 | if (extname === '.jpg') { 257 | options.encodeOptions = {jxl: {}}; 258 | } 259 | 260 | if (extname === '.png') { 261 | options.encodeOptions = {avif: {}}; 262 | options.preprocessOptions = { 263 | quant: { 264 | enabled: true, 265 | numColors: 16 266 | } 267 | }; 268 | } 269 | 270 | return options; 271 | })) 272 | .pipe(gulp.dest(dstdir)); 273 | stream.on('finish', () => { 274 | t.false(fs.existsSync(`${dstdir}/${base}.png`)); 275 | t.true(fs.existsSync(`${dstdir}/${base}.avif`)); 276 | t.false(fs.existsSync(`${dstdir}/80x80.png`)); 277 | t.true(fs.existsSync(`${dstdir}/80x80.avif`)); 278 | t.true(fs.existsSync(`${dstdir}/80x80.jxl`)); 279 | resolve(); 280 | }); 281 | }); 282 | }); 283 | --------------------------------------------------------------------------------