├── .gitignore ├── README.md ├── index.js ├── package.json └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pixel-stream 2 | 3 | `PixelStream` is a base transform stream class for image pixel data. 4 | It propagates image metadata such as size and color space between piped streams, 5 | and makes working with images of multiple frames (e.g. animated GIFs!) much easier. 6 | 7 | ## Installation 8 | 9 | npm install pixel-stream 10 | 11 | ## API for Consumers 12 | 13 | `PixelStream` is a Node [transform stream](http://nodejs.org/api/stream.html#stream_class_stream_transform). 14 | You can write data to it manually, or pipe data to it from another stream. You can pipe more than one 15 | frame of data (as in an animated image), and the `PixelStream` will handle this properly. 16 | 17 | ### `PixelStream(width = 0, height = 0, options = {})` 18 | 19 | The constructor for a `PixelStream` accepts three optional arguments: `width`, `height`, and an 20 | object for other options. If you are not piping another stream into this one that has these 21 | properties, `width` and `height` are required. One additional option handled by the `PixelStream` 22 | base class is `colorSpace`, described below, which is set to 'rgb' by default. Other options can 23 | be handled by subclasses. 24 | 25 | ### `format` 26 | 27 | An object describing characteristics about the image, such as its `width`, `height`, 28 | `colorSpace` (e.g. rgb, rgba, gray, cmyk, etc.), and other properties. 29 | 30 | ### `addFrame(frame)` 31 | 32 | This method adds a frame metadata object describing the characteristics of a frame in an 33 | animated image. This does not include the actual pixel data for the frame, which is written 34 | through the stream in the usual way. It just describes characteristics such as frame size, etc. 35 | This frame object can be used by `PixelStream` subclasses, such as encoders, and is 36 | passed on to `PixelStream`s further down the pipes by emitting `frame` events (described below). 37 | 38 | ### `'format'` event 39 | 40 | If this event is emitted by a source stream (e.g. image decoder), which is then piped to a 41 | `PixelStream`, the `PixelStream` will use this opportunity to learn about the above image 42 | characteristics from the source stream automatically. A format object, as described above, 43 | should be passed as an argument to the event. 44 | 45 | ```javascript 46 | fs.createReadStream('in.png') 47 | .pipe(new PNGDecoder) 48 | .pipe(new MyPixelStream) 49 | ``` 50 | 51 | In the above example, the instance of `MyPixelStream` is not initialized with a `width`, `height`, 52 | or `colorSpace` since it learns those characteristics automatically when the `PNGDecoder` emits 53 | a `'format'` event. 54 | 55 | ### `'frame'` event 56 | 57 | If this event is emitted by a source stream (e.g. image decoder), which is then piped to a 58 | `PixelStream`, the frame object is added to the frame queue through the `addFrame` method 59 | described above. 60 | 61 | ## API for Subclasses 62 | 63 | The following methods can be implemented by subclasses to provide useful behavior. 64 | Only `_writePixels` is required. 65 | 66 | ### `_start(callback)` 67 | 68 | This method is called at the start of the stream, before any data has been passed to `_writePixels`. 69 | You should call the provided callback when you are done. 70 | 71 | ### `_startFrame(frame, callback)` 72 | 73 | This method is called at the start of each frame, before any data for this frame has been passed to 74 | `_writePixels`. It is passed a frame metadata object, which either came from a call to `addFrame` or 75 | piped from another stream (described above). You should call the provided callback when you are done. 76 | 77 | ### `_writePixels(data, callback)` 78 | 79 | This method is called with the actual pixel data for a frame (not necessarily all at once). 80 | You should call the provided callback when you are done. This is the only method that you MUST implement. 81 | 82 | ### `_endFrame(callback)` 83 | 84 | This method is called at the end of each frame, after all data for the frame has been passed to `_writePixels`. 85 | You should call the provided callback when you are done. 86 | 87 | ### `_end(callback)` 88 | 89 | This method is called at the end of the entire data stream for all frames. 90 | You should call the provided callback when you are done. 91 | 92 | ## Example 93 | 94 | `PixelStream` is an abstract class, which means it doesn't do much by itself. You need to extend it 95 | with your functionality to make it useful. 96 | 97 | The following example converts RGB images to grayscale. 98 | 99 | ```javascript 100 | var PixelStream = require('pixel-stream'); 101 | var inherits = require('util').inherits; 102 | 103 | function MyPixelStream() { 104 | PixelStream.apply(this, arguments); 105 | } 106 | 107 | inherits(MyPixelStream, PixelStream); 108 | 109 | MyPixelStream.prototype._writePixels = function(data, done) { 110 | if (this.colorSpace !== 'rgb') 111 | return done(new Error('Only supports rgb input')); 112 | 113 | var res = new Buffer(data.length / 3); 114 | var i = 0, j = 0; 115 | 116 | while (i < data.length) { 117 | res[j++] = 0.2126 * data[i++] + 118 | 0.7152 * data[i++] + 119 | 0.0722 * data[i++]; 120 | } 121 | 122 | this.push(res); 123 | done(); 124 | }; 125 | ``` 126 | 127 | ## License 128 | 129 | MIT 130 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Transform = require('stream').Transform; 2 | var PassThrough = require('stream').PassThrough; 3 | var util = require('util'); 4 | var copy = require('shallow-copy'); 5 | 6 | // color space component counts 7 | var COMPONENTS = { 8 | 'rgb': 3, 9 | 'rgba': 4, 10 | 'cmyk': 4, 11 | 'gray': 1, 12 | 'graya': 2, 13 | 'indexed': 1 14 | }; 15 | 16 | // pixel stream states 17 | var START = 0; 18 | var FRAME_START = 1; 19 | var FRAME_DATA = 2; 20 | var FRAME_END = 3; 21 | 22 | var EMPTY_BUFFER = new Buffer(0); 23 | 24 | function PixelStream(width, height, opts) { 25 | Transform.call(this); 26 | 27 | if (typeof width === 'object') { 28 | opts = width; 29 | width = height = 0; 30 | } 31 | 32 | this.format = { 33 | width: width || 0, 34 | height: height || 0, 35 | colorSpace: 'rgb' 36 | }; 37 | 38 | if (typeof opts === 'object') { 39 | for (var key in opts) 40 | this.format[key] = opts[key]; 41 | } 42 | 43 | this._consumed = 0; 44 | this._state = START; 45 | this._frameQueue = []; 46 | 47 | var self = this; 48 | this.once('pipe', function(src) { 49 | src.on('format', function(format) { 50 | // extend the frame object 51 | for (var key in format) 52 | self.format[key] = format[key] 53 | }); 54 | 55 | src.on('frame', this.addFrame.bind(this)); 56 | }); 57 | } 58 | 59 | util.inherits(PixelStream, Transform); 60 | 61 | /** 62 | * Adds a frame metadata object to the frame queue. 63 | * This object can represent any information about 64 | * the frame, such as its size, and will be used 65 | * when the frame is reached in the data stream. 66 | */ 67 | PixelStream.prototype.addFrame = function(frame) { 68 | this._frameQueue.push(frame); 69 | }; 70 | 71 | // Transform stream implementation 72 | PixelStream.prototype._transform = function(data, encoding, done) { 73 | var self = this; 74 | 75 | // recursive state machine to consume the given data by 76 | // calling the correct sequence of functions on our subclass 77 | function write(data) { 78 | switch (self._state) { 79 | case START: 80 | // compute the byte size of a single frame 81 | self._frameSize = self.format.width * self.format.height * COMPONENTS[self.format.colorSpace]; 82 | self._inputFormat = copy(self.format); 83 | 84 | self._start(function(err) { 85 | if (err) return done(err); 86 | 87 | // emit format object for streams further down the pipes 88 | self.emit('format', self.format); 89 | 90 | self._state = FRAME_START; 91 | write(data); 92 | }); 93 | 94 | break; 95 | 96 | case FRAME_START: 97 | var frame = self._frameQueue.shift() || {}; 98 | 99 | // if the frame object has width and height 100 | // properties, recompute the frame size. 101 | if (frame.width && frame.height) 102 | self._frameSize = frame.width * frame.height * COMPONENTS[self._inputFormat.colorSpace]; 103 | 104 | self._startFrame(frame, function(err) { 105 | if (err) return done(err); 106 | 107 | self.emit('frame', frame); 108 | self._state = FRAME_DATA; 109 | write(data); 110 | }); 111 | 112 | break; 113 | 114 | case FRAME_DATA: 115 | if (data.length === 0) 116 | return done(); 117 | 118 | // if the frame size is zero, just call frame end 119 | if (self._frameSize === 0) { 120 | self._state = FRAME_END; 121 | write(EMPTY_BUFFER); 122 | break; 123 | } 124 | 125 | var chunk = data.slice(0, self._frameSize - self._consumed); 126 | self._writePixels(chunk, function(err) { 127 | if (err) return done(err); 128 | 129 | self._consumed += chunk.length; 130 | if (self._consumed === self._frameSize) 131 | self._state = FRAME_END; 132 | 133 | write(data.slice(chunk.length)); 134 | }); 135 | 136 | break; 137 | 138 | case FRAME_END: 139 | self._endFrame(function(err) { 140 | if (err) return done(err); 141 | 142 | self._consumed = 0; 143 | self._state = FRAME_START; 144 | if (data.length) 145 | write(data); 146 | else 147 | done(); 148 | }); 149 | 150 | break; 151 | } 152 | } 153 | 154 | write(data); 155 | }; 156 | 157 | PixelStream.prototype._flush = function(done) { 158 | this._end(done); 159 | }; 160 | 161 | /** 162 | * For optional implementation by subclasses. 163 | * Called before the start of the first frame. 164 | * Implementations should the callback when done. 165 | */ 166 | PixelStream.prototype._start = function(done) { 167 | done(); 168 | }; 169 | 170 | /** 171 | * For optional implementation by subclasses. 172 | * Called at the start of each frame, including the 173 | * frame metadata. Implementations should the callback 174 | * when done. 175 | */ 176 | PixelStream.prototype._startFrame = function(frame, done) { 177 | done(); 178 | }; 179 | 180 | /** 181 | * Required to be implemented by subclasses. 182 | * Called to write pixel data to the current frame. 183 | */ 184 | PixelStream.prototype._writePixels = function(data, done) { 185 | done(new Error('No _writePixels implementation')); 186 | }; 187 | 188 | /** 189 | * For optional implementation by subclasses. 190 | * Called after each frame has been written. 191 | */ 192 | PixelStream.prototype._endFrame = function(done) { 193 | done(); 194 | }; 195 | 196 | /** 197 | * For optional implementation by subclasses. 198 | * Called after all frames have been written. 199 | */ 200 | PixelStream.prototype._end = function(done) { 201 | done(); 202 | }; 203 | 204 | module.exports = PixelStream; 205 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pixel-stream", 3 | "version": "1.0.3", 4 | "description": "A base transform stream for image pixel data", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/devongovett/pixel-stream.git" 12 | }, 13 | "author": "Devon Govett ", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/devongovett/pixel-stream/issues" 17 | }, 18 | "homepage": "https://github.com/devongovett/pixel-stream", 19 | "devDependencies": { 20 | "mocha": "^2.0.1" 21 | }, 22 | "dependencies": { 23 | "shallow-copy": "0.0.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var inherits = require('util').inherits; 2 | var PixelStream = require('../'); 3 | var assert = require('assert'); 4 | var PassThrough = require('stream').PassThrough; 5 | 6 | describe('pixel-stream', function() { 7 | describe('constructor', function() { 8 | it('should allow optional width and height', function() { 9 | var s = new PixelStream(100, 100); 10 | assert.deepEqual(s.format, { 11 | width: 100, 12 | height: 100, 13 | colorSpace: 'rgb' 14 | }); 15 | }); 16 | 17 | it('should allow colorSpace option', function() { 18 | var s = new PixelStream({ colorSpace: 'rgba' }); 19 | assert.deepEqual(s.format, { 20 | width: 0, 21 | height: 0, 22 | colorSpace: 'rgba' 23 | }); 24 | }); 25 | 26 | it('should allow any other options', function() { 27 | var s = new PixelStream({ foo: 2, bar: 'hi' }); 28 | assert.deepEqual(s.format, { 29 | width: 0, 30 | height: 0, 31 | colorSpace: 'rgb', 32 | foo: 2, 33 | bar: 'hi' 34 | }); 35 | }); 36 | 37 | it('should allow both size and options', function() { 38 | var s = new PixelStream(100, 100, { colorSpace: 'rgba' }); 39 | assert.deepEqual(s.format, { 40 | width: 100, 41 | height: 100, 42 | colorSpace: 'rgba' 43 | }); 44 | }); 45 | 46 | it('should compute frame size', function() { 47 | function TestPixelStream() { 48 | PixelStream.apply(this, arguments); 49 | } 50 | inherits(TestPixelStream, PixelStream); 51 | 52 | TestPixelStream.prototype._writePixels = function(data, done) { 53 | done(); 54 | }; 55 | 56 | var s = new TestPixelStream(100, 100); 57 | s.end(new Buffer(100 * 100 * 3 * 2)); 58 | 59 | assert.equal(s._frameSize, 100 * 100 * 3); 60 | }); 61 | 62 | it('should compute frame size for another color space', function() { 63 | function TestPixelStream() { 64 | PixelStream.apply(this, arguments); 65 | } 66 | inherits(TestPixelStream, PixelStream); 67 | 68 | TestPixelStream.prototype._writePixels = function(data, done) { 69 | done(); 70 | }; 71 | 72 | var s = new TestPixelStream(100, 100, { colorSpace: 'graya' }); 73 | s.end(new Buffer(100 * 100 * 2 * 2)); 74 | 75 | assert.equal(s._frameSize, 100 * 100 * 2); 76 | }); 77 | 78 | it('should receive and update from format events of piped source', function(done) { 79 | var p = new PassThrough; 80 | function TestPixelStream() { 81 | PixelStream.apply(this, arguments); 82 | } 83 | inherits(TestPixelStream, PixelStream); 84 | 85 | TestPixelStream.prototype._writePixels = function(data, done) { 86 | done(); 87 | }; 88 | 89 | var s = new TestPixelStream; 90 | 91 | // should forward format event 92 | s.on('format', function() { 93 | assert.deepEqual(s.format, { 94 | width: 200, 95 | height: 100, 96 | colorSpace: 'rgba' 97 | }); 98 | assert.equal(s._frameSize, 200 * 100 * 4); 99 | done(); 100 | }); 101 | 102 | p.pipe(s); 103 | 104 | // didn't update yet 105 | assert.equal(s.format.width, 0); 106 | p.emit('format', { 107 | width: 200, 108 | height: 100, 109 | colorSpace: 'rgba' 110 | }); 111 | 112 | p.end(new Buffer(200 * 100 * 4 * 2)); 113 | }); 114 | }); 115 | 116 | describe('methods', function() { 117 | it('should require subclass to implement _writePixels', function(done) { 118 | var s = new PixelStream(10, 10); 119 | s.on('error', function(err) { 120 | assert(err); 121 | assert(err instanceof Error); 122 | assert.equal(err.message, 'No _writePixels implementation'); 123 | done(); 124 | }); 125 | 126 | s.end(new Buffer(10)); 127 | }); 128 | 129 | it('should get all data', function(done) { 130 | function TestPixelStream() { 131 | PixelStream.apply(this, arguments); 132 | this.len = 0; 133 | } 134 | inherits(TestPixelStream, PixelStream); 135 | 136 | TestPixelStream.prototype._writePixels = function(data, done) { 137 | this.len += data.length; 138 | done(); 139 | }; 140 | 141 | var s = new TestPixelStream(10, 10); 142 | for (var i = 0; i < 10; i++) 143 | s.write(new Buffer(10 * 3)); 144 | 145 | s.end(function() { 146 | assert.equal(s.len, 10 * 10 * 3); 147 | done(); 148 | }); 149 | }); 150 | 151 | it('should not call _writePixels if frame size is zero', function(done) { 152 | function TestPixelStream() { 153 | PixelStream.apply(this, arguments); 154 | this.called = false; 155 | } 156 | inherits(TestPixelStream, PixelStream); 157 | 158 | TestPixelStream.prototype._writePixels = function(data, done) { 159 | this.called = true; 160 | done(); 161 | }; 162 | 163 | var s = new TestPixelStream; 164 | s.end(new Buffer(10), function() { 165 | assert.equal(s.called, false); 166 | done(); 167 | }); 168 | }); 169 | 170 | it('calls all methods in sequence', function(done) { 171 | function TestPixelStream() { 172 | PixelStream.apply(this, arguments); 173 | this.ops = []; 174 | this.len = 0; 175 | } 176 | inherits(TestPixelStream, PixelStream); 177 | 178 | TestPixelStream.prototype._start = function(done) { 179 | assert.deepEqual(this.format, { 180 | width: 10, 181 | height: 10, 182 | colorSpace: 'rgb' 183 | }); 184 | 185 | this.ops.push('start'); 186 | done(); 187 | }; 188 | 189 | TestPixelStream.prototype._startFrame = function(frame, done) { 190 | assert.equal(typeof frame, 'object'); 191 | this.ops.push('startFrame'); 192 | done(); 193 | }; 194 | 195 | TestPixelStream.prototype._writePixels = function(data, done) { 196 | this.len += data.length; 197 | this.ops.push('writePixels'); 198 | done(); 199 | }; 200 | 201 | TestPixelStream.prototype._endFrame = function(done) { 202 | this.ops.push('endFrame'); 203 | done(); 204 | }; 205 | 206 | TestPixelStream.prototype._end = function(done) { 207 | this.ops.push('end'); 208 | done(); 209 | }; 210 | 211 | var s = new TestPixelStream(10, 10); 212 | for (var i = 0; i < 10 * 4; i++) 213 | s.write(new Buffer(10 * 3)); 214 | 215 | s.end(function() { 216 | assert.equal(s.len, 10 * 10 * 3 * 4); 217 | assert.deepEqual(s.ops, [ 218 | 'start', 219 | 'startFrame', 220 | 'writePixels', 'writePixels', 'writePixels', 'writePixels', 'writePixels', 221 | 'writePixels', 'writePixels', 'writePixels', 'writePixels', 'writePixels', 222 | 'endFrame', 223 | 'startFrame', 224 | 'writePixels', 'writePixels', 'writePixels', 'writePixels', 'writePixels', 225 | 'writePixels', 'writePixels', 'writePixels', 'writePixels', 'writePixels', 226 | 'endFrame', 227 | 'startFrame', 228 | 'writePixels', 'writePixels', 'writePixels', 'writePixels', 'writePixels', 229 | 'writePixels', 'writePixels', 'writePixels', 'writePixels', 'writePixels', 230 | 'endFrame', 231 | 'startFrame', 232 | 'writePixels', 'writePixels', 'writePixels', 'writePixels', 'writePixels', 233 | 'writePixels', 'writePixels', 'writePixels', 'writePixels', 'writePixels', 234 | 'endFrame', 235 | 'end' 236 | ]); 237 | done(); 238 | }); 239 | }); 240 | 241 | it('works asynchronously', function(done) { 242 | function TestPixelStream() { 243 | PixelStream.apply(this, arguments); 244 | this.ops = []; 245 | this.len = 0; 246 | } 247 | inherits(TestPixelStream, PixelStream); 248 | 249 | TestPixelStream.prototype._start = function(done) { 250 | this.ops.push('start'); 251 | setTimeout(done, Math.random() * 10); 252 | }; 253 | 254 | TestPixelStream.prototype._startFrame = function(frame, done) { 255 | this.ops.push('startFrame'); 256 | setTimeout(done, Math.random() * 10); 257 | }; 258 | 259 | TestPixelStream.prototype._writePixels = function(data, done) { 260 | this.len += data.length; 261 | this.ops.push('writePixels'); 262 | setTimeout(done, Math.random() * 10); 263 | }; 264 | 265 | TestPixelStream.prototype._endFrame = function(done) { 266 | this.ops.push('endFrame'); 267 | setTimeout(done, Math.random() * 10); 268 | }; 269 | 270 | TestPixelStream.prototype._end = function(done) { 271 | this.ops.push('end'); 272 | setTimeout(done, Math.random() * 10); 273 | }; 274 | 275 | var s = new TestPixelStream(10, 10); 276 | for (var i = 0; i < 10 * 4; i++) 277 | s.write(new Buffer(10 * 3)); 278 | 279 | s.end(function() { 280 | assert.equal(s.len, 10 * 10 * 3 * 4); 281 | assert.deepEqual(s.ops, [ 282 | 'start', 283 | 'startFrame', 284 | 'writePixels', 'writePixels', 'writePixels', 'writePixels', 'writePixels', 285 | 'writePixels', 'writePixels', 'writePixels', 'writePixels', 'writePixels', 286 | 'endFrame', 287 | 'startFrame', 288 | 'writePixels', 'writePixels', 'writePixels', 'writePixels', 'writePixels', 289 | 'writePixels', 'writePixels', 'writePixels', 'writePixels', 'writePixels', 290 | 'endFrame', 291 | 'startFrame', 292 | 'writePixels', 'writePixels', 'writePixels', 'writePixels', 'writePixels', 293 | 'writePixels', 'writePixels', 'writePixels', 'writePixels', 'writePixels', 294 | 'endFrame', 295 | 'startFrame', 296 | 'writePixels', 'writePixels', 'writePixels', 'writePixels', 'writePixels', 297 | 'writePixels', 'writePixels', 'writePixels', 'writePixels', 'writePixels', 298 | 'endFrame', 299 | 'end' 300 | ]); 301 | done(); 302 | }); 303 | }); 304 | 305 | it('sends frame metadata objects', function(done) { 306 | function TestPixelStream() { 307 | PixelStream.apply(this, arguments); 308 | this.frames = []; 309 | } 310 | inherits(TestPixelStream, PixelStream); 311 | 312 | TestPixelStream.prototype._startFrame = function(frame, done) { 313 | assert.equal(typeof frame, 'object'); 314 | this.frames.push(frame); 315 | done(); 316 | }; 317 | 318 | TestPixelStream.prototype._writePixels = function(data, done) { 319 | done(); 320 | }; 321 | 322 | var s = new TestPixelStream(10, 10); 323 | s.addFrame({ index: 0 }); 324 | s.addFrame({ index: 1 }); 325 | s.addFrame({ index: 2 }); 326 | s.addFrame({ index: 3 }); 327 | 328 | var emittedFrames = []; 329 | s.on('frame', function(frame) { 330 | emittedFrames.push(frame); 331 | }); 332 | 333 | for (var i = 0; i < 10 * 4; i++) 334 | s.write(new Buffer(10 * 3)); 335 | 336 | s.end(function() { 337 | assert.deepEqual(s.frames, [ 338 | { index: 0 }, 339 | { index: 1 }, 340 | { index: 2 }, 341 | { index: 3 } 342 | ]); 343 | assert.deepEqual(emittedFrames, [ 344 | { index: 0 }, 345 | { index: 1 }, 346 | { index: 2 }, 347 | { index: 3 } 348 | ]); 349 | done(); 350 | }); 351 | }); 352 | 353 | it('sends frame metadata objects piped from another stream', function(done) { 354 | function TestPixelStream() { 355 | PixelStream.apply(this, arguments); 356 | this.frames = []; 357 | } 358 | inherits(TestPixelStream, PixelStream); 359 | 360 | TestPixelStream.prototype._startFrame = function(frame, done) { 361 | assert.equal(typeof frame, 'object'); 362 | this.frames.push(frame); 363 | done(); 364 | }; 365 | 366 | TestPixelStream.prototype._writePixels = function(data, done) { 367 | done(); 368 | }; 369 | 370 | var s = new TestPixelStream; 371 | var p = new PassThrough; 372 | 373 | p.pipe(s); 374 | 375 | p.emit('format', { 376 | width: 10, 377 | height: 10, 378 | colorSpace: 'rgb' 379 | }); 380 | 381 | p.emit('frame', { index: 0 }); 382 | p.emit('frame', { index: 1 }); 383 | p.emit('frame', { index: 2 }); 384 | p.emit('frame', { index: 3 }); 385 | 386 | var emittedFrames = []; 387 | s.on('frame', function(frame) { 388 | emittedFrames.push(frame); 389 | }); 390 | 391 | for (var i = 0; i < 10 * 4; i++) 392 | p.write(new Buffer(10 * 3)); 393 | 394 | p.end(function() { 395 | assert.deepEqual(s.frames, [ 396 | { index: 0 }, 397 | { index: 1 }, 398 | { index: 2 }, 399 | { index: 3 } 400 | ]); 401 | assert.deepEqual(emittedFrames, [ 402 | { index: 0 }, 403 | { index: 1 }, 404 | { index: 2 }, 405 | { index: 3 } 406 | ]); 407 | done(); 408 | }); 409 | }); 410 | 411 | it('recomputes frame size if frame object has a width and height', function(done) { 412 | function TestPixelStream() { 413 | PixelStream.apply(this, arguments); 414 | this.ops = []; 415 | this.len = 0; 416 | } 417 | inherits(TestPixelStream, PixelStream); 418 | 419 | TestPixelStream.prototype._start = function(done) { 420 | this.ops.push('start'); 421 | done(); 422 | }; 423 | 424 | TestPixelStream.prototype._startFrame = function(frame, done) { 425 | assert.equal(typeof frame, 'object'); 426 | this.ops.push('startFrame'); 427 | done(); 428 | }; 429 | 430 | TestPixelStream.prototype._writePixels = function(data, done) { 431 | this.len += data.length; 432 | this.ops.push('writePixels'); 433 | done(); 434 | }; 435 | 436 | TestPixelStream.prototype._endFrame = function(done) { 437 | this.ops.push('endFrame'); 438 | done(); 439 | }; 440 | 441 | TestPixelStream.prototype._end = function(done) { 442 | this.ops.push('end'); 443 | done(); 444 | }; 445 | 446 | var s = new TestPixelStream(10, 10); 447 | 448 | s.addFrame({ width: 100, height: 100 }); 449 | s.write(new Buffer(100 * 100 * 3)); 450 | 451 | s.addFrame({ width: 10, height: 10 }); 452 | s.write(new Buffer(10 * 10 * 3)); 453 | 454 | s.end(function() { 455 | assert.equal(s.len, 100 * 100 * 3 + 10 * 10 * 3); 456 | assert.deepEqual(s.ops, [ 457 | 'start', 458 | 'startFrame', 459 | 'writePixels', 460 | 'endFrame', 461 | 'startFrame', 462 | 'writePixels', 463 | 'endFrame', 464 | 'end' 465 | ]); 466 | done(); 467 | }); 468 | }); 469 | 470 | it('recomputes frame size from frame object correctly when input color space != output color space', function(done) { 471 | function TestPixelStream() { 472 | PixelStream.apply(this, arguments); 473 | this.ops = []; 474 | this.len = 0; 475 | } 476 | inherits(TestPixelStream, PixelStream); 477 | 478 | TestPixelStream.prototype._start = function(done) { 479 | this.format.colorSpace = 'rgba'; 480 | this.ops.push('start'); 481 | done(); 482 | }; 483 | 484 | TestPixelStream.prototype._startFrame = function(frame, done) { 485 | assert.equal(typeof frame, 'object'); 486 | this.ops.push('startFrame'); 487 | done(); 488 | }; 489 | 490 | TestPixelStream.prototype._writePixels = function(data, done) { 491 | this.len += data.length; 492 | this.ops.push('writePixels'); 493 | done(); 494 | }; 495 | 496 | TestPixelStream.prototype._endFrame = function(done) { 497 | this.ops.push('endFrame'); 498 | done(); 499 | }; 500 | 501 | TestPixelStream.prototype._end = function(done) { 502 | this.ops.push('end'); 503 | done(); 504 | }; 505 | 506 | var s = new TestPixelStream(10, 10); 507 | 508 | s.addFrame({ width: 100, height: 100 }); 509 | s.write(new Buffer(100 * 100 * 3)); 510 | assert.equal(s._frameSize, 100 * 100 * 3); 511 | 512 | s.addFrame({ width: 10, height: 10 }); 513 | s.write(new Buffer(10 * 10 * 3)); 514 | assert.equal(s._frameSize, 10 * 10 * 3); 515 | 516 | s.end(function() { 517 | assert.equal(s.len, 100 * 100 * 3 + 10 * 10 * 3); 518 | assert.deepEqual(s.ops, [ 519 | 'start', 520 | 'startFrame', 521 | 'writePixels', 522 | 'endFrame', 523 | 'startFrame', 524 | 'writePixels', 525 | 'endFrame', 526 | 'end' 527 | ]); 528 | done(); 529 | }); 530 | }); 531 | }); 532 | }); 533 | --------------------------------------------------------------------------------