├── .npmignore ├── .gitignore ├── jpegsrc.v9a.tar ├── test ├── images │ ├── cmyk.jpg │ ├── gray.jpg │ ├── j1.jpg │ ├── j2.jpg │ ├── rgb.jpg │ └── tetons.jpg ├── encoder.js └── decoder.js ├── index.js ├── Makefile ├── package.json ├── encoder.js ├── decoder.js ├── src ├── decoder.h ├── encoder.cc ├── encoder.h └── decoder.cc └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | jpeg-9a/ 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | jpeg-9a/ 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /jpegsrc.v9a.tar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devongovett/jpg-stream/HEAD/jpegsrc.v9a.tar -------------------------------------------------------------------------------- /test/images/cmyk.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devongovett/jpg-stream/HEAD/test/images/cmyk.jpg -------------------------------------------------------------------------------- /test/images/gray.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devongovett/jpg-stream/HEAD/test/images/gray.jpg -------------------------------------------------------------------------------- /test/images/j1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devongovett/jpg-stream/HEAD/test/images/j1.jpg -------------------------------------------------------------------------------- /test/images/j2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devongovett/jpg-stream/HEAD/test/images/j2.jpg -------------------------------------------------------------------------------- /test/images/rgb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devongovett/jpg-stream/HEAD/test/images/rgb.jpg -------------------------------------------------------------------------------- /test/images/tetons.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devongovett/jpg-stream/HEAD/test/images/tetons.jpg -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | exports.Decoder = require('./decoder'); 2 | exports.Encoder = require('./encoder'); 3 | exports.mime = 'image/jpeg'; 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: build/jpeg.js 2 | 3 | jpeg-9a/.libs/libjpeg.dylib: jpegsrc.v9a.tar 4 | tar -xvf jpegsrc.v9a.tar 5 | cd jpeg-9a && emconfigure ./configure && emmake make 6 | 7 | build/jpeg.js: jpeg-9a/.libs/libjpeg.dylib src/*.h src/*.cc 8 | mkdir -p build/ 9 | emcc --memory-init-file 0 \ 10 | --bind -Ijpeg-9a/ \ 11 | -s NO_FILESYSTEM=1 \ 12 | -s NO_BROWSER=1 \ 13 | -s DISABLE_EXCEPTION_CATCHING=1 \ 14 | -s PRECISE_I64_MATH=0 \ 15 | -s TOTAL_MEMORY=67108864 \ 16 | -O3 jpeg-9a/.libs/libjpeg.dylib src/*.cc -o build/jpeg.js 17 | echo "module.exports = Module;" >> build/jpeg.js 18 | 19 | clean: 20 | rm -rf jpeg-9a/ build/ -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jpg-stream", 3 | "version": "1.1.2", 4 | "description": "A streaming JPEG encoder and decoder", 5 | "main": "index.js", 6 | "dependencies": { 7 | "exif-reader": "^1.0.0", 8 | "pixel-stream": "^1.0.3" 9 | }, 10 | "devDependencies": { 11 | "concat-frames": "^1.0.1", 12 | "mocha": "^2.0.1" 13 | }, 14 | "scripts": { 15 | "test": "mocha" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git://github.com/devongovett/jpg-stream.git" 20 | }, 21 | "keywords": [ 22 | "pixel-stream", 23 | "image", 24 | "jpeg", 25 | "codec" 26 | ], 27 | "author": "Devon Govett ", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/devongovett/jpg-stream/issues" 31 | }, 32 | "homepage": "https://github.com/devongovett/jpg-stream" 33 | } 34 | -------------------------------------------------------------------------------- /encoder.js: -------------------------------------------------------------------------------- 1 | var jpeg = require('./build/jpeg'); 2 | var Encoder = jpeg.JPEGEncoder; 3 | var util = require('util'); 4 | var PixelStream = require('pixel-stream'); 5 | 6 | function JPEGEncoder(width, height, opts) { 7 | PixelStream.apply(this, arguments); 8 | if (typeof width === 'object') 9 | opts = width; 10 | 11 | this.encoder = new Encoder; 12 | this.ended = false; 13 | 14 | var self = this; 15 | this.encoder.callback = function(type, ptr, len) { 16 | switch (type) { 17 | case 'data': 18 | self.push(new Buffer(jpeg.HEAPU8.subarray(ptr, ptr + len))); 19 | break; 20 | 21 | case 'error': 22 | self.ended = true; 23 | self.encoder.delete(); 24 | self.emit('error', new Error(ptr)); 25 | break; 26 | } 27 | }; 28 | } 29 | 30 | util.inherits(JPEGEncoder, PixelStream); 31 | 32 | JPEGEncoder.prototype.supportedColorSpaces = ['rgb', 'gray', 'cmyk']; 33 | 34 | JPEGEncoder.prototype._start = function(done) { 35 | this.encoder.width = this.format.width; 36 | this.encoder.height = this.format.height; 37 | this.encoder.colorSpace = this.format.colorSpace; 38 | this.encoder.quality = this.format.quality || 100; 39 | done(); 40 | }; 41 | 42 | JPEGEncoder.prototype._writePixels = function(data, done) { 43 | if (!this.ended) { 44 | var buf = data instanceof Uint8Array ? data : new Uint8Array(data); 45 | this.encoder.encode(buf); 46 | } 47 | 48 | done(); 49 | }; 50 | 51 | JPEGEncoder.prototype._endFrame = function(done) { 52 | if (!this.ended) { 53 | this.ended = true; 54 | this.encoder.end(); 55 | this.encoder.delete(); 56 | } 57 | 58 | done(); 59 | }; 60 | 61 | module.exports = JPEGEncoder; 62 | -------------------------------------------------------------------------------- /decoder.js: -------------------------------------------------------------------------------- 1 | var jpeg = require('./build/jpeg'); 2 | var Decoder = jpeg.JPEGDecoder; 3 | var util = require('util'); 4 | var Transform = require('stream').Transform; 5 | var exif = require('exif-reader'); 6 | 7 | function JPEGDecoder(opts) { 8 | Transform.call(this); 9 | this.decoder = new Decoder; 10 | 11 | if (opts && opts.width && opts.height) 12 | this.decoder.setDesiredSize(opts.width, opts.height); 13 | 14 | var self = this; 15 | this.decoder.callback = function(type, ptr, len) { 16 | switch (type) { 17 | case 'outputSize': 18 | self.format = { 19 | width: self.decoder.width, 20 | height: self.decoder.height, 21 | colorSpace: self.decoder.colorSpace 22 | }; 23 | 24 | self.emit('format', self.format); 25 | break; 26 | 27 | case 'exif': 28 | var buf = new Buffer(jpeg.HEAPU8.subarray(ptr, ptr + len)); 29 | self.emit('meta', exif(buf)); 30 | break; 31 | 32 | case 'scanline': 33 | self.push(new Buffer(jpeg.HEAPU8.subarray(ptr, ptr + len))); 34 | break; 35 | 36 | case 'end': 37 | self.push(null); 38 | break; 39 | 40 | case 'error': 41 | self.decoder.delete(); 42 | self.emit('error', new Error(ptr)); 43 | break; 44 | } 45 | }; 46 | } 47 | 48 | util.inherits(JPEGDecoder, Transform); 49 | 50 | JPEGDecoder.probe = function(buf) { 51 | return buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff; 52 | }; 53 | 54 | JPEGDecoder.prototype._transform = function(data, encoding, done) { 55 | var buf = data instanceof Uint8Array ? data : new Uint8Array(data); 56 | this.decoder.decode(buf); 57 | done(); 58 | }; 59 | 60 | JPEGDecoder.prototype._flush = function(done) { 61 | this.decoder.delete(); 62 | done(); 63 | }; 64 | 65 | module.exports = JPEGDecoder; 66 | -------------------------------------------------------------------------------- /src/decoder.h: -------------------------------------------------------------------------------- 1 | extern "C" { 2 | #include 3 | #include 4 | } 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | using namespace emscripten; 12 | 13 | enum JPEGState { 14 | JPEG_HEADER, 15 | JPEG_START_DECOMPRESS, 16 | JPEG_DECOMPRESS, 17 | JPEG_DONE, 18 | JPEG_ERROR 19 | }; 20 | 21 | class JPEGDecoder { 22 | public: 23 | JPEGDecoder(); 24 | ~JPEGDecoder(); 25 | void setDesiredSize(int dw, int dh) { 26 | desiredWidth = dw; 27 | desiredHeight = dh; 28 | } 29 | void skipBytes(long numBytes); 30 | bool decode(uint8_t *buffer, size_t length); 31 | bool decodeStr(std::string buf) { 32 | return decode((uint8_t *) buf.data(), buf.size()); 33 | } 34 | 35 | int getWidth() const { 36 | return outputWidth; 37 | } 38 | 39 | int getHeight() const { 40 | return outputHeight; 41 | } 42 | 43 | val getCallback() const { 44 | return callback; 45 | } 46 | 47 | void setCallback(val cb) { 48 | callback = cb; 49 | } 50 | 51 | std::string getColorSpace() const { 52 | switch (dec.out_color_space) { 53 | case JCS_RGB: 54 | return std::string("rgb"); 55 | 56 | case JCS_GRAYSCALE: 57 | return std::string("gray"); 58 | 59 | case JCS_CMYK: 60 | return std::string("cmyk"); 61 | 62 | default: 63 | return std::string("rgb"); 64 | } 65 | } 66 | 67 | void error(char *message); 68 | 69 | private: 70 | bool readHeader(); 71 | bool startDecompress(); 72 | void findExif(); 73 | bool decompress(); 74 | 75 | jpeg_error_mgr err; 76 | jpeg_decompress_struct dec; 77 | JPEGState state; 78 | int imageWidth, imageHeight; 79 | int desiredWidth, desiredHeight; 80 | int outputWidth, outputHeight; 81 | uint8_t *output; 82 | int bytesToSkip; 83 | std::vector data; 84 | val callback; 85 | }; 86 | 87 | EMSCRIPTEN_BINDINGS(decoder) { 88 | class_("JPEGDecoder") 89 | .constructor() 90 | .property("callback", &JPEGDecoder::getCallback, &JPEGDecoder::setCallback) 91 | .function("setDesiredSize", &JPEGDecoder::setDesiredSize) 92 | .property("width", &JPEGDecoder::getWidth) 93 | .property("height", &JPEGDecoder::getHeight) 94 | .property("colorSpace", &JPEGDecoder::getColorSpace) 95 | .function("decode", &JPEGDecoder::decodeStr) 96 | ; 97 | } 98 | -------------------------------------------------------------------------------- /test/encoder.js: -------------------------------------------------------------------------------- 1 | var JPEGEncoder = require('../encoder'); 2 | var JPEGDecoder = require('../decoder'); 3 | var assert = require('assert'); 4 | var fs = require('fs'); 5 | var concat = require('concat-frames'); 6 | 7 | describe('JPEGEncoder', function() { 8 | it('encodes an RGB image', function(done) { 9 | var pixels = new Buffer(10 * 10 * 3); 10 | for (var i = 0; i < pixels.length; i += 3) { 11 | pixels[i] = 204; 12 | pixels[i + 1] = 0; 13 | pixels[i + 2] = 151; 14 | } 15 | 16 | var enc = new JPEGEncoder(10, 10); 17 | 18 | enc.pipe(new JPEGDecoder) 19 | .pipe(concat(function(frames) { 20 | assert.equal(frames.length, 1); 21 | assert.equal(frames[0].width, 10); 22 | assert.equal(frames[0].height, 10); 23 | assert.equal(frames[0].colorSpace, 'rgb'); 24 | assert.deepEqual(frames[0].pixels.slice(0, 3), new Buffer([ 204, 0, 151 ])); 25 | done(); 26 | })); 27 | 28 | enc.end(pixels); 29 | }); 30 | 31 | it('encodes a CMYK image', function(done) { 32 | var pixels = new Buffer(10 * 10 * 4); 33 | for (var i = 0; i < pixels.length; i += 4) { 34 | pixels[i] = 0; 35 | pixels[i + 1] = 56; 36 | pixels[i + 2] = 128; 37 | pixels[i + 3] = 32; 38 | } 39 | 40 | var enc = new JPEGEncoder(10, 10, { colorSpace: 'cmyk' }); 41 | 42 | enc.pipe(new JPEGDecoder) 43 | .pipe(concat(function(frames) { 44 | assert.equal(frames.length, 1); 45 | assert.equal(frames[0].width, 10); 46 | assert.equal(frames[0].height, 10); 47 | assert.equal(frames[0].colorSpace, 'cmyk'); 48 | assert.deepEqual(frames[0].pixels.slice(0, 4), new Buffer([ 0, 56, 128, 32 ])); 49 | done(); 50 | })); 51 | 52 | enc.end(pixels); 53 | }); 54 | 55 | it('encodes a grayscale image', function(done) { 56 | var pixels = new Buffer(10 * 10); 57 | pixels.fill(128); 58 | 59 | var enc = new JPEGEncoder(10, 10, { colorSpace: 'gray' }); 60 | 61 | enc.pipe(new JPEGDecoder) 62 | .pipe(concat(function(frames) { 63 | assert.equal(frames.length, 1); 64 | assert.equal(frames[0].width, 10); 65 | assert.equal(frames[0].height, 10); 66 | assert.equal(frames[0].colorSpace, 'gray'); 67 | assert.equal(frames[0].pixels[0], 128); 68 | done(); 69 | })); 70 | 71 | enc.end(pixels); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /src/encoder.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include "encoder.h" 3 | 4 | #define BUF_SIZE 8192 5 | 6 | void initDestination(jpeg_compress_struct *enc) {} 7 | void termDestination(jpeg_compress_struct *enc) {} 8 | 9 | boolean emptyOutputBuffer(jpeg_compress_struct *enc) { 10 | JPEGEncoder *encoder = static_cast(enc->client_data); 11 | encoder->emptyOutput(); 12 | return TRUE; 13 | } 14 | 15 | static void error_exit(j_common_ptr cinfo) { 16 | char buffer[JMSG_LENGTH_MAX]; 17 | (*cinfo->err->format_message) (cinfo, buffer); 18 | 19 | JPEGEncoder *encoder = static_cast(cinfo->client_data); 20 | encoder->error(buffer); 21 | } 22 | 23 | JPEGEncoder::JPEGEncoder() : callback(val::undefined()) { 24 | enc.err = jpeg_std_error(&err); 25 | err.error_exit = error_exit; 26 | jpeg_create_compress(&enc); 27 | 28 | enc.image_width = 0; 29 | enc.image_height = 0; 30 | enc.input_components = 3; 31 | enc.in_color_space = JCS_RGB; 32 | 33 | // set up the destination manager 34 | jpeg_destination_mgr *dest = (jpeg_destination_mgr *) calloc(1, sizeof(jpeg_destination_mgr)); 35 | enc.dest = dest; 36 | enc.client_data = this; 37 | 38 | dest->init_destination = initDestination; 39 | dest->empty_output_buffer = emptyOutputBuffer; 40 | dest->term_destination = termDestination; 41 | 42 | output = (uint8_t *) malloc(BUF_SIZE); 43 | dest->next_output_byte = output; 44 | dest->free_in_buffer = BUF_SIZE; 45 | 46 | quality = 100; 47 | decoding = false; 48 | scanlineLength = 0; 49 | buf.resize(0); 50 | } 51 | 52 | JPEGEncoder::~JPEGEncoder() { 53 | free(enc.dest); 54 | jpeg_destroy_compress(&enc); 55 | if (output) 56 | free(output); 57 | } 58 | 59 | void JPEGEncoder::encode(uint8_t *buffer, size_t length) { 60 | if (!decoding) { 61 | jpeg_set_defaults(&enc); 62 | jpeg_set_quality(&enc, quality, TRUE); 63 | jpeg_start_compress(&enc, TRUE); 64 | decoding = true; 65 | } 66 | 67 | buf.insert(buf.end(), buffer, buffer + length); 68 | uint8_t *data = &buf[0]; 69 | 70 | while (length >= scanlineLength) { 71 | jpeg_write_scanlines(&enc, &data, 1); 72 | data += scanlineLength; 73 | length -= scanlineLength; 74 | } 75 | 76 | buf.erase(buf.begin(), buf.end() - length); 77 | } 78 | 79 | void JPEGEncoder::emptyOutput() { 80 | callback(std::string("data"), (unsigned int) output, (size_t) BUF_SIZE); 81 | enc.dest->next_output_byte = output; 82 | enc.dest->free_in_buffer = BUF_SIZE; 83 | } 84 | 85 | void JPEGEncoder::end() { 86 | jpeg_finish_compress(&enc); 87 | 88 | size_t remaining = BUF_SIZE - enc.dest->free_in_buffer; 89 | if (remaining > 0) 90 | callback(std::string("data"), (unsigned int) output, remaining); 91 | } 92 | 93 | void JPEGEncoder::error(char *message) { 94 | callback(std::string("error"), std::string(message)); 95 | } 96 | -------------------------------------------------------------------------------- /src/encoder.h: -------------------------------------------------------------------------------- 1 | extern "C" { 2 | #include 3 | #include 4 | } 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | using namespace emscripten; 13 | 14 | class JPEGEncoder { 15 | public: 16 | JPEGEncoder(); 17 | ~JPEGEncoder(); 18 | void encode(uint8_t *buffer, size_t length); 19 | void emptyOutput(); 20 | void end(); 21 | void encodeStr(std::string buf) { 22 | encode((uint8_t *) buf.data(), buf.size()); 23 | } 24 | 25 | int getWidth() const { 26 | return enc.image_width; 27 | } 28 | 29 | void setWidth(int w) { 30 | enc.image_width = w; 31 | } 32 | 33 | int getHeight() const { 34 | return enc.image_height; 35 | } 36 | 37 | void setHeight(int h) { 38 | enc.image_height = h; 39 | } 40 | 41 | int getQuality() const { 42 | return quality; 43 | } 44 | 45 | void setQuality(int q) { 46 | quality = q; 47 | } 48 | 49 | std::string getColorSpace() const { 50 | switch (enc.in_color_space) { 51 | case JCS_RGB: 52 | return std::string("rgb"); 53 | 54 | case JCS_GRAYSCALE: 55 | return std::string("gray"); 56 | 57 | case JCS_CMYK: 58 | return std::string("cmyk"); 59 | 60 | default: 61 | return std::string("rgb"); 62 | } 63 | } 64 | 65 | void setColorSpace(std::string cs) { 66 | if (cs.compare("gray") == 0) { 67 | enc.input_components = 1; 68 | enc.in_color_space = JCS_GRAYSCALE; 69 | } else if (cs.compare("cmyk") == 0) { 70 | enc.input_components = 4; 71 | enc.in_color_space = JCS_CMYK; 72 | } else { 73 | enc.input_components = 3; 74 | enc.in_color_space = JCS_RGB; 75 | } 76 | 77 | scanlineLength = enc.image_width * enc.input_components; 78 | } 79 | 80 | val getCallback() const { 81 | return callback; 82 | } 83 | 84 | void setCallback(val cb) { 85 | callback = cb; 86 | } 87 | 88 | void error(char *message); 89 | 90 | private: 91 | struct jpeg_compress_struct enc; 92 | struct jpeg_error_mgr err; 93 | 94 | int quality; 95 | std::string colorSpace; 96 | val callback; 97 | bool decoding; 98 | int scanlineLength; 99 | std::vector buf; 100 | uint8_t *output; 101 | }; 102 | 103 | EMSCRIPTEN_BINDINGS(encoder) { 104 | class_("JPEGEncoder") 105 | .constructor() 106 | .property("callback", &JPEGEncoder::getCallback, &JPEGEncoder::setCallback) 107 | .property("width", &JPEGEncoder::getWidth, &JPEGEncoder::setWidth) 108 | .property("height", &JPEGEncoder::getHeight, &JPEGEncoder::setHeight) 109 | .property("quality", &JPEGEncoder::getQuality, &JPEGEncoder::setQuality) 110 | .property("colorSpace", &JPEGEncoder::getColorSpace, &JPEGEncoder::setColorSpace) 111 | .function("encode", &JPEGEncoder::encodeStr) 112 | .function("end", &JPEGEncoder::end) 113 | ; 114 | } 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jpg-stream 2 | 3 | A streaming JPEG encoder and decoder for Node and the browser. It is a direct compilation 4 | of [libjpeg](http://www.ijg.org) to JavaScript using [Emscripten](http://emscripten.org/). 5 | 6 | ## Installation 7 | 8 | npm install jpg-stream 9 | 10 | For the browser, you can build using [Browserify](http://browserify.org/). 11 | 12 | ## Decoding 13 | 14 | This example uses the [concat-frames](https://github.com/devongovett/concat-frames) 15 | module to collect the output of the JPEG decoder into a single buffer. 16 | It also shows how to get EXIF metadata contained in the JPEG file. 17 | 18 | ```javascript 19 | var JPEGDecoder = require('jpg-stream/decoder'); 20 | var concat = require('concat-frames'); 21 | 22 | // decode a JPEG file to RGB pixels 23 | fs.createReadStream('in.jpg') 24 | .pipe(new JPEGDecoder) 25 | .on('meta', function(meta) { 26 | // meta contains an exif object as decoded by 27 | // https://github.com/devongovett/exif-reader 28 | }) 29 | .pipe(concat(function(frames) { 30 | // frames is an array of frame objects (one for JPEGs) 31 | // each element has a `pixels` property containing 32 | // the raw RGB pixel data for that frame, as 33 | // well as the width, height, etc. 34 | })); 35 | ``` 36 | 37 | ### Scaling 38 | 39 | Large JPEGs from DSLRs can be somewhat slow to decode. If you don't need the image at 40 | its full size for preview, or will be resizing the image anyway, there is an option to 41 | perform scaling at decode time. This improves performance dramatically since only the 42 | DCT coefficients necessary for the desired size are decoded. 43 | 44 | To specify decode scaling, provide `width` and `height` options to the decoder. This 45 | represents the minimum size you want, and the decoder will output an image of at least 46 | this size, but likely not exactly that size. For exact resizing, provide your minimum 47 | allowed size to the decoder and use the [resize-pixels](https://github.com/devongovett/resize-pixels) 48 | module to resize the JPEG decoder's output to the exact size. 49 | 50 | ```javascript 51 | fs.createReadStream('large.jpg') 52 | .pipe(new JPEGDecoder({ width: 600, height: 400 })) 53 | .pipe(concat(function(frames) { 54 | // frames[0].width >= 600 and frames[0].height >= 400 55 | })); 56 | ``` 57 | 58 | ## Encoding 59 | 60 | You can encode a JPEG by writing or piping pixel data to a `JPEGEncoder` stream. 61 | You can set the `quality` option to a number between 1 and 100 to control the 62 | size vs quality tradeoff made by the encoder. 63 | 64 | The JPEG encoder supports writing data in the RGB, grayscale, or CMYK color spaces. 65 | If you need to convert from another unsupported color space, first pipe your data 66 | through the [color-transform](https://github.com/devongovett/color-transform) module. 67 | 68 | ```javascript 69 | var JPEGEncoder = require('jpg-stream/encoder'); 70 | var ColorTransform = require('color-transform'); 71 | 72 | // convert a PNG to a JPEG 73 | fs.createReadStream('in.png') 74 | .pipe(new PNGDecoder) 75 | .pipe(new JPEGEncoder({ quality: 80 })) 76 | .pipe(fs.createWriteStream('out.jpg')); 77 | 78 | // colorspace conversion to convert from RGBA to RGB 79 | fs.createReadStream('rgba.png') 80 | .pipe(new PNGDecoder) 81 | .pipe(new ColorTransform('rgb')) 82 | .pipe(new JPEGEncoder) 83 | .pipe(fs.createWriteStream('rgb.jpg')); 84 | ``` 85 | 86 | ## License 87 | 88 | MIT 89 | -------------------------------------------------------------------------------- /test/decoder.js: -------------------------------------------------------------------------------- 1 | var JPEGDecoder = require('../decoder'); 2 | var assert = require('assert'); 3 | var fs = require('fs'); 4 | var concat = require('concat-frames'); 5 | 6 | describe('JPEGDecoder', function() { 7 | it('can probe to see if a file is a jpeg', function() { 8 | var file = fs.readFileSync(__dirname + '/images/j1.jpg'); 9 | assert(JPEGDecoder.probe(file)); 10 | assert(!JPEGDecoder.probe(new Buffer(100))); 11 | }); 12 | 13 | it('decodes a file', function(done) { 14 | fs.createReadStream(__dirname + '/images/j1.jpg') 15 | .pipe(new JPEGDecoder) 16 | .pipe(concat(function(frames) { 17 | assert.equal(frames.length, 1); 18 | assert.equal(frames[0].width, 261); 19 | assert.equal(frames[0].height, 202); 20 | assert.equal(frames[0].colorSpace, 'rgb'); 21 | assert(Buffer.isBuffer(frames[0].pixels)); 22 | assert.equal(frames[0].pixels.length, 261 * 202 * 3); 23 | done(); 24 | })); 25 | }); 26 | 27 | it('decodes a progressive file', function(done) { 28 | fs.createReadStream(__dirname + '/images/j2.jpg') 29 | .pipe(new JPEGDecoder) 30 | .pipe(concat(function(frames) { 31 | assert.equal(frames.length, 1); 32 | assert.equal(frames[0].width, 261); 33 | assert.equal(frames[0].height, 202); 34 | assert.equal(frames[0].colorSpace, 'rgb'); 35 | assert(Buffer.isBuffer(frames[0].pixels)); 36 | assert.equal(frames[0].pixels.length, 261 * 202 * 3); 37 | done(); 38 | })); 39 | }); 40 | 41 | it('can decode an rgb image', function(done) { 42 | fs.createReadStream(__dirname + '/images/rgb.jpg') 43 | .pipe(new JPEGDecoder) 44 | .pipe(concat(function(frames) { 45 | assert.equal(frames.length, 1); 46 | assert.equal(frames[0].width, 620); 47 | assert.equal(frames[0].height, 371); 48 | assert.equal(frames[0].colorSpace, 'rgb'); 49 | assert(Buffer.isBuffer(frames[0].pixels)); 50 | assert.equal(frames[0].pixels.length, 620 * 371 * 3); 51 | done(); 52 | })); 53 | }); 54 | 55 | it('can decode a grayscale image', function(done) { 56 | fs.createReadStream(__dirname + '/images/gray.jpg') 57 | .pipe(new JPEGDecoder) 58 | .pipe(concat(function(frames) { 59 | assert.equal(frames.length, 1); 60 | assert.equal(frames[0].width, 620); 61 | assert.equal(frames[0].height, 371); 62 | assert.equal(frames[0].colorSpace, 'gray'); 63 | assert(Buffer.isBuffer(frames[0].pixels)); 64 | assert.equal(frames[0].pixels.length, 620 * 371); 65 | done(); 66 | })); 67 | }); 68 | 69 | it('can decode a cmyk image', function(done) { 70 | fs.createReadStream(__dirname + '/images/cmyk.jpg') 71 | .pipe(new JPEGDecoder) 72 | .pipe(concat(function(frames) { 73 | assert.equal(frames.length, 1); 74 | assert.equal(frames[0].width, 620); 75 | assert.equal(frames[0].height, 371); 76 | assert.equal(frames[0].colorSpace, 'cmyk'); 77 | assert(Buffer.isBuffer(frames[0].pixels)); 78 | assert.equal(frames[0].pixels.length, 620 * 371 * 4); 79 | done(); 80 | })); 81 | }); 82 | 83 | it('can downscale an image while decoding', function(done) { 84 | // the original image is 1600x1195 85 | // this tests that we can scale down while decoding 86 | // (much faster than decoding and then resizing afterward) 87 | fs.createReadStream(__dirname + '/images/tetons.jpg') 88 | .pipe(new JPEGDecoder({ width: 600, height: 400 })) 89 | .pipe(concat(function(frames) { 90 | assert.equal(frames.length, 1); 91 | assert.equal(frames[0].width, 800); 92 | assert.equal(frames[0].height, 598); 93 | assert.equal(frames[0].colorSpace, 'rgb'); 94 | assert(Buffer.isBuffer(frames[0].pixels)); 95 | assert.equal(frames[0].pixels.length, 800 * 598 * 3); 96 | done(); 97 | })); 98 | }); 99 | 100 | it('emits exif data', function(done) { 101 | var metaEmitted = false; 102 | 103 | fs.createReadStream(__dirname + '/images/tetons.jpg') 104 | .pipe(new JPEGDecoder({ width: 600, height: 400 })) 105 | .on('meta', function(meta) { 106 | assert.equal(typeof meta, 'object'); 107 | assert.equal(meta.image.Make, 'Canon'); 108 | assert.deepEqual(meta.exif.DateTimeOriginal, new Date("2004-06-17T06:47:02.000Z")); 109 | metaEmitted = true; 110 | }) 111 | .pipe(concat(function(frames) { 112 | assert(metaEmitted); 113 | done(); 114 | })); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /src/decoder.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "decoder.h" 5 | 6 | #define EXIF_MARKER (JPEG_APP0 + 1) 7 | 8 | void init_source(j_decompress_ptr) {} 9 | void term_source(j_decompress_ptr jd) {} 10 | 11 | void skip_input_data(j_decompress_ptr jd, long numBytes) { 12 | JPEGDecoder *decoder = static_cast(jd->client_data); 13 | decoder->skipBytes(numBytes); 14 | } 15 | 16 | boolean fill_input_buffer(j_decompress_ptr) { 17 | return FALSE; 18 | } 19 | 20 | static void error_exit(j_common_ptr cinfo) { 21 | char buffer[JMSG_LENGTH_MAX]; 22 | (*cinfo->err->format_message) (cinfo, buffer); 23 | 24 | JPEGDecoder *decoder = static_cast(cinfo->client_data); 25 | decoder->error(buffer); 26 | } 27 | 28 | JPEGDecoder::JPEGDecoder() : callback(val::undefined()) { 29 | dec.err = jpeg_std_error(&err); 30 | err.error_exit = error_exit; 31 | jpeg_create_decompress(&dec); 32 | 33 | jpeg_source_mgr *src = (jpeg_source_mgr *) calloc(1, sizeof(jpeg_source_mgr)); 34 | dec.src = src; 35 | dec.client_data = this; 36 | 37 | src->init_source = init_source; 38 | src->fill_input_buffer = fill_input_buffer; 39 | src->skip_input_data = skip_input_data; 40 | src->resync_to_restart = jpeg_resync_to_restart; 41 | src->term_source = term_source; 42 | 43 | // Keep APP1 blocks, for obtaining exif data. 44 | jpeg_save_markers(&dec, EXIF_MARKER, 0xFFFF); 45 | 46 | imageWidth = 0; 47 | imageHeight = 0; 48 | desiredWidth = 0; 49 | desiredHeight = 0; 50 | outputWidth = 0; 51 | outputHeight = 0; 52 | 53 | bytesToSkip = 0; 54 | output = NULL; 55 | 56 | state = JPEG_HEADER; 57 | } 58 | 59 | JPEGDecoder::~JPEGDecoder() { 60 | free(dec.src); 61 | jpeg_destroy_decompress(&dec); 62 | if (output) 63 | free(output); 64 | } 65 | 66 | void JPEGDecoder::skipBytes(long numBytes) { 67 | long skip = std::min(numBytes, (long) dec.src->bytes_in_buffer); 68 | dec.src->bytes_in_buffer -= (size_t) skip; 69 | dec.src->next_input_byte += skip; 70 | bytesToSkip = std::max(numBytes - skip, 0L); 71 | } 72 | 73 | bool JPEGDecoder::decode(uint8_t *buffer, size_t length) { 74 | int offset = data.size() - dec.src->bytes_in_buffer; 75 | 76 | data.erase(data.begin(), data.begin() + offset); 77 | data.insert(data.end(), buffer, buffer + length); 78 | 79 | dec.src->bytes_in_buffer += length; 80 | dec.src->next_input_byte = &data[0]; 81 | 82 | if (bytesToSkip) 83 | skipBytes(bytesToSkip); 84 | 85 | switch (state) { 86 | case JPEG_HEADER: 87 | if (!readHeader()) 88 | return false; 89 | 90 | case JPEG_START_DECOMPRESS: 91 | if (!startDecompress()) 92 | return false; 93 | 94 | case JPEG_DECOMPRESS: 95 | if (!decompress()) 96 | return false; 97 | 98 | case JPEG_DONE: 99 | return jpeg_finish_decompress(&dec); 100 | 101 | case JPEG_ERROR: 102 | return false; 103 | } 104 | 105 | return true; 106 | } 107 | 108 | bool JPEGDecoder::readHeader() { 109 | if (jpeg_read_header(&dec, TRUE) == JPEG_SUSPENDED) 110 | return false; // I/O suspension. 111 | 112 | imageWidth = dec.image_width; 113 | imageHeight = dec.image_height; 114 | callback(std::string("inputSize")); 115 | 116 | state = JPEG_START_DECOMPRESS; 117 | return true; 118 | } 119 | 120 | bool JPEGDecoder::startDecompress() { 121 | switch (dec.jpeg_color_space) { 122 | case JCS_YCbCr: 123 | case JCS_RGB: 124 | dec.out_color_space = JCS_RGB; 125 | break; 126 | 127 | case JCS_GRAYSCALE: 128 | dec.out_color_space = JCS_GRAYSCALE; 129 | break; 130 | 131 | case JCS_CMYK: 132 | case JCS_YCCK: 133 | dec.out_color_space = JCS_CMYK; 134 | break; 135 | 136 | default: 137 | callback(std::string("error"), std::string("Unknown JPEG color space")); 138 | return false; 139 | } 140 | 141 | // Calculate scale so we only decode what we need 142 | if (desiredWidth && desiredHeight) { 143 | int wdeg = imageWidth / desiredWidth; 144 | int hdeg = imageHeight / desiredHeight; 145 | dec.scale_num = 1; 146 | dec.scale_denom = std::max(1, std::min(std::min(wdeg, hdeg), 8)); 147 | } 148 | 149 | jpeg_calc_output_dimensions(&dec); 150 | outputWidth = dec.output_width; 151 | outputHeight = dec.output_height; 152 | callback(std::string("outputSize")); 153 | 154 | findExif(); 155 | 156 | if (!jpeg_start_decompress(&dec)) 157 | return false; // I/O suspension. 158 | 159 | // Allocate output buffer for a single scanline 160 | output = (uint8_t *) malloc(dec.output_width * dec.output_components); 161 | 162 | state = JPEG_DECOMPRESS; 163 | return true; 164 | } 165 | 166 | void JPEGDecoder::findExif() { 167 | for (jpeg_saved_marker_ptr marker = dec.marker_list; marker; marker = marker->next) { 168 | if (marker->marker == EXIF_MARKER && marker->data_length >= 14 && !memcmp(marker->data, "Exif", 5)) { 169 | callback(std::string("exif"), (unsigned int) marker->data, (size_t) marker->data_length); 170 | } 171 | } 172 | } 173 | 174 | bool JPEGDecoder::decompress() { 175 | while (dec.output_scanline < dec.output_height) { 176 | if (jpeg_read_scanlines(&dec, &output, 1) != 1) 177 | return false; 178 | 179 | callback(std::string("scanline"), (unsigned int) output, (size_t) (dec.output_width * dec.output_components)); 180 | } 181 | 182 | callback(std::string("end")); 183 | 184 | state = JPEG_DONE; 185 | return true; 186 | } 187 | 188 | void JPEGDecoder::error(char *message) { 189 | state = JPEG_ERROR; 190 | callback(std::string("error"), std::string(message)); 191 | } 192 | --------------------------------------------------------------------------------