├── .gitignore ├── test ├── purplealpha24bit.png └── PngQuant.js ├── package.json ├── README.md ├── LICENSE └── lib └── PngQuant.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /test/purplealpha24bit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/podio/node-pngquant/master/test/purplealpha24bit.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pngquant", 3 | "version": "0.4.1-podio", 4 | "description": "The pngquant utility as a readable/writable stream", 5 | "main": "lib/PngQuant.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "optionalDependencies": { 10 | "pngquant-bin": "https://github.com/podio/pngquant-bin/archive/v2.0.4-podio.tar.gz" 11 | }, 12 | "devDependencies": { 13 | "mocha": "=1.7.3", 14 | "unexpected": "=1.0.11" 15 | }, 16 | "scripts": { 17 | "prepublish": "mocha", 18 | "test": "mocha" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git://github.com/papandreou/node-pngquant.git" 23 | }, 24 | "keywords": [ 25 | "pngquant", 26 | "png", 27 | "image", 28 | "optimization", 29 | "stream", 30 | "filter", 31 | "read/write", 32 | "duplex" 33 | ], 34 | "author": "Andreas Lind Petersen ", 35 | "license": "BSD" 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-pngquant 2 | ============= 3 | 4 | The pngquant command line utility as a readable/writable stream. 5 | 6 | The constructor optionally takes an array of command line options for 7 | the `pngquant` binary (defaults to `[256]`): 8 | 9 | ```javascript 10 | var PngQuant = require('pngquant'), 11 | myPngQuanter = new PngQuant([192, '-ordered']); 12 | 13 | sourceStream.pipe(myPngQuanter).pipe(destinationStream); 14 | ``` 15 | 16 | PngQuant as a web service (sends back a png with the number of colors 17 | quantized to 128): 18 | 19 | ```javascript 20 | var PngQuant = require('pngquant'), 21 | http = require('http'); 22 | 23 | http.createServer(function (req, res) { 24 | if (req.headers['content-type'] === 'image/png') { 25 | res.writeHead(200, {'Content-Type': 'image/png'}); 26 | req.pipe(new PngQuant([128])).pipe(res); 27 | } else { 28 | res.writeHead(400); 29 | res.end('Feed me a PNG!'); 30 | } 31 | }).listen(1337); 32 | ``` 33 | 34 | Installation 35 | ------------ 36 | 37 | Make sure you have node.js and npm installed, and that the `pngquant` binary is in your PATH, then run: 38 | 39 | npm install pngquant 40 | 41 | License 42 | ------- 43 | 44 | 3-clause BSD license -- see the `LICENSE` file for details. 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Andreas Lind Petersen 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 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of the author nor the names of contributors may 15 | be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /test/PngQuant.js: -------------------------------------------------------------------------------- 1 | var expect = require('unexpected'), 2 | PngQuant = require('../lib/PngQuant'), 3 | Path = require('path'), 4 | fs = require('fs'); 5 | 6 | describe('PngQuant', function () { 7 | it('should produce a smaller file', function (done) { 8 | var pngQuant = new PngQuant([128]), 9 | chunks = []; 10 | fs.createReadStream(Path.resolve(__dirname, 'purplealpha24bit.png')) 11 | .pipe(pngQuant) 12 | .on('data', function (chunk) { 13 | chunks.push(chunk); 14 | }) 15 | .on('end', function () { 16 | var resultPngBuffer = Buffer.concat(chunks); 17 | expect(resultPngBuffer.length, 'to be greater than', 0); 18 | expect(resultPngBuffer.length, 'to be less than', 8285); 19 | done(); 20 | }) 21 | .on('error', done); 22 | }); 23 | 24 | it('should not emit data events while paused', function (done) { 25 | var pngQuant = new PngQuant(); 26 | 27 | function fail() { 28 | done(new Error('PngQuant emitted data while it was paused!')); 29 | } 30 | pngQuant.pause(); 31 | pngQuant.on('data', fail).on('error', done); 32 | 33 | fs.createReadStream(Path.resolve(__dirname, 'purplealpha24bit.png')).pipe(pngQuant); 34 | 35 | setTimeout(function () { 36 | pngQuant.removeListener('data', fail); 37 | var chunks = []; 38 | 39 | pngQuant 40 | .on('data', function (chunk) { 41 | chunks.push(chunk); 42 | }) 43 | .on('end', function () { 44 | var resultPngBuffer = Buffer.concat(chunks); 45 | expect(resultPngBuffer.length, 'to be greater than', 0); 46 | expect(resultPngBuffer.length, 'to be less than', 8285); 47 | done(); 48 | }); 49 | 50 | pngQuant.resume(); 51 | }, 1000); 52 | }); 53 | 54 | it('should emit an error if an invalid image is processed', function (done) { 55 | var pngQuant = new PngQuant(); 56 | 57 | pngQuant.on('error', function (err) { 58 | done(); 59 | }).on('data', function (chunk) { 60 | done(new Error('PngQuant emitted data when an error was expected')); 61 | }).on('end', function (chunk) { 62 | done(new Error('PngQuant emitted end when an error was expected')); 63 | }); 64 | 65 | pngQuant.end(new Buffer('qwvopeqwovkqvwiejvq', 'utf-8')); 66 | }); 67 | 68 | it('should emit a single error if an invalid command line is specified', function (done) { 69 | var pngQuant = new PngQuant(['--blabla']), 70 | seenError = false; 71 | 72 | pngQuant.on('error', function (err) { 73 | expect(pngQuant.commandLine, 'to match', /pngquant --blabla/); 74 | if (seenError) { 75 | done(new Error('More than one error event was emitted')); 76 | } else { 77 | seenError = true; 78 | setTimeout(done, 100); 79 | } 80 | }).on('data', function (chunk) { 81 | done(new Error('PngQuant emitted data when an error was expected')); 82 | }).on('end', function (chunk) { 83 | done(new Error('PngQuant emitted end when an error was expected')); 84 | }); 85 | 86 | pngQuant.end(new Buffer('qwvopeqwovkqvwiejvq', 'utf-8')); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /lib/PngQuant.js: -------------------------------------------------------------------------------- 1 | var childProcess = require('child_process'), 2 | Stream = require('stream').Stream, 3 | util = require('util'), 4 | pngQuantBin; 5 | 6 | try { 7 | pngQuantBin = require('pngquant-bin'); 8 | } catch (e) {} 9 | 10 | var binPath = pngQuantBin ? pngQuantBin.path : 'pngquant'; 11 | 12 | function PngQuant(pngQuantArgs) { 13 | Stream.call(this); 14 | 15 | this.pngQuantArgs = pngQuantArgs; 16 | 17 | if (!this.pngQuantArgs || this.pngQuantArgs.length === 0) { 18 | this.pngQuantArgs = [256]; 19 | } 20 | 21 | this.writable = this.readable = true; 22 | this.commandLine = binPath + (pngQuantArgs ? ' ' + pngQuantArgs.join(' ') : ''); // For debugging 23 | 24 | this.hasEnded = false; 25 | this.seenDataOnStdout = false; 26 | } 27 | 28 | util.inherits(PngQuant, Stream); 29 | 30 | PngQuant.prototype._reportError = function (err) { 31 | if (!this.hasEnded) { 32 | this.hasEnded = true; 33 | this.emit('error', err); 34 | } 35 | }; 36 | 37 | PngQuant.prototype.write = function (chunk) { 38 | if (!this.pngQuantProcess) { 39 | this.pngQuantProcess = childProcess.spawn(binPath, this.pngQuantArgs); 40 | this.pngQuantProcess.on('error', this._reportError.bind(this)); 41 | this.pngQuantProcess.stdin.on('error', this._reportError.bind(this)); 42 | this.pngQuantProcess.stdout.on('error', this._reportError.bind(this)); 43 | 44 | this.pngQuantProcess.stderr.on('data', function (data) { 45 | if (!this.hasEnded) { 46 | this._reportError(new Error('Saw pngquant output on stderr: ' + data.toString('ascii'))); 47 | this.hasEnded = true; 48 | } 49 | }.bind(this)); 50 | 51 | this.pngQuantProcess.on('exit', function (exitCode) { 52 | if (exitCode > 0 && !this.hasEnded) { 53 | this._reportError(new Error('The pngquant process exited with a non-zero exit code: ' + exitCode)); 54 | this.hasEnded = true; 55 | } 56 | }.bind(this)); 57 | 58 | this.pngQuantProcess.stdout.on('data', function (chunk) { 59 | this.seenDataOnStdout = true; 60 | this.emit('data', chunk); 61 | }.bind(this)).on('end', function () { 62 | if (!this.hasEnded) { 63 | if (this.seenDataOnStdout) { 64 | this.emit('end'); 65 | } else { 66 | this._reportError(new Error('PngQuant: The stdout stream ended without emitting any data')); 67 | } 68 | this.hasEnded = true; 69 | } 70 | }.bind(this)); 71 | 72 | if (this.pauseStdoutOfPngQuantProcessAfterStartingIt) { 73 | this.pngQuantProcess.stdout.pause(); 74 | } 75 | } 76 | this.pngQuantProcess.stdin.write(chunk); 77 | }; 78 | 79 | PngQuant.prototype.end = function (chunk) { 80 | if (chunk) { 81 | this.write(chunk); 82 | } else if (!this.pngQuantProcess) { 83 | // No chunks have been rewritten. Write an empty one to make sure there's pngquant process. 84 | this.write(new Buffer(0)); 85 | } 86 | this.pngQuantProcess.stdin.end(); 87 | }; 88 | 89 | PngQuant.prototype.pause = function () { 90 | if (this.pngQuantProcess) { 91 | this.pngQuantProcess.stdout.pause(); 92 | } else { 93 | this.pauseStdoutOfPngQuantProcessAfterStartingIt = true; 94 | } 95 | }; 96 | 97 | PngQuant.prototype.resume = function () { 98 | if (this.pngQuantProcess) { 99 | this.pngQuantProcess.stdout.resume(); 100 | } else { 101 | this.pauseStdoutOfPngQuantProcessAfterStartingIt = false; 102 | } 103 | }; 104 | 105 | module.exports = PngQuant; 106 | --------------------------------------------------------------------------------