├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bin └── thumb ├── package.json ├── spec ├── node-thumbnail │ └── HelloSpec.js └── support │ └── jasmine.json └── src └── thumbnail.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "default-case": 0, 4 | "indent": [ 2, 2 ], 5 | "linebreak-style": [ 2, "unix" ], 6 | "max-len": [1, 100, 2, { "ignoreComments": true }], 7 | "no-cond-assign": [2, "except-parens"], 8 | "no-else-return": 0, 9 | "no-param-reassign": 0, 10 | "no-unused-vars": [1, {"vars": "local", "args": "none"}], 11 | "prettier/prettier": ["error", {"singleQuote": true}], 12 | "quote-props": [1, "consistent-as-needed"], 13 | "radix": 0, 14 | "space-infix-ops": 0, 15 | "semi": [ 2, "always" ], 16 | "strict": 0 17 | }, 18 | "env": { 19 | "node": true, 20 | "es6": true 21 | }, 22 | "extends": "prettier", 23 | "plugins": ["prettier"] 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | notifications: 5 | email: false 6 | script: 7 | - npm run lint 8 | - npm test 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2019, Honza Pokorny 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL HONZA POKORNY BE LIABLE FOR ANY DIRECT, INDIRECT, 17 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 18 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 19 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 20 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 21 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 22 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-thumbnail 2 | ============== 3 | 4 | [![npm version](https://badge.fury.io/js/node-thumbnail.svg)](https://badge.fury.io/js/node-thumbnail) [![Build Status](https://travis-ci.org/honza/node-thumbnail.svg?branch=master)](https://travis-ci.org/honza/node-thumbnail) 5 | 6 | thumbnail all the things 7 | 8 | node-thumbnail creates a queue of images and converts them asynchronously into 9 | thumbnails. node-thumbnail has no binary dependencies --- only javascript. 10 | 11 | Command-line usage 12 | ------------------ 13 | 14 | thumb [options] source/path dest/path 15 | 16 | options: 17 | 18 | -h, --help 19 | -v, --version 20 | 21 | -s SUFFIX, --suffix SUFFIX 22 | -p PREFIX, --prefix PREFIX 23 | -d, --digest 24 | -t TYPE, --hashing-type TYPE 25 | 26 | -w, --width 27 | 28 | -c NUM, --concurrency NUM 29 | 30 | -o, --overwrite 31 | -s, --skip 32 | -i, --ignore 33 | -q, --quiet 34 | 35 | API 36 | --- 37 | 38 | You can use this library with callbacks, or with promises. 39 | 40 | ### callbacks 41 | 42 | ```js 43 | var thumb = require('node-thumbnail').thumb; 44 | 45 | // thumb(options, callback); 46 | 47 | thumb({ 48 | source: 'source/path', // could be a filename: dest/path/image.jpg 49 | destination: 'dest/path', 50 | concurrency: 4 51 | }, function(files, err, stdout, stderr) { 52 | console.log('All done!'); 53 | }); 54 | ``` 55 | 56 | default options: 57 | 58 | ```js 59 | defaults = { 60 | prefix: '', 61 | suffix: '_thumb', 62 | digest: false, 63 | hashingType: 'sha1', // 'sha1', 'md5', 'sha256', 'sha512' 64 | width: 800, 65 | concurrency: , 66 | quiet: false, // if set to 'true', console.log status messages will be supressed 67 | overwrite: false, 68 | skip: false, // Skip generation of existing thumbnails 69 | basename: undefined, // basename of the thumbnail. If unset, the name of the source file is used as basename. 70 | ignore: false, // Ignore unsupported files in "dest" 71 | logger: function(message) { 72 | console.log(message); 73 | } 74 | }; 75 | ``` 76 | 77 | **Note** you must specify at least `source` and `destination` 78 | 79 | ### promises 80 | 81 | The options that you can pass in are the same as above. 82 | 83 | ```js 84 | thumb({ 85 | source: 'src', 86 | destination: 'dest' 87 | }).then(function() { 88 | console.log('Success'); 89 | }).catch(function(e) { 90 | console.log('Error', e.toString()); 91 | }); 92 | ``` 93 | 94 | Installation 95 | ------------ 96 | 97 | $ npm install node-thumbnail 98 | 99 | License 100 | ------- 101 | 102 | BSD, short and sweet 103 | -------------------------------------------------------------------------------- /bin/thumb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const program = require('commander'); 4 | 5 | program 6 | .version('0.15.0') 7 | .usage('[options] source/dir dest/dir') 8 | .option('-s, --suffix [suffix]', 'Add [suffix] to thumb filename', '_thumb') 9 | .option('-p, --prefix [prefix]', 'Add [prefix] to thumb filename', '') 10 | .option('-e, --extension [extension]', 'Force file extension', '') 11 | .option('-d, --digest', 'Use digest for thumb filename') 12 | .option( 13 | '-t, --hashing-type [type]', 14 | 'Hashing type sha1, md5. Must be used with -d' 15 | ) 16 | .option('-w, --width [width]', 'Thumbnail width in pixels. Default: 800', 800) 17 | .option( 18 | '-c, --concurrency [num]', 19 | 'Number of workers (defaults to num of CPUs)' 20 | ) 21 | .option('-q, --quiet', 'Supress all output') 22 | .option('-o, --overwrite', 'Overwrite existing thumbs') 23 | .option('-s, --skip', 'Skip generation of existing thumbs') 24 | .option('-i, --ignore', 'Ignore unsupported files') 25 | .parse(process.argv); 26 | 27 | const main = require('../src/thumbnail.js').cli; 28 | main(program); 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-thumbnail", 3 | "description": "thumbnail all the things", 4 | "keywords": [ 5 | "thumbnail", 6 | "images" 7 | ], 8 | "author": "Honza Pokorny", 9 | "version": "0.15.0", 10 | "license": "BSD-2-Clause", 11 | "main": "./src/thumbnail.js", 12 | "engines": { 13 | "node": ">=8.11.0" 14 | }, 15 | "bin": { 16 | "thumb": "./bin/thumb" 17 | }, 18 | "homepage": "https://github.com/honza/node-thumbnail", 19 | "repository": { 20 | "type": "git", 21 | "url": "git://github.com/honza/node-thumbnail.git" 22 | }, 23 | "dependencies": { 24 | "async": "2.3.0", 25 | "commander": "2.9.0", 26 | "jimp": "0.2.27", 27 | "lodash": "^4.17.12" 28 | }, 29 | "scripts": { 30 | "test": "jasmine", 31 | "lint": "eslint src bin/thumb", 32 | "prettier": "prettier --write --single-quote src/thumbnail.js bin/thumb" 33 | }, 34 | "devDependencies": { 35 | "eslint": "^4.15.0", 36 | "eslint-config-prettier": "^2.9.0", 37 | "eslint-plugin-prettier": "^2.5.0", 38 | "jasmine": "^2.5.3", 39 | "prettier": "^1.5.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /spec/node-thumbnail/HelloSpec.js: -------------------------------------------------------------------------------- 1 | describe('Node thumbnail', function() { 2 | it('should work', function() { 3 | expect(1).toEqual(1); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | -------------------------------------------------------------------------------- /src/thumbnail.js: -------------------------------------------------------------------------------- 1 | // node-thumbnail 2 | // (c) 2012-2017 Honza Pokorny 3 | // Licensed under BSD 4 | // https://github.com/honza/node-thumbnail 5 | 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | const crypto = require('crypto'); 9 | const os = require('os'); 10 | 11 | const jimp = require('jimp'); 12 | const async = require('async'); 13 | const _ = require('lodash'); 14 | 15 | let options, 16 | queue, 17 | defaults, 18 | done, 19 | extensions, 20 | createQueue, 21 | run, 22 | resizer, 23 | isValidFilename, 24 | thumb; 25 | 26 | defaults = { 27 | prefix: '', 28 | suffix: '_thumb', 29 | digest: false, 30 | hashingType: 'sha1', 31 | width: 800, 32 | concurrency: os.cpus().length, 33 | quiet: false, 34 | overwrite: false, 35 | skip: false, 36 | ignore: false, // Ignore unsupported format 37 | logger: message => console.log(message) // eslint-disable-line no-console 38 | }; 39 | 40 | extensions = ['.jpg', '.jpeg', '.png']; 41 | 42 | resizer = (options, callback) => 43 | jimp.read(options.srcPath, (err, file) => { 44 | if (err) { 45 | let message = err.message + options.srcPath; 46 | return callback(null, message); 47 | } 48 | 49 | file.resize(options.width, jimp.AUTO); 50 | file.write(options.dstPath, (err, result) => { 51 | callback(result, err); 52 | }); 53 | }); 54 | 55 | isValidFilename = file => extensions.includes(path.extname(file).toLowerCase()); 56 | 57 | evalCustomExtension = (customExtension, srcPath) => { 58 | if (extensions.includes(customExtension)) { 59 | return customExtension; 60 | } 61 | 62 | return path.extname(srcPath); 63 | }; 64 | 65 | createQueue = (settings, resolve, reject) => { 66 | const finished = []; 67 | 68 | queue = async.queue((task, callback) => { 69 | if (settings.digest) { 70 | const hash = crypto.createHash(settings.hashingType); 71 | const stream = fs.ReadStream(task.options.srcPath); 72 | 73 | stream.on('data', d => hash.update(d)); 74 | 75 | stream.on('end', () => { 76 | const d = hash.digest('hex'); 77 | 78 | task.options.dstPath = path.join( 79 | settings.destination, 80 | d + 81 | '_' + 82 | settings.width + 83 | evalCustomExtension(settings.extension, task.options.srcPath) 84 | ); 85 | 86 | const fileExists = fs.existsSync(task.options.dstPath); 87 | if (settings.skip && fileExists) { 88 | finished.push(task.options); 89 | callback(); 90 | } else if (settings.overwrite || !fileExists) { 91 | resizer(task.options, (_, err) => { 92 | if (err) { 93 | callback(err); 94 | return reject(err); 95 | } 96 | finished.push(task.options); 97 | callback(); 98 | }); 99 | } 100 | }); 101 | } else { 102 | const name = task.options.srcPath; 103 | const ext = path.extname(name); 104 | const base = task.options.basename || path.basename(name, ext); 105 | 106 | task.options.dstPath = path.join( 107 | settings.destination, 108 | settings.prefix + 109 | base + 110 | settings.suffix + 111 | evalCustomExtension(settings.extension, name) 112 | ); 113 | 114 | const fileExists = fs.existsSync(task.options.dstPath); 115 | if (settings.skip && fileExists) { 116 | finished.push(task.options); 117 | callback(); 118 | } else if (settings.overwrite || !fileExists) { 119 | resizer(task.options, (_, err) => { 120 | if (err) { 121 | callback(err); 122 | return reject(err); 123 | } 124 | finished.push(task.options); 125 | callback(); 126 | }); 127 | } 128 | } 129 | }, settings.concurrency); 130 | 131 | queue.drain = () => { 132 | if (done) { 133 | done(finished, null); 134 | } 135 | 136 | resolve(finished, null); 137 | 138 | if (!settings.quiet) { 139 | settings.logger('All items have been processed.'); 140 | } 141 | }; 142 | }; 143 | 144 | run = (settings, resolve, reject) => { 145 | let images; 146 | 147 | const warnIfContainsDirectories = images => { 148 | let dirs = images.filter(image => image.isDirectory()); 149 | dirs.map(dir => { 150 | if (!settings.quiet) { 151 | settings.logger(`Warning: '${dir.name}' is a directory, skipping...`); 152 | } 153 | }); 154 | return images.filter(image => image.isFile()).map(image => image.name); 155 | }; 156 | 157 | if (fs.statSync(settings.source).isFile()) { 158 | images = [path.basename(settings.source)]; 159 | settings.source = path.dirname(settings.source); 160 | } else { 161 | images = fs.readdirSync(settings.source, { withFileTypes: true }); 162 | images = warnIfContainsDirectories(images); 163 | } 164 | 165 | const invalidFilenames = _.filter(images, _.negate(isValidFilename)); 166 | const containsInvalidFilenames = _.some(invalidFilenames); 167 | 168 | if (containsInvalidFilenames && !settings.ignore) { 169 | const files = invalidFilenames.join(', '); 170 | return reject('Your source directory contains unsupported files: ' + files); 171 | } 172 | 173 | createQueue(settings, resolve, reject); 174 | 175 | _.each(images, image => { 176 | if (isValidFilename(image)) { 177 | options = { 178 | srcPath: path.join(settings.source, image), 179 | width: settings.width, 180 | basename: settings.basename 181 | }; 182 | queue.push({ options: options }, () => { 183 | if (!settings.quiet) { 184 | settings.logger('Processing ' + image); 185 | } 186 | }); 187 | } 188 | }); 189 | }; 190 | 191 | thumb = (options, callback) => 192 | new Promise((resolve, reject) => { 193 | const settings = _.defaults(options, defaults); 194 | 195 | // options.args is present if run through the command line 196 | if (options.args) { 197 | if (options.args.length !== 2) { 198 | options.logger('Please provide a source and destination directories.'); 199 | return; 200 | } 201 | 202 | options.source = options.args[0]; 203 | options.destination = options.args[1]; 204 | } 205 | 206 | settings.width = parseInt(settings.width, 10); 207 | 208 | const sourceExists = fs.existsSync(options.source); 209 | const destExists = fs.existsSync(options.destination); 210 | let errorMessage; 211 | 212 | if (sourceExists && !destExists) { 213 | errorMessage = 214 | "Destination '" + options.destination + "' does not exist."; 215 | } else if (destExists && !sourceExists) { 216 | errorMessage = "Source '" + options.source + "' does not exist."; 217 | } else if (!sourceExists && !destExists) { 218 | errorMessage = 219 | "Source '" + 220 | options.source + 221 | "' and destination '" + 222 | options.destination + 223 | "' do not exist."; 224 | } 225 | 226 | if (errorMessage) { 227 | options.logger(errorMessage); 228 | 229 | if (callback) { 230 | callback(null, new Error(errorMessage)); 231 | } 232 | 233 | reject(new Error(errorMessage)); 234 | } 235 | 236 | if (callback) { 237 | done = callback; 238 | } 239 | 240 | run(settings, resolve, reject); 241 | }); 242 | 243 | exports.cli = options => { 244 | thumb(options).catch(error => { 245 | options.logger('ERROR: ' + error); 246 | process.exit(1); 247 | }); 248 | }; 249 | 250 | exports.thumb = thumb; 251 | --------------------------------------------------------------------------------