-
Notifications
You must be signed in to change notification settings - Fork 11
/
index.js
375 lines (322 loc) · 10.4 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
var Stream = require('stream').Stream;
var util = require('util');
var bufferEqual = require('buffer-equal');
var BitReader = require('bitreader');
var Chunk = require('./lib/chunk.js');
/**
* Constructor
*
* @param {Buffer|Stream} input
* @return instance
*/
function StreamPng(input) {
if (!(this instanceof StreamPng))
return new StreamPng(input);
this.parser = new BitReader();
this.strict = true;
this.writable = true;
this.readable = true;
this.finished = false;
this.chunks = [];
this.injections = []
// Input can be either a buffer or a stream. When we get a buffer, we can
// pretend it's a stream by writing the entire buffer at once, as if we just
// got a single `data` event. If it's not a buffer, check whether input
// quacks like a stream and pipe it back to this instance. Otherwise throw
// an error.
if (input) {
// is it a buffer?
if (Buffer.isBuffer(input))
this.end(input);
// does it quack like a stream?
else if (typeof input.pipe === 'function')
input.pipe(this);
// can't handle it.
else {
var err = new TypeError('PNG constructor takes either a buffer or a stream');
err.input = input;
err.inputType = typeof input;
throw err;
}
}
}
util.inherits(StreamPng, Stream);
StreamPng.Chunk = Chunk;
/**
* Emit on the nextTick to allow for late-bound listeners.
*
* @see EventEmitter#emit
* @return instance
*/
StreamPng.prototype.delayEmit = function delayEmit() {
var args = arguments;
process.nextTick(function () {
this.emit.apply(this, args);
}.bind(this));
return this;
};
/**
* Handle data input. Re-emits incoming data as a `data` event to allow
* for transparent piping to another Writable Stream.
*
* @param {Buffer} data
* @emits {'data', Buffer} same as incoming data
* @return instance
*/
StreamPng.prototype.write = function write(data) {
if (!data) return this;
this.delayEmit('data', data);
this.parser.write(data);
this.process();
return this;
};
/**
* Called when either the input or output stream has ended.
*
* When the `data` argument exists, call `Stream#write` first. Sets
* `this.writable` to false so no more data can be piped in and
* `this.finished` to true so methods that require the fully parsed PNG
* like `StreamPng#out` and `StreamPng#inject` know that they can execute
* immediately instead of listening for the `end` event.
*
* @see StreamPng#write
* @see StreamPng#processInjections
* @return instance
*/
StreamPng.prototype.end = function end(data) {
if (data) this.write(data);
this.writable = false;
this.finished = true;
this.delayEmit('end', this.chunks);
this.processInjections();
return this;
};
/**
* Pushes a chunk to main internal list and chunk-specific list.
*
* @return instance
*/
StreamPng.prototype.addChunk = function (chunk) {
this.chunks.push(chunk);
if (this[chunk.type]) {
this[chunk.type].push(chunk);
} else {
this[chunk.type] = [chunk];
}
return this;
};
/**
* Ensure that this is valid png by checking the signature. Emits a
* `signature` event if successful, an `error` event if the signature
* is not valid. Calls `this.process` to continue processing when a valid
* signature is found.
*
* @emits {'signature'}
* @emits {'error', TypeError}
* @return instance
* @see StreamPng#process
*/
StreamPng.prototype._readSignature = function readSignature() {
var parser = this.parser;
var validSignature = StreamPng.SIGNATURE;
var bufferLength = parser.getBuffer().length;
var signatureLength = validSignature.length;
var possibleSignature;
// Make sure we have enough bytes to read the signature, then get the
// signature and compare it to the known valid PNG signature. Emit an
// error and set this stream to be non-writable if there's a mismatch,
// otherwise emit a `signature` event and continue processing.
if (bufferLength < signatureLength)
return this;
possibleSignature = parser.eat(signatureLength);
if (!bufferEqual(possibleSignature, validSignature)) {
this.delayEmit('error', new TypeError('Signature mismatch, not a PNG'));
this.writable = false;
return this;
}
this.delayEmit('signature');
this.process();
return this;
};
/**
* Attempt to parse a chunk. Recurses until there aren't enough bytes
* in the parser to be able to read a new chunk or until the end of the
* PNG has been reached.
*
* @emits {chunkType, Chunk}
* @emits {'chunk', Chunk}
* @emits {'imagedata begin'}
* @return instance
* @see StreamPng#_readSignature
* @see StreamPng#addChunk
*/
StreamPng.prototype.process = function () {
var parser = this.parser;
if (parser.position() < 8)
return this._readSignature();
// We need to make sure there are enough bytes in the buffer to read
// the entire chunk. If there are less than four bytes, we can't even
// read the length of the chunk, so we'll return and wait for the next
// `write`.
var remaining = parser.remaining();
if (!remaining || remaining < 4)
return this;
// If we can read the length but there aren't enough bytes to read
// through the end of the chunk, wait for the next `write`.
var dataLength = parser.peak(4).readUInt32BE(0);
dataLength += StreamPng.TYPE_LENGTH + StreamPng.CRC_LENGTH;
if (remaining < dataLength)
return this;
// #XXX: Until we can think of something better to do, just pass errors
// straight through.
try {
var chunk = new Chunk(parser, this.IHDR);
} catch (err) {
this.delayEmit('error', err);
return this;
}
// When we detect the first image data chunk, process the chunk injection
// list before adding the image data chunk to the internal list. Also emit
// convenience events so the user knows when the metadata section ended.
if (chunk.type === 'IDAT' && !this.IDAT)
this.delayEmit('imagedata begin');
// #TODO: Give an option for reading `transparently`, to save memory,
// meaning that we don't store the chunks and only emit them. Useful if
// the user doesn't care about re-writing the PNG.
this.addChunk(chunk);
this.delayEmit('chunk', chunk);
this.delayEmit(chunk.type, chunk);
if (chunk.type !== 'IEND')
this.process();
return this;
};
/**
* Queues a new chunk for injection into the stream with an optional
* condition for inclusion.
*
* If the input stream is finished parsing, processes the chunk immediately.
*
* The condition will be called once for every chunk that exists with the
* parameter `existingChunk`. It should return either `true` or `false`.
* If any `condition(existingChunk)` call returns `false`, injection
* will not occur.
*
* @param {Chunk} chunk
* @param {Function} condition `function(existingChunk) {...}`
* @return instance
* @see `StreamPng#processInjections`
*/
StreamPng.prototype.inject = function inject(chunk, opts, condition) {
var defaults = { test: 'same' };
if (typeof opts === 'function')
condition = opts, opts = defaults;
if (!condition) condition = true;
this.injections.push({ chunk: chunk, opts: opts, condition: condition });
if (this.finished) this.processInjections();
return this;
};
/**
* Process the queued injections. Inserts all chunks right
* after the IHDR chunk.
*
* #TODO: make sure inserting after the IHDR chunk is the most reasonable
* thing to do. This obviously won't work for things like IDAT chunks, but
* those probably shouldn't be supported by `inject()`.
*
* @return instance
*/
StreamPng.prototype.processInjections = function () {
if (!this.injections.length) return;
var all = this.chunks;
var additions = [];
var idx;
this.injections.forEach(function (chunk) {
var options = chunk.opts;
var chunkType = chunk.chunk.type;
var condition = chunk.condition;
if (condition === true)
return additions.push(chunk.chunk);
for (idx = 0; idx < all.length; idx++) {
if (options.test === 'same' && all[idx].type !== chunkType)
continue;
if (condition(all[idx]) === false)
return false;
}
additions.push(chunk.chunk);
});
this.injections = [];
// Splice in the new chunks right after the the IHDR chunk.
var splicer = all.splice.bind(all, 1, 0);
splicer.apply(null, additions);
return this;
};
/**
* Output the buffer for the PNG. Works either callback or stream style.
*
* Callback example:
* ```
* png.out(function (buf) {
* fs.writeFile('example.png', buf, function() {
* console.log('done');
* });
* });
* ```
*
* Stream example:
* ```
* var infile = fs.createWriteStream('example.png');
* png.out().pipe(infile).once('close', function() {
* console.log('done');
* });
* ```
*
* @param {Function} callback `function(buffer) { ... }`
* @param {Stream} _stream internal, should not use.
* @return {Stream} a `Readable Stream` ready to be piped somewhere
*/
StreamPng.prototype.out = function out(callback, _stream) {
var stream = _stream || new Stream();
stream.readable = true;
var chunks = this.chunks;
var expect = chunks.length;
var hits = 0;
var buffers = [];
var signature = StreamPng.SIGNATURE;
// We don't want to force the user to manually listen for the end
// event before calling `out`. If we know it's not done parsing yet,
// bind the callback and the output stream to this function and setup
// a listener for them.
if (!this.finished) {
var boundFn = this.out.bind(this, callback, stream);
this.once('end', boundFn);
return stream;
}
// Loop over all of the chunks in order and get their buffers. Whenever
// their callback returns, stick them in an array, indexed by their
// original order. We expect `chunks.length` hits and once that
// expectation has been met, we push the PNG signature to the beginning
// of the array, `Buffer.concat` the whole thing and send it off.
function proceed(buffer, idx) {
buffers[idx] = buffer;
if (++hits !== expect) return;
var output;
buffers.unshift(signature);
output = Buffer.concat(buffers)
if (callback) callback(null, output);
process.nextTick(function () {
stream.emit('data', output);
stream.emit('end', output);
stream.readable = false;
});
}
chunks.forEach(function (chunk, idx) {
if (chunk._buffer) return proceed(chunk._buffer, idx);
chunk.out(function (buf) { proceed(buf, idx) });
});
return stream;
};
StreamPng.prototype.destroy = function noop() {};
StreamPng.SIGNATURE = Buffer([137, 80, 78, 71, 13, 10, 26, 10]);
StreamPng.TYPE_LENGTH = 4;
StreamPng.CRC_LENGTH = 4;
module.exports = StreamPng;