├── .gitattributes ├── .gitignore ├── README.md ├── examples └── index.js ├── lib ├── canvas.js ├── canvas_resize.js └── imagemagick.js ├── limby-resize.js └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text eol=lf 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Windows image file caches 3 | Thumbs.db 4 | ehthumbs.db 5 | 6 | # Folder config file 7 | Desktop.ini 8 | 9 | # Recycle Bin used on file shares 10 | $RECYCLE.BIN/ 11 | 12 | # Windows Installer files 13 | *.cab 14 | *.msi 15 | *.msm 16 | *.msp 17 | 18 | # ========================= 19 | # Operating System Files 20 | # ========================= 21 | 22 | # OSX 23 | # ========================= 24 | 25 | .DS_Store 26 | .AppleDouble 27 | .LSOverride 28 | 29 | # Icon must ends with two \r. 30 | Icon 31 | 32 | # Thumbnails 33 | ._* 34 | 35 | # Files that might appear on external disk 36 | .Spotlight-V100 37 | .Trashes 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | limby-resize 2 | ============ 3 | 4 | npm install limby-resize 5 | 6 | 7 | ### Resize with `canvas` or `imagemagick` node modules. 8 | 9 | Wrapper for both. 10 | 11 | 12 | #### Better resizing with `canvas` 13 | 14 | Normally, resizing with `canvas` produces some not so great images. This module implements it's own resizing algorithm. 15 | 16 | 17 | ##### Math behind the algorithm 18 | 19 | lets say we have 3 pixels being resized into 2 pixels. 20 | 21 | normally, each pixel will have 4 numbers: red, green, blue, alpha. Lets just look at a simplified version where pixels are just 1 number. 22 | 23 | Lets say the original image is (these represent 3 different pixels, separated by |): 24 | 25 | `0 | 100 | 255` 26 | 27 | The regular canvas drawImage resize will grab nearest neighbor and produce 28 | 29 | `0 | 100` or 30 | `0 | 255` 31 | 32 | This sometimes is fine, but it loses details and it can be a very ugly and jagged image. 33 | If you think about it, the sum of all the color in the original is 355 (0 + 100 + 255), leaving the average pixel 118.33. The resized average pixel would be 50 or 127.5, which could look okay or very different! 34 | 35 | The image algorithm implemented in `limby-resize` will produce a similar image to imagemagick, keeping all the pixel data, so the average pixel will be the same. 36 | 37 | Our algorithm would produce the following image: 38 | 39 | `33 | 201.3` 40 | 41 | `(0 * .66 + 100 * .33) | (100 * .33 + 255 * .66)` 42 | 43 | The total in ours is 234.3, leaving the average of 117.15, which is going to equal the first image ( if we weren't rounding to 2 decimals for this example ). 44 | 45 | #### tl;dr 46 | 47 | This image resizer is better than the default canvas drawImage, but it is slower and slightly processor intensive. 48 | 49 | Overall, this library wraps both `imagemagic` and `canvas` so you can switch out either one at a whim 50 | 51 | 52 | ## Usage 53 | 54 | 55 | ### Canvas 56 | 57 | ```javascript 58 | var resizer = require('limby-resize')({ 59 | canvas: require('canvas'), 60 | }); 61 | 62 | resizer.resize('/tmp/image01.jpg', { 63 | width: 300, 64 | height: 500, 65 | destination: '/uploads/myimage.jpg', 66 | }); 67 | ``` 68 | 69 | ### Image Magick 70 | 71 | ```javascript 72 | var resizer = require('limby-resize')({ 73 | imagemagick: require('imagemagick'), 74 | }); 75 | 76 | resizer.resize('/tmp/myanimation.gif', { 77 | width: 300, 78 | height: 500, 79 | coalesce: true, // animated gif support ( if your image magick supports ) 80 | destination: '/uploads/myanimation.gif', 81 | }); 82 | 83 | // [0] takes first frame for previews, etc 84 | resizer.resize('/tmp/myanimation.gif[0]', { 85 | width: 300, 86 | height: 500, 87 | destination: '/uploads/myanimation_preview.gif', 88 | }); 89 | ``` 90 | 91 | 92 | *Note:* If you scale up ( make a bigger image ), it will bypass the algorithm and use default `drawImage` 93 | 94 | * Gif support only for image magick at the moment 95 | 96 | canvas will just take the first frame, similar to using `[0]` with image magick 97 | 98 | 99 | ### Browser support 100 | 101 | _DEMO_ 102 | http://jsbin.com/palota/1/ 103 | http://jsbin.com/palota/1/edit?html,js,output 104 | 105 | ![How it looks](https://cloud.githubusercontent.com/assets/1516973/6985039/73b5fb4e-d9f4-11e4-921b-6fc873fa0b45.png) 106 | 107 | `lib/canvas_resize.js` should be able to be included on the frontend for better resizing client side. 108 | 109 | ```javascript 110 | var img, canvas, resized; 111 | img = new Image; 112 | img.onload = function(){ 113 | canvas = document.createElement('canvas'); 114 | canvas.width = img.width; 115 | canvas.height = img.height; 116 | canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height); 117 | resized = document.createElement('canvas'); 118 | resized.width = 300; 119 | resized.height = 500; 120 | // see lib/canvas_resize for window.canvasResize = function(){...} 121 | canvasResize(canvas, resized, function(){ 122 | // resized will now be a properly resized version of canvas 123 | }); 124 | } 125 | 126 | img.src = '/path/to/img.jpg'; 127 | ``` 128 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | // for this example, include: 2 | // https://rawgit.com/danschumann/limby-resize/master/lib/canvas_resize.js 3 | // jquery 4 | // this file 5 | // 6 | // Then upload an image to test 7 | $(function(){ 8 | var originalCanvas = $("")[0]; 9 | var originalContext = originalCanvas.getContext('2d'); 10 | 11 | var crappyCanvas = $("")[0]; 12 | var crappyContext = crappyCanvas.getContext('2d'); 13 | 14 | var goodCanvas = $("")[0]; 15 | var goodContext = goodCanvas.getContext('2d'); 16 | 17 | $container = $('
'); 18 | 19 | var $goodContainer = $container.clone().append( 20 | 'Good:', 21 | goodCanvas 22 | ); 23 | 24 | var $factor = $(''); 25 | 26 | $('body').append( 27 | $container.clone().append( 28 | 'Scale down by: ', 29 | $factor 30 | ), 31 | $container.clone().append( 32 | $('') 33 | ), 34 | $container.clone().append( 35 | 'Original:', 36 | originalCanvas 37 | ), 38 | $container.clone().append( 39 | 'Crappy:', 40 | crappyCanvas 41 | ), 42 | $goodContainer 43 | ); 44 | 45 | function readImage() { 46 | if ( this.files && this.files[0] ) { 47 | var FR= new FileReader(); 48 | FR.onload = function(e) { 49 | 50 | var img = new Image(); 51 | img.onload = function() { 52 | draw(img); 53 | }; 54 | img.src = e.target.result; 55 | }; 56 | FR.readAsDataURL( this.files[0] ); 57 | } 58 | } 59 | 60 | function draw(img) { 61 | originalCanvas.width = img.width; 62 | originalCanvas.height = img.height; 63 | factor = parseFloat($factor.val()); 64 | if (!(factor>0 && factor<1)) alert('Scale down must be between 0 and 1'); 65 | 66 | crappyCanvas.width = goodCanvas.width = Math.floor(img.width * factor); 67 | crappyCanvas.height = goodCanvas.height = Math.floor(img.height * factor); 68 | 69 | originalContext.drawImage(img, 0, 0); 70 | crappyContext.drawImage(img, 0, 0, crappyCanvas.width, crappyCanvas.height); 71 | 72 | var $processing = $('
Processing...
').appendTo($goodContainer); 73 | canvasResize(originalCanvas, goodCanvas, function(){ 74 | console.log('resized!'); 75 | $processing.remove(); 76 | }); 77 | } 78 | 79 | document.getElementById("fileUpload").addEventListener("change", readImage, false); 80 | }); 81 | -------------------------------------------------------------------------------- /lib/canvas.js: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * You may use this code as long as you retain this notice. Use at your own risk! :) 4 | * https://github.com/danschumann/limby-resize 5 | */ 6 | var 7 | _ = require('underscore'), 8 | when = require('when'), 9 | fs = require('final-fs'), 10 | canvasResize = require('./canvas_resize'), 11 | nodefn = require('when/node/function'); 12 | 13 | module.exports = function(limbyResizer) { 14 | 15 | var Canvas = limbyResizer.config.canvas; 16 | 17 | limbyResizer._resize = function(path, options) { 18 | 19 | var img; 20 | 21 | return fs.readFile(path) 22 | .then(function(data) { 23 | img = new Canvas.Image; 24 | img.src = data; 25 | }) 26 | .then(function() { 27 | 28 | var width = img.width, height = img.height; 29 | 30 | var canvas = new Canvas(width, height); 31 | var ctx = canvas.getContext('2d'); 32 | ctx.drawImage(img, 0, 0, width, height); 33 | 34 | var resizedCanvas; 35 | 36 | if (options.width) { 37 | 38 | if (options.constrain === false) { 39 | resizedCanvas = new Canvas(options.width, options.height); 40 | } else { 41 | // We take the smaller of the two ratios to ensure it fits within our options 42 | var ratio = Math.min(options.width / img.width, options.height / img.height); 43 | 44 | width = Math.floor(width * ratio); 45 | height = Math.floor(height * ratio); 46 | resizedCanvas = new Canvas(width, height); 47 | } 48 | 49 | return when.promise(function(resolve, reject){ 50 | canvasResize(canvas, resizedCanvas, function(){ 51 | (resizedCanvas || canvas).toBuffer(function(err, buf){ 52 | fs.writeFile(options.destination, buf) 53 | .then(resolve).otherwise(reject); 54 | }); 55 | 56 | }); 57 | }); 58 | }; 59 | 60 | }) 61 | .otherwise(function(er){ 62 | console.log('limby-resize: error processing image', er, er.stack); 63 | }); 64 | 65 | }; 66 | 67 | }; 68 | -------------------------------------------------------------------------------- /lib/canvas_resize.js: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * You may use this code as long as you retain this notice. Use at your own risk! :) 4 | * https://github.com/danschumann/limby-resize 5 | * 0.0.8 6 | */ 7 | (function(){ 8 | var isNode = !(typeof process == 'undefined' || !process.versions); 9 | 10 | return (function(_module, namespace) { 11 | 12 | // assigns to module.exports | window.canvasResize 13 | _module[namespace] = function(original, canvas, callback) { 14 | 15 | var 16 | w1 = original.width, 17 | h1 = original.height, 18 | w2 = canvas.width, 19 | h2 = canvas.height, 20 | img = original.getContext("2d").getImageData(0, 0, w1, h1), 21 | img2 = canvas.getContext("2d").getImageData(0, 0, w2, h2); 22 | 23 | if (w2 > w1 || h2 > h1) { 24 | canvas.getContext('2d').drawImage(original, 0, 0, w2, h2); 25 | return callback(); 26 | }; 27 | 28 | 29 | var data = img.data; 30 | // it's an _ because we don't use it much, as working with doubles isn't great 31 | var _data2 = img2.data; 32 | // Instead, we enforce float type for every entity in the array 33 | // this prevents weird faded lines when things get rounded off 34 | var data2 = Array(_data2.length); 35 | for (var i = 0; i < _data2.length; i++){ 36 | data2[i] = 0.0; 37 | } 38 | 39 | // We track alphas, since we need to use alphas to correct colors later on 40 | var alphas = Array(_data2.length >> 2); 41 | for (var i = 0; i < _data2.length >> 2; i++){ 42 | alphas[i] = 1; 43 | } 44 | 45 | // this will always be between 0 and 1 46 | var xScale = w2 / w1; 47 | var yScale = h2 / h1; 48 | 49 | var deferred; 50 | 51 | // We process 1 row at a time ( and then let the process rest for 0ms [async] ) 52 | var nextY = function(y1){ 53 | for (var x1 = 0; x1 < w1; x1++) { 54 | 55 | var 56 | 57 | // the original pixel is split between two pixels in the output, we do an extra step 58 | extraX = false, 59 | extraY = false, 60 | 61 | // the output pixel 62 | targetX = Math.floor(x1 * xScale), 63 | targetY = Math.floor(y1 * yScale), 64 | 65 | // The percentage of this pixel going to the output pixel (this gets modified) 66 | xFactor = xScale, 67 | yFactor = yScale, 68 | 69 | // The percentage of this pixel going to the right neighbor or bottom neighbor 70 | bottomFactor = 0, 71 | rightFactor = 0, 72 | 73 | // positions of pixels in the array 74 | offset = (y1 * w1 + x1) * 4, 75 | targetOffset = (targetY * w2 + targetX) * 4; 76 | 77 | // Right side goes into another pixel 78 | if (targetX < Math.floor((x1 + 1) * xScale)) { 79 | 80 | rightFactor = (((x1 + 1) * xScale) % 1); 81 | xFactor -= rightFactor; 82 | 83 | extraX = true; 84 | 85 | } 86 | 87 | // Bottom side goes into another pixel 88 | if (targetY < Math.floor((y1 + 1) * yScale)) { 89 | 90 | bottomFactor = (((y1 + 1) * yScale) % 1); 91 | yFactor -= bottomFactor; 92 | 93 | extraY = true; 94 | 95 | } 96 | 97 | var a; 98 | 99 | a = (data[offset + 3] / 255); 100 | 101 | var alphaOffset = targetOffset / 4; 102 | 103 | if (extraX) { 104 | 105 | // Since we're not adding the color of invisible pixels, we multiply by a 106 | data2[targetOffset + 4] += data[offset] * rightFactor * yFactor * a; 107 | data2[targetOffset + 5] += data[offset + 1] * rightFactor * yFactor * a; 108 | data2[targetOffset + 6] += data[offset + 2] * rightFactor * yFactor * a; 109 | 110 | data2[targetOffset + 7] += data[offset + 3] * rightFactor * yFactor; 111 | 112 | // if we left out the color of invisible pixels(fully or partly) 113 | // the entire average we end up with will no longer be out of 255 114 | // so we subtract the percentage from the alpha ( originally 1 ) 115 | // so that we can reverse this effect by dividing by the amount. 116 | // ( if one pixel is black and invisible, and the other is white and visible, 117 | // the white pixel will weight itself at 50% because it does not know the other pixel is invisible 118 | // so the total(color) for the new pixel would be 128(gray), but it should be all white. 119 | // the alpha will be the correct 128, combinging alphas, but we need to preserve the color 120 | // of the visible pixels ) 121 | alphas[alphaOffset + 1] -= (1 - a) * rightFactor * yFactor; 122 | } 123 | 124 | if (extraY) { 125 | data2[targetOffset + w2 * 4] += data[offset] * xFactor * bottomFactor * a; 126 | data2[targetOffset + w2 * 4 + 1] += data[offset + 1] * xFactor * bottomFactor * a; 127 | data2[targetOffset + w2 * 4 + 2] += data[offset + 2] * xFactor * bottomFactor * a; 128 | 129 | data2[targetOffset + w2 * 4 + 3] += data[offset + 3] * xFactor * bottomFactor; 130 | 131 | alphas[alphaOffset + w2] -= (1 - a) * xFactor * bottomFactor; 132 | } 133 | 134 | if (extraX && extraY) { 135 | data2[targetOffset + w2 * 4 + 4] += data[offset] * rightFactor * bottomFactor * a; 136 | data2[targetOffset + w2 * 4 + 5] += data[offset + 1] * rightFactor * bottomFactor * a; 137 | data2[targetOffset + w2 * 4 + 6] += data[offset + 2] * rightFactor * bottomFactor * a; 138 | 139 | data2[targetOffset + w2 * 4 + 7] += data[offset + 3] * rightFactor * bottomFactor; 140 | 141 | alphas[alphaOffset + w2 + 1] -= (1 - a) * rightFactor * bottomFactor; 142 | } 143 | 144 | data2[targetOffset] += data[offset] * xFactor * yFactor * a; 145 | data2[targetOffset + 1] += data[offset + 1] * xFactor * yFactor * a; 146 | data2[targetOffset + 2] += data[offset + 2] * xFactor * yFactor * a; 147 | 148 | data2[targetOffset + 3] += data[offset + 3] * xFactor * yFactor; 149 | 150 | alphas[alphaOffset] -= (1 - a) * xFactor * yFactor; 151 | }; 152 | 153 | if (y1++ < h1) { 154 | // Big images shouldn't block for a long time. 155 | // This breaks up the process and allows other processes to tick 156 | setTimeout(function(){ 157 | nextY(y1) 158 | }, 0); 159 | } else 160 | done(); 161 | 162 | }; 163 | 164 | var done = function(){ 165 | 166 | // fully distribute the color of pixels that are partially full because their neighbor is transparent 167 | // (i.e. undo the invisible pixels are averaged with visible ones) 168 | for (var i = 0; i < (_data2.length >> 2); i++){ 169 | if (alphas[i] && alphas[i] < 1) { 170 | data2[(i<<2)] /= alphas[i]; // r 171 | data2[(i<<2) + 1] /= alphas[i]; // g 172 | data2[(i<<2) + 2] /= alphas[i]; // b 173 | } 174 | } 175 | 176 | // re populate the actual imgData 177 | for (var i = 0; i < data2.length; i++){ 178 | _data2[i] = Math.round(data2[i]); 179 | } 180 | 181 | var context = canvas.getContext("2d") 182 | context.putImageData(img2, 0, 0); 183 | callback(); 184 | 185 | }; 186 | 187 | // Start processing the image at row 0 188 | nextY(0); 189 | 190 | }; 191 | 192 | })( isNode ? module : window, isNode ? 'exports' : 'canvasResize'); 193 | })() 194 | -------------------------------------------------------------------------------- /lib/imagemagick.js: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * You may use this code as long as you retain this notice. Use at your own risk! :) 4 | * https://github.com/danschumann/limby-resize 5 | */ 6 | var 7 | _ = require('underscore'), 8 | when = require('when'), 9 | nodefn = require('when/node/function'); 10 | 11 | module.exports = function(limbyResizer) { 12 | 13 | var im = _.clone(limbyResizer.config.imagemagick); 14 | 15 | _.each(['identify', 'convert', 'resize'], function(fnName) { 16 | im[fnName] = nodefn.lift(_.bind(im[fnName], im)); 17 | im[fnName].path = fnName; // BUGFIX: imagemagick depends on a string of what bash command to run 18 | }); 19 | 20 | limbyResizer._resize = function(path, options) { 21 | 22 | return when().then(function() { 23 | if (!options.width) return im.identify(['-format', '%wx%h_', path]) 24 | }) 25 | .then(function(output) { 26 | 27 | var args = [ path ]; 28 | if (options.coalesce) args.push('-coalesce'); 29 | args.push( 30 | '-resize', output ? output.split('_')[0] : options.width + 'x' + options.height, 31 | options.destination 32 | ); 33 | 34 | return im.convert(args); 35 | 36 | }); 37 | 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /limby-resize.js: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * You may use this code as long as you retain this notice. Use at your own risk! :) 4 | * https://github.com/danschumann/limby-resize 5 | */ 6 | (function(){ 7 | var _ = require('underscore'); 8 | var debug = require('debug')('limbyResizer'); 9 | 10 | var LimbyResizer; 11 | LimbyResizer = function(config) { 12 | 13 | if (!(this instanceof LimbyResizer)) return new LimbyResizer(config); 14 | 15 | debug('constructor'); 16 | 17 | // Disable if they didn't configure a image resizing module to use 18 | if (!config.imagemagick && !config.canvas) 19 | throw new Error("You must initialize limby-resizer with config.(canvas|imagemagick) = require('canvas|imagemagick')"); 20 | 21 | this.config = config; 22 | 23 | if (config.imagemagick) 24 | require('./lib/imagemagick')(this); 25 | else 26 | require('./lib/canvas')(this); 27 | 28 | }; 29 | 30 | LimbyResizer.prototype.resize = function(filePath, options){ 31 | 32 | var width, height; 33 | width = options && options.width || this.config.width; 34 | height = options && options.height || this.config.height; 35 | 36 | if (width && !height) 37 | throw new Error('If you specify width, you must also specify height'); 38 | if (!width && height) 39 | throw new Error('If you specify height, you must also specify width'); 40 | 41 | return this._resize(filePath, _.extend(_.clone(options), {width: width, height: height})); 42 | 43 | }; 44 | 45 | module.exports = LimbyResizer; 46 | 47 | })(); 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "limby-resize", 3 | "version": "0.0.8", 4 | "description": "Wrapper for resizing using ImageMagick OR Canvas.", 5 | "main": "./limby-resize.js", 6 | "scripts": {}, 7 | "repository": "https://github.com/danschumann/limby-resize", 8 | "author": "Dan Schumann", 9 | "dependencies": { 10 | "debug": "^1.0.4", 11 | "final-fs": "^1.6.0", 12 | "underscore": "^1.6.0", 13 | "when": "^3.4.2" 14 | }, 15 | "license": "MIT" 16 | } 17 | --------------------------------------------------------------------------------