├── .gitignore ├── index.js ├── public └── images │ └── cape cod.jpg ├── package.json ├── test ├── simple-test.js └── express-test.js ├── LICENSE ├── bin └── make-thumb.js ├── README.md └── lib └── quickthumb.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public/.cache -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/quickthumb'); -------------------------------------------------------------------------------- /public/images/cape cod.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zivester/node-quickthumb/HEAD/public/images/cape cod.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quickthumb", 3 | "version": "0.0.12", 4 | "description": "On the fly, thumbnail creation middleware for express.", 5 | "author": "Zach Ivester ", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "node test/simple-test.js", 9 | "start": "node test/express-test.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/zivester/node-quickthumb.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/zivester/node-quickthumb/issues" 17 | }, 18 | "licenses": [ 19 | { 20 | "type": "MIT", 21 | "url": "http://github.com/zivester/node-quickthumb/raw/master/LICENSE" 22 | } 23 | ], 24 | "keywords": [ 25 | "thumb", 26 | "thumbnail", 27 | "image", 28 | "imagemagick", 29 | "timthumb", 30 | "timthumb.php", 31 | "resize" 32 | ], 33 | "dependencies": { 34 | "imagemagick": "git://github.com/rsms/node-imagemagick.git", 35 | "mkdirp": "~0.3.3" 36 | }, 37 | "devDependencies": { 38 | "express": "^4.14.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/simple-test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | fs = require('fs'), 3 | path = require('path'), 4 | im = require('imagemagick'), 5 | qt = require('../'); 6 | 7 | var ppath = path.normalize(__dirname + '/../public/images'), 8 | src = path.join(ppath, 'cape cod.jpg'); 9 | 10 | var sizes = [ 11 | { width: 100, height: 100}, 12 | { width: 100, height: 50}, 13 | ]; 14 | 15 | sizes.forEach(function(options){ 16 | var opt = { 17 | src : src, 18 | dst : path.join(ppath, 'red_' + options.width + 'x' + options.height + '.gif'), 19 | width : options.width, 20 | height : options.height, 21 | quality : 1, 22 | }; 23 | qt.convert( opt, function(err, image){ 24 | assert.ifError(err); 25 | assert.equal(image, opt.dst); 26 | im.identify(image, function(err, features){ 27 | assert.ifError(err); 28 | assert.equal(features.width, opt.width); 29 | assert.equal(features.height, opt.height); 30 | assert.ifError(fs.unlinkSync(image)); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Zach Ivester 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /test/express-test.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | app = express(), 3 | qt = require('../'), 4 | filename = 'cape cod.jpg'; 5 | 6 | 7 | // Crop 8 | app.use('/public/crop', qt.static(__dirname + '/../public', { 9 | cacheDir : '/tmp/cache', 10 | quality : .95 11 | })); 12 | // Resize 13 | app.use('/public/resize', qt.static(__dirname + '/../public', { 14 | type : 'resize', 15 | })); 16 | 17 | 18 | app.get('/', function(req, res){ 19 | var types = [ 'crop', 'resize' ]; 20 | 21 | function img(type,q){ 22 | var src = '/public/' + ( type ? type + '/' : '' ) + 'images/' + filename + q; 23 | return ''; 24 | } 25 | 26 | var h = '
'; 27 | types.forEach(function(type){ 28 | h += '
' + type + '
'; 29 | [ '200', '100x100', 'x60', '35', '10x10', 'x35', '60', '100x150', 'x200' ].forEach(function(dim){ 30 | h += img(type, '?dim=' + dim) + ' '; 31 | }); 32 | h += '
' + img(type, '?dim=800x100') + '
'; 33 | }); 34 | h += '
original
' + img('crop','') + '
'; 35 | res.send(h); 36 | }); 37 | 38 | 39 | app.listen(3000); 40 | console.log("running on http://127.0.0.1:3000"); 41 | -------------------------------------------------------------------------------- /bin/make-thumb.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | path = require('path'), 3 | qt = require('../'); 4 | 5 | function exit(msg){ 6 | console.log(msg); 7 | process.exit(); 8 | } 9 | 10 | if (process.argv.length < 5){ 11 | exit('Usage: make-thumb.js src dst (x||x) [-r] [-p] [--resize]'); 12 | } 13 | 14 | var args = process.argv.slice(2), 15 | dimensions = args[2], 16 | src = args[0], 17 | dst = args[1], 18 | options = args.slice(3), 19 | recursive = false, 20 | width = '', 21 | height = '', 22 | type = 'crop'; 23 | 24 | // Create dimension directories 25 | // e.g. 200x150, 200, etc 26 | if (options.indexOf('-p') != -1){ 27 | dst = path.join(dst, dimensions); 28 | } 29 | 30 | // Recursive 31 | if (options.indexOf('-r') != -1){ 32 | recursive = true; 33 | } 34 | 35 | // Resize 36 | if (options.indexOf('--resize') != -1){ 37 | type = 'resize'; 38 | } 39 | 40 | (function(){ 41 | var match = /(\d*)x?(\d*)/.exec(dimensions); 42 | if (!match) { 43 | exit('dimensions must be x'); 44 | } 45 | if (match[1]){ 46 | width = match[1]; 47 | } 48 | if (match[2]){ 49 | height = match[2]; 50 | } 51 | })(); 52 | 53 | console.log('Converting to ' + width + ' x ' + height); 54 | 55 | 56 | if (!fs.existsSync(src)){ 57 | exit('Cannot read ' + src); 58 | } 59 | 60 | function convert(src, dst){ 61 | qt.convert({ 62 | src : src, 63 | dst : path.join(dst, path.basename(src)), 64 | width : width, 65 | height : height, 66 | type : type 67 | }, function(err, image){ 68 | if (err){ 69 | return console.error(err); 70 | } 71 | console.log("CREATED", image); 72 | }); 73 | } 74 | 75 | function processDir(src, dst){ 76 | fs.readdirSync(src).forEach(function(filename){ 77 | var spath = path.join(src, filename); 78 | if (fs.statSync(spath).isFile()){ 79 | convert(spath, dst); 80 | } 81 | else if (recursive){ 82 | processDir(spath, path.join(dst, filename)); 83 | } 84 | }); 85 | } 86 | 87 | if (fs.statSync(src).isFile()){ 88 | convert(src, dst); 89 | } 90 | else{ 91 | processDir(src, dst); 92 | } 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QuickThumb 2 | 3 | QuickThumb is an on the fly, thumbnail creation middleware for express. It utilizes the popular *nix image library, ImageMagick. It allows for the automatic creation of thumbnails by adding query parameters onto a standard image url. It's ideal for web developers who would like to easily experiment with different size thumbnails, wihout having to worry about pre-generating an entire library. 4 | 5 | QuickThumb also comes with a command line utility to batch create thumbnails. This is more appropriate for production systems where all images should be pre-generated. 6 | 7 | ## Examples 8 | 9 | ```js 10 | var express = require('express'), 11 | app = express(), 12 | qt = require('quickthumb'); 13 | 14 | app.use('/public', qt.static(__dirname + '/../public')); 15 | ``` 16 | 17 | ```html 18 | 19 | ``` 20 | 21 | ## Install 22 | 23 | npm install quickthumb 24 | 25 | ImageMagick is required for this module, so make sure it is installed. 26 | 27 | Ubuntu 28 | 29 | apt-get install imagemagick 30 | 31 | Mac OS X 32 | 33 | brew install imagemagick 34 | 35 | Fedora/CentOS 36 | 37 | yum install imagemagick 38 | 39 | 40 | ## Documentation 41 | 42 | ### qt.static(path, [options]) 43 | 44 | Middleware to replace `express.static()` or `connect.static()`. 45 | 46 | `path` is the base directory where images are located. 47 | 48 | `options` is an object to specify customizations. It currently has the following options: 49 | 50 | * `type` The type of imagemagick conversion to take place. There are currently only two options: 51 | * `crop` (default) Crops and zooms images to the exact size specified. Proxy to *imagemagick.crop*. 52 | * `resize` Resizes an image to fit within the specified dimensions, but actual dimensions may not be exactly as specified. Proxy to *imagemagick.resize*. 53 | * `cacheDir` The directory where generated images will be created. If not supplied, images will be created in `[path]/.cache/` 54 | * `quality` The quality to use when resizing the image. Values should be between 0 (worst quality) and 1 (best quality) 55 | 56 | Resizing of images is directed by the query parameter `dim`. This is in the format [width]x[height]. E.g. `red.gif?dim=200x100` 57 | 58 | Resized images will be created on an as needed basis, and stored in `[cacheDir]/[type]/[dim]`. 59 | 60 | If the `dim` parameter is not present, the original image will be served. 61 | 62 | ### qt.convert(options, callback) 63 | 64 | The first argument is an options object. `src`, `dst`, and at least one of `width` and `height` are required 65 | 66 | * `src` (required) Path to source image 67 | * `dst` (required) Path to destination image 68 | * `width` Width of resized image 69 | * `height` Height of resized image 70 | 71 | The callback argument gets 2 arguments. The first is an error object, most likely from imagemagick's *convert*. The second argument is the path to the created image. 72 | 73 | 74 | ## CLI utils 75 | 76 | ```js 77 | node bin/make-thumb.js src dst [width]x[height] [-p] [-r] [--resize] 78 | ``` 79 | 80 | * `src` Path to the source image or directory 81 | * `dst` Path to the destination image or directory 82 | * `[width]x[height]` Dimensions of the resized images 83 | * `-p` Create a subdirectory in `dst` based off of the dimensions 84 | * `-r` Process images recursively from `src` 85 | * `--resize` Use *resize* instead of *crop* 86 | 87 | __Example__ 88 | 89 | ```js 90 | // Resize a single image and write it to /tmp/red.gif 91 | node bin/make-thumb.js public/images/red.gif /tmp/ 200x200 92 | // Resize all images recursively from public/images/* and write them to /tmp/200x200/* 93 | node bin/make-thumb.js public/images/ /tmp/ 200x200 -p -r 94 | ``` 95 | 96 | -------------------------------------------------------------------------------- /lib/quickthumb.js: -------------------------------------------------------------------------------- 1 | var qt = {}, 2 | fs = require('fs'), 3 | path = require('path'), 4 | mkdirp = require('mkdirp'), 5 | im = require('imagemagick'); 6 | 7 | 8 | module.exports = qt; 9 | 10 | 11 | // express 4 deprecation support 12 | function sendfile(res, file) { 13 | res[ res.sendFile ? 'sendFile' : 'sendfile' ](file); 14 | } 15 | 16 | 17 | // Take an image from src, and write it to dst 18 | qt.convert = function(options, callback){ 19 | var src = options.src, 20 | dst = options.dst, 21 | width = options.width, 22 | height = options.height, 23 | quality = options.quality, 24 | type = options.type || 'crop'; 25 | 26 | mkdirp(path.dirname(dst)); 27 | 28 | var im_options = { 29 | srcPath : src, 30 | dstPath : dst 31 | }; 32 | 33 | if (options.width) im_options.width = width; 34 | if (options.height) im_options.height = height; 35 | if (options.quality) im_options.quality = quality; 36 | 37 | try{ 38 | im[type](im_options, function(err, stdout, stderr){ 39 | if (err){ 40 | return callback(err); 41 | } 42 | callback(null, dst); 43 | }); 44 | } 45 | catch(err){ 46 | return callback('qt.convert() ERROR: ' + err.message); 47 | } 48 | }; 49 | 50 | 51 | // express/connect middleware 52 | qt.static = function(root, options){ 53 | 54 | root = path.normalize(root); 55 | 56 | options || ( options = {} ); 57 | options.type || ( options.type = 'crop' ); 58 | options.cacheDir || ( options.cacheDir = path.join(root, '.cache') ); 59 | 60 | return function (req, res, next){ 61 | var file = decodeURI(req.url.replace(/\?.*/,'')), 62 | dim = req.query.dim || "", 63 | orig = path.normalize(root + file), 64 | dst = path.join(options.cacheDir, options.type, dim, file); 65 | 66 | function send_if_exists(file, callback){ 67 | fs.exists(file, function(exists){ 68 | if (!exists){ 69 | return callback(); 70 | } 71 | 72 | fs.stat(file, function(err, stats){ 73 | if (err){ 74 | console.error(err); 75 | return callback(); 76 | } 77 | else if (stats.isFile()){ 78 | // Check if the original image has been changed since the cache file 79 | // was created and if so, recreate it, otherwise send cached file. 80 | fs.stat(orig, function (err, origStats) { 81 | if (err) { 82 | console.error(err); 83 | } else if (origStats.mtime.getTime() > stats.mtime.getTime()) { 84 | return callback(); 85 | } 86 | 87 | return sendfile(res, file); 88 | }); 89 | } 90 | else { 91 | callback(); 92 | } 93 | }); 94 | }); 95 | } 96 | 97 | if (!dim){ 98 | return send_if_exists(orig, next); 99 | } 100 | 101 | send_if_exists(dst, function(){ 102 | var dims = dim.split(/x/g), 103 | opts = { 104 | src : orig, 105 | dst : dst, 106 | width : dims[0], 107 | height : dims[1], 108 | type : options.type, 109 | quality : options.quality 110 | }; 111 | 112 | qt.convert(opts, function(err, dst){ 113 | if (err){ 114 | console.error(err); 115 | return next(); 116 | } 117 | sendfile(res, dst); 118 | }); 119 | }); 120 | }; 121 | }; 122 | --------------------------------------------------------------------------------