Merge branch 'streams'

This commit is contained in:
Kuba Niegowski 2012-08-22 23:47:50 +02:00
commit 7f843dec72
14 changed files with 795 additions and 487 deletions

View file

@ -24,7 +24,7 @@ png.on('parsed', function() {
} }
} }
png.pack(); png.pack().pipe(dst);
}); });
src.pipe(png).pipe(dst); src.pipe(png);

View file

@ -1,31 +0,0 @@
#!/usr/bin/env node
var fs = require('fs'),
PNG = require('../lib/png').PNG;
fs.readFile(process.argv[2], function(err, data) {
if (err) throw err;
var png = new PNG();
png.parse(data, function(err) {
if (err) console.log(err.stack);
for (var y = 0; y < png.height; y++) {
for (var x = 0; x < png.width; x++) {
var idx = (png.width * y + x) << 2;
// invert color
png.data[idx] = 255 - png.data[idx];
png.data[idx+1] = 255 - png.data[idx+1];
png.data[idx+2] = 255 - png.data[idx+2];
// and reduce opacity
png.data[idx+3] = png.data[idx+3] >> 1;
}
}
png.pipe(fs.createWriteStream(process.argv[3] || 'out.png'));
png.pack();
});
});

View file

@ -24,5 +24,4 @@ for (var y = 0; y < png.height; y++) {
} }
} }
png.pipe(fs.createWriteStream('bg.png')); png.pack().pipe(fs.createWriteStream(__dirname + '/bg.png'));
png.pack();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 B

After

Width:  |  Height:  |  Size: 93 B

0
examples/test/list.html Normal file → Executable file
View file

17
examples/test/test.js Normal file → Executable file
View file

@ -6,12 +6,12 @@ var fs = require('fs'),
fs.readdir(__dirname + '/img/', function(err, files) { fs.readdir(__dirname + '/img/', function(err, files) {
if (err) throw err; if (err) throw err;
for (var i = 0; i < files.length; i++) { files.forEach(function(file) {
if (!files[i].match(/\.png$/i)) if (!file.match(/\.png$/i))
continue; return;
fs.createReadStream(__dirname + '/img/' + files[i]) fs.createReadStream(__dirname + '/img/' + file)
.pipe(new PNG()) .pipe(new PNG())
.on('parsed', function() { .on('parsed', function() {
@ -29,7 +29,10 @@ fs.readdir(__dirname + '/img/', function(err, files) {
} }
} }
this.pack(); this.pack()
}).pipe(fs.createWriteStream(__dirname + '/out/' + files[i])); .pipe(fs.createWriteStream(__dirname + '/out/' + file));
}
});
});
}); });

199
lib/chunkstream.js Executable file
View file

@ -0,0 +1,199 @@
// Copyright (c) 2012 Kuba Niegowski
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
var util = require('util'),
Stream = require('stream');
var ChunkStream = module.exports = function() {
Stream.call(this);
this._buffers = [];
this._buffered = 0;
this._reads = [];
this._paused = false;
this._encoding = 'utf8';
this.writable = true;
};
util.inherits(ChunkStream, Stream);
ChunkStream.prototype.read = function(length, callback) {
this._reads.push({
length: Math.abs(length), // if length < 0 then at most this length
allowLess: length < 0,
func: callback
});
this._process();
// its paused and there is not enought data then ask for more
if (this._paused && this._reads.length > 0) {
this._paused = false;
this.emit('drain');
}
};
ChunkStream.prototype.write = function(data, encoding) {
if (!this.writable) {
this.emit('error', new Error('Stream not writable'));
return false;
}
if (!Buffer.isBuffer(data))
data = new Buffer(data, encoding || this._encoding);
this._buffers.push(data);
this._buffered += data.length;
this._process();
// ok if there are no more read requests
if (this._reads && this._reads.length == 0)
this._paused = true;
return this.writable && !this._paused;
};
ChunkStream.prototype.end = function(data, encoding) {
if (!this.writable) {
this.emit('error', new Error('Stream not writable'));
return false;
}
if (data) this.write(data, encoding);
this.writable = false;
if (this._buffers.length == 0) {
this._end();
} else {
this._buffers.push(null);
this._process();
}
};
ChunkStream.prototype.destroySoon = ChunkStream.prototype.end;
ChunkStream.prototype._end = function() {
if (this._reads.length > 0) {
this.emit('error',
new Error('There are some read requests waitng on finished stream')
);
}
this.destroy();
};
ChunkStream.prototype.destroy = function() {
if (!this._buffers) return;
this.writable = false;
this._reads = null;
this._buffers = null;
this.emit('close');
};
ChunkStream.prototype._process = function() {
// as long as there is any data and read requests
while (this._buffered > 0 && this._reads.length > 0) {
var read = this._reads[0];
// read any data (but no more than length)
if (read.allowLess) {
// ok there is any data so that we can satisfy this request
this._reads.shift(); // == read
// first we need to peek into first buffer
var buf = this._buffers[0];
// ok there is more data than we need
if (buf.length > read.length) {
this._buffered -= read.length;
this._buffers[0] = buf.slice(read.length);
read.func.call(this, buf.slice(0, read.length));
} else {
// ok this is less than maximum length so use it all
this._buffered -= buf.length;
this._buffers.shift(); // == buf
read.func.call(this, buf);
}
} else if (this._buffered >= read.length) {
// ok we can meet some expectations
this._reads.shift(); // == read
var pos = 0,
count = 0,
data = new Buffer(read.length);
// create buffer for all data
while (pos < read.length) {
var buf = this._buffers[count++],
len = Math.min(buf.length, read.length - pos);
buf.copy(data, pos, 0, len);
pos += len;
// last buffer wasn't used all so just slice it and leave
if (len != buf.length)
this._buffers[--count] = buf.slice(len);
}
// remove all used buffers
if (count > 0)
this._buffers.splice(0, count);
this._buffered -= read.length;
read.func.call(this, data);
} else {
// not enought data to satisfy first request in queue
// so we need to wait for more
break;
}
}
if (this._buffers && this._buffers.length > 0 && this._buffers[0] == null) {
this._end();
}
};

View file

@ -1,94 +0,0 @@
// Copyright (c) 2012 Kuba Niegowski
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
var util = require('util'),
zlib = require('zlib'),
events = require('events');
var Compress = module.exports = function(options) {
events.EventEmitter.call(this);
this._options = options;
options.deflateChunkSize = options.deflateChunkSize || 32 * 1024;
options.deflateLevel = options.deflateLevel || 9;
this._inflate = null;
};
util.inherits(Compress, events.EventEmitter);
Compress.prototype.prepareInflate = function(type) {
if (type != 0)
throw new Error('Unsupported compression method');
this._inflate = zlib.createInflate();
bufferStream(this._inflate, function(data) {
this._inflate = null;
this.emit('inflated', data);
}.bind(this));
this._inflate.on('error', this.emit.bind(this, 'error'));
};
Compress.prototype.writeInflate = function(data) {
this._inflate.write(data);
};
Compress.prototype.endInflate = function() {
this._inflate.end();
};
Compress.prototype.deflate = function(data) {
var deflate = zlib.createDeflate({
chunkSize: this._options.deflateChunkSize,
level: this._options.deflateLevel
});
bufferStream(deflate, function(data) {
this.emit('deflated', data);
}.bind(this));
deflate.on('error', this.emit.bind(this, 'error'));
deflate.end(data);
};
function bufferStream(stream, callback) {
var buffers = [],
length = 0;
stream.on('data', function(data) {
buffers.push(data);
length += data.length;
});
stream.on('end', function() {
callback(Buffer.concat(buffers, length));
});
return stream;
};

38
lib/constants.js Executable file
View file

@ -0,0 +1,38 @@
// Copyright (c) 2012 Kuba Niegowski
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
module.exports = {
PNG_SIGNATURE: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
TYPE_IHDR: 0x49484452,
TYPE_IEND: 0x49454e44,
TYPE_IDAT: 0x49444154,
TYPE_PLTE: 0x504c5445,
TYPE_tRNS: 0x74524e53,
TYPE_gAMA: 0x67414d41,
COLOR_PALETTE: 1,
COLOR_COLOR: 2,
COLOR_ALPHA: 4
};

79
lib/crc.js Executable file
View file

@ -0,0 +1,79 @@
// Copyright (c) 2012 Kuba Niegowski
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
var util = require('util'),
Stream = require('stream');
var CrcStream = module.exports = function() {
Stream.call(this);
this._crc = -1;
this.writable = true;
};
util.inherits(CrcStream, Stream);
CrcStream.prototype.write = function(data) {
for (var i = 0; i < data.length; i++) {
this._crc = crcTable[(this._crc ^ data[i]) & 0xff] ^ (this._crc >>> 8);
}
return true;
};
CrcStream.prototype.end = function(data) {
if (data) this.write(data);
this.emit('crc', this.crc32());
};
CrcStream.prototype.crc32 = function() {
return this._crc ^ -1;
};
CrcStream.crc32 = function(buf) {
var crc = -1;
for (var i = 0; i < buf.length; i++) {
crc = crcTable[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
}
return crc ^ -1;
};
var crcTable = [];
for (var i = 0; i < 256; i++) {
var c = i;
for (var j = 0; j < 8; j++) {
if (c & 1) {
c = 0xedb88320 ^ (c >>> 1);
} else {
c = c >>> 1;
}
}
crcTable[i] = c;
}

205
lib/filter.js Normal file → Executable file
View file

@ -22,17 +22,21 @@
var util = require('util'), var util = require('util'),
zlib = require('zlib'), zlib = require('zlib'),
events = require('events'); ChunkStream = require('./chunkstream');
var Filter = module.exports = function(options) { var Filter = module.exports = function(width, height, Bpp, data, options) {
events.EventEmitter.call(this); ChunkStream.call(this);
this._width = width;
this._height = height;
this._Bpp = Bpp;
this._data = data;
this._options = options; this._options = options;
options.filterType = 'filterType' in options ? options.filterType : -1;
this._width = 0; this._line = 0;
this._height = 0;
options.filterType = 'filterType' in options ? options.filterType : -1;
this._filters = { this._filters = {
0: this._filterNone.bind(this), 0: this._filterNone.bind(this),
@ -42,134 +46,135 @@ var Filter = module.exports = function(options) {
4: this._filterPaeth.bind(this) 4: this._filterPaeth.bind(this)
}; };
this.read(this._width * Bpp + 1, this._reverseFilterLine.bind(this));
}; };
util.inherits(Filter, events.EventEmitter); util.inherits(Filter, ChunkStream);
Filter.prototype.prepare = function(width, height, type) { var pixelBppMap = {
1: { // L
if (type != 0) 0: 0,
throw new Error('Unsupported filter method'); 1: 0,
2: 0,
this.width = width; 3: 0xff,
this.height = height; },
2: { // LA
0: 0,
1: 0,
2: 0,
3: 1
},
3: { // RGB
0: 0,
1: 1,
2: 2,
3: 0xff
},
4: { // RGBA
0: 0,
1: 1,
2: 2,
3: 3
}
}; };
Filter.prototype.unfilter = function(rawData, Bpp) { Filter.prototype._reverseFilterLine = function(rawData) {
var pxLineLength = this.width << 2,
rawLineLength = this.width * Bpp + 1,
pxData = new Buffer(pxLineLength * this.height);
for (var y = 0; y < this.height; y++) {
var rawRowPos = rawLineLength * y + 1,
pxRowPos = y * pxLineLength,
filter = rawData[rawRowPos - 1];
var pxData = this._data,
pxLineLength = this._width << 2,
pxRowPos = this._line * pxLineLength,
filter = rawData[0];
if (filter == 0) { if (filter == 0) {
for (var x = 0; x < this.width; x++) { for (var x = 0; x < this._width; x++) {
var pxPos = pxRowPos + (x << 2), var pxPos = pxRowPos + (x << 2),
rawPos = rawRowPos + x * Bpp; rawPos = 1 + x * this._Bpp;
for (var i = 0; i < Bpp; i++) for (var i = 0; i < 4; i++) {
pxData[pxPos + i] = rawData[rawPos + i]; var idx = pixelBppMap[this._Bpp][i];
pxData[pxPos + i] = idx != 0xff ? rawData[rawPos + idx] : 0xff;
}
} }
} else if (filter == 1) { } else if (filter == 1) {
for (var x = 0; x < this.width; x++) { for (var x = 0; x < this._width; x++) {
var pxPos = pxRowPos + (x << 2), var pxPos = pxRowPos + (x << 2),
rawPos = rawRowPos + x * Bpp; rawPos = 1 + x * this._Bpp;
for (var i = 0; i < Bpp; i++) { for (var i = 0; i < 4; i++) {
var left = x > 0 ? pxData[pxPos + i - 4] : 0; var idx = pixelBppMap[this._Bpp][i],
pxData[pxPos + i] = rawData[rawPos + i] + left; left = x > 0 ? pxData[pxPos + i - 4] : 0;
pxData[pxPos + i] = idx != 0xff ? rawData[rawPos + idx] + left : 0xff;
} }
} }
} else if (filter == 2) { } else if (filter == 2) {
for (var x = 0; x < this.width; x++) { for (var x = 0; x < this._width; x++) {
var pxPos = pxRowPos + (x << 2), var pxPos = pxRowPos + (x << 2),
rawPos = rawRowPos + x * Bpp; rawPos = 1 + x * this._Bpp;
for (var i = 0; i < Bpp; i++) { for (var i = 0; i < 4; i++) {
var up = y > 0 ? pxData[pxPos - pxLineLength + i] : 0; var idx = pixelBppMap[this._Bpp][i],
pxData[pxPos + i] = rawData[rawPos + i] + up; up = this._line > 0 ? pxData[pxPos - pxLineLength + i] : 0;
pxData[pxPos + i] = idx != 0xff ? rawData[rawPos + idx] + up : 0xff;
} }
} }
} else if (filter == 3) { } else if (filter == 3) {
for (var x = 0; x < this.width; x++) { for (var x = 0; x < this._width; x++) {
var pxPos = pxRowPos + (x << 2), var pxPos = pxRowPos + (x << 2),
rawPos = rawRowPos + x * Bpp; rawPos = 1 + x * this._Bpp;
for (var i = 0; i < Bpp; i++) { for (var i = 0; i < 4; i++) {
var left = x > 0 ? pxData[pxPos + i - 4] : 0, var idx = pixelBppMap[this._Bpp][i],
up = y > 0 ? pxData[pxPos - pxLineLength + i] : 0; left = x > 0 ? pxData[pxPos + i - 4] : 0,
up = this._line > 0 ? pxData[pxPos - pxLineLength + i] : 0,
add = Math.floor((left + up) / 2);
pxData[pxPos + i] = rawData[rawPos + i] pxData[pxPos + i] = idx != 0xff ? rawData[rawPos + idx] + add : 0xff;
+ Math.floor((left + up) / 2);
} }
} }
} else if (filter == 4) { } else if (filter == 4) {
for (var x = 0; x < this.width; x++) { for (var x = 0; x < this._width; x++) {
var pxPos = pxRowPos + (x << 2), var pxPos = pxRowPos + (x << 2),
rawPos = rawRowPos + x * Bpp; rawPos = 1 + x * this._Bpp;
for (var i = 0; i < Bpp; i++) { for (var i = 0; i < 4; i++) {
var left = x > 0 ? pxData[pxPos + i - 4] : 0, var idx = pixelBppMap[this._Bpp][i],
up = y > 0 ? pxData[pxPos - pxLineLength + i] : 0, left = x > 0 ? pxData[pxPos + i - 4] : 0,
upLeft = x > 0 && y > 0 up = this._line > 0 ? pxData[pxPos - pxLineLength + i] : 0,
? pxData[pxPos - pxLineLength + i - 4] : 0; upLeft = x > 0 && this._line > 0
? pxData[pxPos - pxLineLength + i - 4] : 0,
add = PaethPredictor(left, up, upLeft);
pxData[pxPos + i] = rawData[rawPos + i] pxData[pxPos + i] = idx != 0xff ? rawData[rawPos + idx] + add : 0xff;
+ PaethPredictor(left, up, upLeft)
}
} }
} }
} }
// expand data to 32 bit
for (var y = 0; y < this.height; y++) {
var pxRowPos = y * pxLineLength;
if (Bpp == 1) { // L this._line++;
for (var x = 0; x < this.width; x++) {
var pxPos = pxRowPos + (x << 2);
pxData[pxPos + 1] = pxData[pxPos + 2] = pxData[pxPos]; if (this._line < this._height)
pxData[pxPos + 3] = 0xff; this.read(this._width * this._Bpp + 1, this._reverseFilterLine.bind(this));
} else
this.emit('complete', this._data, this._width, this._height);
} else if (Bpp == 2) { // LA
for (var x = 0; x < this.width; x++) {
var pxPos = pxRowPos + (x << 2);
pxData[pxPos + 3] = pxData[pxPos + 1];
pxData[pxPos + 1] = pxData[pxPos + 2] = pxData[pxPos];
}
} else if (Bpp == 3) { // RGB
for (var x = 0; x < this.width; x++) {
var pxPos = pxRowPos + (x << 2);
pxData[pxPos + 3] = 0xff;
}
} // else RGBA
}
return pxData;
}; };
Filter.prototype.filter = function(pxData, width, height) {
var rawData = new Buffer(((width << 2) + 1) * height);
for (var y = 0; y < height; y++) { Filter.prototype.filter = function() {
var pxData = this._data,
rawData = new Buffer(((this._width << 2) + 1) * this._height);
for (var y = 0; y < this._height; y++) {
// find best filter for this line (with lowest sum of values) // find best filter for this line (with lowest sum of values)
if (this._options.filterType == -1) { if (this._options.filterType == -1) {
@ -177,7 +182,7 @@ Filter.prototype.filter = function(pxData, width, height) {
sel = 0; sel = 0;
for (var f in this._filters) { for (var f in this._filters) {
var sum = this._filters[f](pxData, y, width, height, null); var sum = this._filters[f](pxData, y, null);
if (sum < min) { if (sum < min) {
sel = f; sel = f;
min = sum; min = sum;
@ -187,14 +192,14 @@ Filter.prototype.filter = function(pxData, width, height) {
} else { } else {
sel = this._options.filterType; sel = this._options.filterType;
} }
this._filters[sel](pxData, y, width, height, rawData); this._filters[sel](pxData, y, rawData);
} }
return rawData; return rawData;
}; };
Filter.prototype._filterNone = function(pxData, y, width, height, rawData) { Filter.prototype._filterNone = function(pxData, y, rawData) {
var pxRowLength = width << 2, var pxRowLength = this._width << 2,
rawRowLength = pxRowLength + 1, rawRowLength = pxRowLength + 1,
sum = 0; sum = 0;
@ -210,9 +215,9 @@ Filter.prototype._filterNone = function(pxData, y, width, height, rawData) {
return sum; return sum;
}; };
Filter.prototype._filterSub = function(pxData, y, width, height, rawData) { Filter.prototype._filterSub = function(pxData, y, rawData) {
var pxRowLength = width << 2, var pxRowLength = this._width << 2,
rawRowLength = pxRowLength + 1, rawRowLength = pxRowLength + 1,
sum = 0; sum = 0;
@ -230,9 +235,9 @@ Filter.prototype._filterSub = function(pxData, y, width, height, rawData) {
return sum; return sum;
}; };
Filter.prototype._filterUp = function(pxData, y, width, height, rawData) { Filter.prototype._filterUp = function(pxData, y, rawData) {
var pxRowLength = width << 2, var pxRowLength = this._width << 2,
rawRowLength = pxRowLength + 1, rawRowLength = pxRowLength + 1,
sum = 0; sum = 0;
@ -250,9 +255,9 @@ Filter.prototype._filterUp = function(pxData, y, width, height, rawData) {
return sum; return sum;
}; };
Filter.prototype._filterAvg = function(pxData, y, width, height, rawData) { Filter.prototype._filterAvg = function(pxData, y, rawData) {
var pxRowLength = width << 2, var pxRowLength = this._width << 2,
rawRowLength = pxRowLength + 1, rawRowLength = pxRowLength + 1,
sum = 0; sum = 0;
@ -271,9 +276,9 @@ Filter.prototype._filterAvg = function(pxData, y, width, height, rawData) {
return sum; return sum;
}; };
Filter.prototype._filterPaeth = function(pxData, y, width, height, rawData) { Filter.prototype._filterPaeth = function(pxData, y, rawData) {
var pxRowLength = width << 2, var pxRowLength = this._width << 2,
rawRowLength = pxRowLength + 1, rawRowLength = pxRowLength + 1,
sum = 0; sum = 0;

108
lib/packer.js Executable file
View file

@ -0,0 +1,108 @@
// Copyright (c) 2012 Kuba Niegowski
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
var util = require('util'),
Stream = require('stream'),
zlib = require('zlib'),
Filter = require('./filter'),
CrcStream = require('./crc'),
constants = require('./constants');
var Packer = module.exports = function(options) {
Stream.call(this);
this._options = options;
options.deflateChunkSize = options.deflateChunkSize || 32 * 1024;
options.deflateLevel = options.deflateLevel || 9;
this.readable = true;
};
util.inherits(Packer, Stream);
Packer.prototype.pack = function(data, width, height) {
// Signature
this.emit('data', new Buffer(constants.PNG_SIGNATURE));
this.emit('data', this._packIHDR(width, height));
// filter pixel data
var filter = new Filter(width, height, 4, data, this._options);
var data = filter.filter();
// compress it
var deflate = zlib.createDeflate({
chunkSize: this._options.deflateChunkSize,
level: this._options.deflateLevel
});
deflate.on('error', this.emit.bind(this, 'error'));
deflate.on('data', function(data) {
this.emit('data', this._packIDAT(data));
}.bind(this));
deflate.on('end', function() {
this.emit('data', this._packIEND());
this.emit('end');
}.bind(this));
deflate.end(data);
};
Packer.prototype._packChunk = function(type, data) {
var len = (data ? data.length : 0),
buf = new Buffer(len + 12);
buf.writeUInt32BE(len, 0);
buf.writeUInt32BE(type, 4);
if (data) data.copy(buf, 8);
buf.writeInt32BE(CrcStream.crc32(buf.slice(4, buf.length - 4)), buf.length - 4);
return buf;
};
Packer.prototype._packIHDR = function(width, height) {
var buf = new Buffer(13);
buf.writeUInt32BE(width, 0);
buf.writeUInt32BE(height, 4);
buf[8] = 8;
buf[9] = 6; // colorType
buf[10] = 0; // compression
buf[11] = 0; // filter
buf[12] = 0; // interlace
return this._packChunk(constants.TYPE_IHDR, buf);
};
Packer.prototype._packIDAT = function(data) {
return this._packChunk(constants.TYPE_IDAT, data);
};
Packer.prototype._packIEND = function() {
return this._packChunk(constants.TYPE_IEND, null);
};

415
lib/parser.js Normal file → Executable file
View file

@ -22,27 +22,15 @@
var util = require('util'), var util = require('util'),
Stream = require('stream'), zlib = require('zlib'),
Compress = require('./compress'), CrcStream = require('./crc'),
ChunkStream = require('./chunkstream'),
constants = require('./constants'),
Filter = require('./filter'); Filter = require('./filter');
var signature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
var TYPE_IHDR = 0x49484452;
var TYPE_IEND = 0x49454e44;
var TYPE_IDAT = 0x49444154;
var TYPE_PLTE = 0x504c5445;
var TYPE_tRNS = 0x74524e53;
var TYPE_gAMA = 0x67414d41;
var COLOR_PALETTE = 1;
var COLOR_COLOR = 2;
var COLOR_ALPHA = 4;
var Parser = module.exports = function(options) { var Parser = module.exports = function(options) {
Stream.call(this); ChunkStream.call(this);
this._options = options; this._options = options;
options.checkCRC = options.checkCRC !== false; options.checkCRC = options.checkCRC !== false;
@ -50,190 +38,135 @@ var Parser = module.exports = function(options) {
this._hasIHDR = false; this._hasIHDR = false;
this._hasIEND = false; this._hasIEND = false;
// input flags this._inflate = null;
this._filter = null;
this._crc = null;
// input flags/metadata
this._palette = []; this._palette = [];
this._colorType = 0; this._colorType = 0;
this._chunks = {}; this._chunks = {};
this._chunks[TYPE_IHDR] = this._parseIHDR.bind(this); this._chunks[constants.TYPE_IHDR] = this._handleIHDR.bind(this);
this._chunks[TYPE_IEND] = this._parseIEND.bind(this); this._chunks[constants.TYPE_IEND] = this._handleIEND.bind(this);
this._chunks[TYPE_IDAT] = this._parseIDAT.bind(this); this._chunks[constants.TYPE_IDAT] = this._handleIDAT.bind(this);
this._chunks[TYPE_PLTE] = this._parsePLTE.bind(this); this._chunks[constants.TYPE_PLTE] = this._handlePLTE.bind(this);
this._chunks[TYPE_tRNS] = this._parseTRNS.bind(this); this._chunks[constants.TYPE_tRNS] = this._handleTRNS.bind(this);
this._chunks[TYPE_gAMA] = this._parseGAMA.bind(this); this._chunks[constants.TYPE_gAMA] = this._handleGAMA.bind(this);
this._compress = new Compress(options); this.writable = true;
this._filter = new Filter(options);
this._initCompress(); this.on('error', this._handleError.bind(this));
this._handleSignature();
}; };
util.inherits(Parser, Stream); util.inherits(Parser, ChunkStream);
Parser.prototype._initCompress = function() {
this._compress.on('error', this.emit.bind(this, 'error')); Parser.prototype._handleError = function() {
this._compress.on('deflated', this._packData.bind(this)); this.writable = false;
this._compress.on('inflated', this._unfilter.bind(this));
this.destroy();
if (this._inflate)
this._inflate.destroy();
}; };
Parser.prototype._parse = function(data) { Parser.prototype._handleSignature = function() {
this.read(constants.PNG_SIGNATURE.length,
var idx = 0; this._parseSignature.bind(this)
);
try {
// check PNG file signature
while (idx < signature.length) {
if (data[idx] != signature[idx]) {
throw new Error('Invalid file signature');
}
idx++;
}
//console.log('Signature is ok');
// iterate chunks
while (idx < data.length) {
idx = this._parseChunk(data, idx);
}
}
catch(err) {
this.emit('error', err);
}
}; };
Parser.prototype._pack = function(width, height, data) { Parser.prototype._parseSignature = function(data) {
// Signature var signature = constants.PNG_SIGNATURE;
this.emit('data', new Buffer(signature));
this.emit('data', this._packIHDR(width, height));
// filter pixel data for (var i = 0; i < signature.length; i++) {
var data = this._filter.filter(data, width, height); if (data[i] != signature[i]) {
this.emit('error', new Error('Invalid file signature'));
// compress it return;
this._compress.deflate(data);
};
Parser.prototype._packData = function(data) {
// console.log('deflate', data.length);
this.emit('data', this._packIDAT(data));
this.emit('data', this._packIEND());
this.emit('end');
};
Parser.prototype._unfilter = function(data) {
// expand data to 32 bit depending on colorType
if (this._colorType == 0) { // L
data = this._filter.unfilter(data, 1); // 1 Bpp
} else if (this._colorType == 2) { // RGB
data = this._filter.unfilter(data, 3); // 3 Bpp
} else if (this._colorType == 3) { // I
data = this._filter.unfilter(data, 1); // 1 Bpp
// use values fom palette
var pxLineLength = this.width << 2;
for (var y = 0; y < this.height; y++) {
var pxRowPos = y * pxLineLength;
for (var x = 0; x < this.width; x++) {
var pxPos = pxRowPos + (x << 2),
color = this._palette[data[pxPos]];
for (var i = 0; i < 4; i++)
data[pxPos + i] = color[i];
} }
} }
this.read(8, this._parseChunkBegin.bind(this));
} else if (this._colorType == 4) { // LA
data = this._filter.unfilter(data, 2); // 2 Bpp
} else if (this._colorType == 6) { // RGBA
data = this._filter.unfilter(data, 4); // 4 Bpp
} else throw new Error('Unsupported color type');
this.emit('parsed', data);
}; };
Parser.prototype._parseChunkBegin = function(data) {
// chunk content length
var length = data.readUInt32BE(0);
Parser.prototype._parseChunk = function(data, idx) {
if (this._hasIEND)
throw new Error('Not expected chunk after IEND');
// chunk size (only content)
var length = data.readUInt32BE(idx);
idx += 4;
// chunk type // chunk type
var type = data.readUInt32BE(idx), var type = data.readUInt32BE(4),
ancillary = !!(data[idx] & 0x20), // or critical
priv = !!(data[idx+1] & 0x20), // or public
safeToCopy = !!(data[idx+3] & 0x20), // or unsafe
name = ''; name = '';
for (var i = 0; i < 4; i++) for (var i = 4; i < 8; i++)
name += String.fromCharCode(data[idx+i]); name += String.fromCharCode(data[i]);
idx += 4;
// console.log('chunk ', name, length); // console.log('chunk ', name, length);
// calc CRC (of chunk type and content) // chunk flags
var calcCrc = crc32(data.slice(idx - 4, idx + length)), var ancillary = !!(data[4] & 0x20), // or critical
content = data.slice(idx, idx + length); priv = !!(data[5] & 0x20), // or public
idx += length; safeToCopy = !!(data[7] & 0x20); // or unsafe
// read CRC if (!this._hasIHDR && type != constants.TYPE_IHDR) {
var fileCrc = data.readInt32BE(idx); this.emit('error', new Error('Expected IHDR on beggining'));
idx += 4; return;
}
// and check CRC this._crc = new CrcStream();
if (this._options.checkCRC && calcCrc != fileCrc) this._crc.write(new Buffer(name));
throw new Error('Crc error');
if (!this._hasIHDR && type != TYPE_IHDR)
throw new Error('Expected IHDR on beggining');
if (this._chunks[type]) { if (this._chunks[type]) {
this._chunks[type](content); return this._chunks[type](length);
} else if (!ancillary) } else if (!ancillary) {
throw new Error('Unsupported critical chunk type ' + name); this.emit('error', new Error('Unsupported critical chunk type ' + name));
// else return;
// console.log('Ignoring chunk', name, type.toString(16)); } else {
this.read(length + 4, this._skipChunk.bind(this));
return idx; }
}; };
Parser.prototype._packChunk = function(type, data) { Parser.prototype._skipChunk = function(data) {
this.read(8, this._parseChunkBegin.bind(this));
};
var len = (data ? data.length : 0), Parser.prototype._handleChunkEnd = function() {
buf = new Buffer(len + 12); this.read(4, this._parseChunkEnd.bind(this));
};
buf.writeUInt32BE(len, 0); Parser.prototype._parseChunkEnd = function(data) {
buf.writeUInt32BE(type, 4);
if (data) data.copy(buf, 8); var fileCrc = data.readInt32BE(0),
calcCrc = this._crc.crc32();
buf.writeInt32BE(crc32(buf.slice(4, buf.length - 4)), buf.length - 4); // check CRC
return buf; if (this._options.checkCRC && calcCrc != fileCrc) {
this.emit('error', new Error('Crc error'));
return;
}
if (this._hasIEND) {
this.destroySoon();
} else {
this.read(8, this._parseChunkBegin.bind(this));
}
}; };
Parser.prototype._handleIHDR = function(length) {
this.read(length, this._parseIHDR.bind(this));
};
Parser.prototype._parseIHDR = function(data) { Parser.prototype._parseIHDR = function(data) {
this._crc.write(data);
var width = data.readUInt32BE(0), var width = data.readUInt32BE(0),
height = data.readUInt32BE(4), height = data.readUInt32BE(4),
depth = data[8], depth = data[8],
colorType = data[9], // 1 palette, 2 color, 4 alpha colorType = data[9], // bits: 1 palette, 2 color, 4 alpha
compr = data[10], compr = data[10],
filter = data[11], filter = data[11],
interlace = data[12]; interlace = data[12];
@ -243,38 +176,59 @@ Parser.prototype._parseIHDR = function(data) {
// 'compr', compr, 'filter', filter, 'interlace', interlace // 'compr', compr, 'filter', filter, 'interlace', interlace
// ); // );
if (depth != 8) if (depth != 8) {
throw new Error('Unsupported bit depth ' + depth); this.emit('error', new Error('Unsupported bit depth ' + depth));
if (interlace != 0) return;
throw new Error('Unsupported interlace method'); }
if (!(colorType in colorTypeToBppMap)) {
this.emit('error', new Error('Unsupported color type'));
return;
}
if (compr != 0) {
this.emit('error', new Error('Unsupported compression method'));
return;
}
if (filter != 0) {
this.emit('error', new Error('Unsupported filter method'));
return;
}
if (interlace != 0) {
this.emit('error', new Error('Unsupported interlace method'));
return;
}
this._colorType = colorType; this._colorType = colorType;
this._compress.prepareInflate(compr); this._data = new Buffer(width * height * 4);
this._filter.prepare(width, height, filter); this._filter = new Filter(
width, height,
colorTypeToBppMap[this._colorType],
this._data,
this._options
);
this._hasIHDR = true; this._hasIHDR = true;
this.emit('metadata', width, height); this.emit('metadata', {
}; width: width,
height: height,
palette: !!(colorType & constants.COLOR_PALETTE),
color: !!(colorType & constants.COLOR_COLOR),
alpha: !!(colorType & constants.COLOR_ALPHA),
data: this._data
});
Parser.prototype._packIHDR = function(width, height) { this._handleChunkEnd();
var buf = new Buffer(13);
buf.writeUInt32BE(width, 0);
buf.writeUInt32BE(height, 4);
buf[8] = 8;
buf[9] = 6; // colorType
buf[10] = 0; // compression
buf[11] = 0; // filter
buf[12] = 0; // interlace
return this._packChunk(TYPE_IHDR, buf);
}; };
Parser.prototype._handlePLTE = function(length) {
this.read(length, this._parsePLTE.bind(this));
};
Parser.prototype._parsePLTE = function(data) { Parser.prototype._parsePLTE = function(data) {
this._crc.write(data);
var entries = Math.floor(data.length / 3); var entries = Math.floor(data.length / 3);
// console.log('Palette:', entries); // console.log('Palette:', entries);
@ -286,83 +240,120 @@ Parser.prototype._parsePLTE = function(data) {
0xff 0xff
]); ]);
} }
this._handleChunkEnd();
}; };
Parser.prototype._handleTRNS = function(length) {
this.read(length, this._parseTRNS.bind(this));
};
Parser.prototype._parseTRNS = function(data) { Parser.prototype._parseTRNS = function(data) {
this._crc.write(data);
// palette // palette
if (this._colorType == 3) { if (this._colorType == 3) {
if (this._palette.length == 0) if (this._palette.length == 0) {
throw new Error('Transparency chunk must be after palette'); this.emit('error', new Error('Transparency chunk must be after palette'));
return;
if (data.length > this._palette.length) }
throw new Error('More transparent colors than palette size'); if (data.length > this._palette.length) {
this.emit('error', new Error('More transparent colors than palette size'));
for (var i = 0; i < this._palette.length; i++) return;
}
for (var i = 0; i < this._palette.length; i++) {
this._palette[i][3] = i < data.length ? data.readUInt8(i) : 0xff; this._palette[i][3] = i < data.length ? data.readUInt8(i) : 0xff;
} }
}
// for colorType 0 (grayscale) and 2 (rgb) // for colorType 0 (grayscale) and 2 (rgb)
// there might be one gray/color defined as transparent // there might be one gray/color defined as transparent
this._handleChunkEnd();
}; };
Parser.prototype._handleGAMA = function(length) {
this.read(length, this._parseGAMA.bind(this));
};
Parser.prototype._parseGAMA = function(data) { Parser.prototype._parseGAMA = function(data) {
this._crc.write(data);
this.emit('gamma', data.readUInt32BE(0) / 100000); this.emit('gamma', data.readUInt32BE(0) / 100000);
this._handleChunkEnd();
}; };
Parser.prototype._parseIDAT = function(data) { Parser.prototype._handleIDAT = function(length) {
this.read(-length, this._parseIDAT.bind(this, length));
};
Parser.prototype._parseIDAT = function(length, data) {
this._crc.write(data);
if (this._colorType == 3 && this._palette.length == 0) if (this._colorType == 3 && this._palette.length == 0)
throw new Error('Expected palette not found'); throw new Error('Expected palette not found');
this._compress.writeInflate(data); if (!this._inflate) {
}; this._inflate = zlib.createInflate();
Parser.prototype._packIDAT = function(data) { this._inflate.on('error', this.emit.bind(this, 'error'));
return this._packChunk(TYPE_IDAT, data); this._filter.on('complete', this._reverseFiltered.bind(this));
this._inflate.pipe(this._filter);
}
this._inflate.write(data);
length -= data.length;
if (length > 0)
this._handleIDAT(length);
else
this._handleChunkEnd();
}; };
Parser.prototype._handleIEND = function(length) {
this.read(length, this._parseIEND.bind(this));
};
Parser.prototype._parseIEND = function(data) { Parser.prototype._parseIEND = function(data) {
this._crc.write(data);
// no more data to inflate // no more data to inflate
this._compress.endInflate(); this._inflate.end();
this._hasIEND = true; this._hasIEND = true;
}; this._handleChunkEnd();
Parser.prototype._packIEND = function() {
return this._packChunk(TYPE_IEND, null);
}; };
var colorTypeToBppMap = {
0: 1,
2: 3,
3: 1,
4: 2,
6: 4
};
Parser.prototype._reverseFiltered = function(data, width, height) {
if (this._colorType == 3) { // paletted
// use values from palette
var pxLineLength = width << 2;
for (var y = 0; y < height; y++) {
var pxRowPos = y * pxLineLength;
for (var x = 0; x < width; x++) {
var pxPos = pxRowPos + (x << 2),
color = this._palette[data[pxPos]];
// prepare crc table as in PNG Specification for (var i = 0; i < 4; i++)
var crcTable = []; data[pxPos + i] = color[i];
for (var i = 0; i < 256; i++) {
var c = i;
for (var j = 0; j < 8; j++) {
if (c & 1) {
c = 0xedb88320 ^ (c >>> 1);
} else {
c = c >>> 1;
} }
} }
crcTable[i] = c;
}
function crc32(buf) {
var crc = -1;
for (var i = 0; i < buf.length; i++) {
crc = crcTable[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
} }
return crc ^ -1;
}
this.emit('parsed', data);
};

53
lib/png.js Normal file → Executable file
View file

@ -22,11 +22,15 @@
var util = require('util'), var util = require('util'),
Parser = require('./parser'); Stream = require('stream'),
Parser = require('./parser'),
Packer = require('./packer');
var PNG = exports.PNG = function(options) { var PNG = exports.PNG = function(options) {
Parser.call(this, options = options || {}); Stream.call(this);
options = options || {};
this.width = options.width || 0; this.width = options.width || 0;
this.height = options.height || 0; this.height = options.height || 0;
@ -35,23 +39,34 @@ var PNG = exports.PNG = function(options) {
? new Buffer(4 * this.width * this.height) : null; ? new Buffer(4 * this.width * this.height) : null;
this.gamma = 0; this.gamma = 0;
this.readable = this.writable = true; this.readable = this.writable = true;
this._buffers = [];
this._buffLen = 0;
this.on('metadata', this._metadata.bind(this)); this._parser = new Parser(options || {});
this.on('gamma', this._gamma.bind(this));
this.on('parsed', function(data) { this._parser.on('error', this.emit.bind(this, 'error'));
this._parser.on('close', this.emit.bind(this, 'close'));
this._parser.on('metadata', this._metadata.bind(this));
this._parser.on('gamma', this._gamma.bind(this));
this._parser.on('parsed', function(data) {
this.data = data; this.data = data;
this.emit('parsed', data);
}.bind(this)); }.bind(this));
this._packer = new Packer(options);
this._packer.on('data', this.emit.bind(this, 'data'));
this._packer.on('end', this.emit.bind(this, 'end'));
this._packer.on('error', this.emit.bind(this, 'error'));
}; };
util.inherits(PNG, Parser); util.inherits(PNG, Stream);
PNG.prototype.pack = function() { PNG.prototype.pack = function() {
this._pack(this.width, this.height, this.data);
process.nextTick(function() {
this._packer.pack(this.data, this.width, this.height);
}.bind(this));
return this; return this;
}; };
@ -76,27 +91,23 @@ PNG.prototype.parse = function(data, callback) {
}.bind(this)); }.bind(this));
} }
this._parse(data); this.end(data);
return this; return this;
}; };
PNG.prototype.write = function(data) { PNG.prototype.write = function(data) {
this._buffers.push(data); this._parser.write(data);
this._buffLen += data.length;
return true; return true;
}; };
PNG.prototype.end = function(data) { PNG.prototype.end = function(data) {
if (data) this.write(data); this._parser.end(data);
this._parse(Buffer.concat(this._buffers, this._buffLen));
this._buffers = [];
this._buffLen = 0;
}; };
PNG.prototype._metadata = function(width, height) { PNG.prototype._metadata = function(metadata) {
this.width = width; this.width = metadata.width;
this.height = height; this.height = metadata.height;
this.data = null; this.data = metadata.data;
}; };
PNG.prototype._gamma = function(gamma) { PNG.prototype._gamma = function(gamma) {