diff --git a/examples/test2.js b/examples/simple.js similarity index 93% rename from examples/test2.js rename to examples/simple.js index bc2a768..9edaab6 100755 --- a/examples/test2.js +++ b/examples/simple.js @@ -24,7 +24,7 @@ png.on('parsed', function() { } } - png.pack(); + png.pack().pipe(dst); }); -src.pipe(png).pipe(dst); +src.pipe(png); diff --git a/examples/test.js b/examples/test.js deleted file mode 100755 index 785f66c..0000000 --- a/examples/test.js +++ /dev/null @@ -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(); - }); -}); diff --git a/examples/test/bg.js b/examples/test/bg.js index a38985c..d36ff79 100644 --- a/examples/test/bg.js +++ b/examples/test/bg.js @@ -24,5 +24,4 @@ for (var y = 0; y < png.height; y++) { } } -png.pipe(fs.createWriteStream('bg.png')); -png.pack(); +png.pack().pipe(fs.createWriteStream(__dirname + '/bg.png')); diff --git a/examples/test/bg.png b/examples/test/bg.png index 491ae6e..b030229 100644 Binary files a/examples/test/bg.png and b/examples/test/bg.png differ diff --git a/examples/test/list.html b/examples/test/list.html old mode 100644 new mode 100755 diff --git a/examples/test/test.js b/examples/test/test.js old mode 100644 new mode 100755 index 4c58849..1a9f6a1 --- a/examples/test/test.js +++ b/examples/test/test.js @@ -6,12 +6,12 @@ var fs = require('fs'), fs.readdir(__dirname + '/img/', function(err, files) { if (err) throw err; - for (var i = 0; i < files.length; i++) { + files.forEach(function(file) { - if (!files[i].match(/\.png$/i)) - continue; + if (!file.match(/\.png$/i)) + return; - fs.createReadStream(__dirname + '/img/' + files[i]) + fs.createReadStream(__dirname + '/img/' + file) .pipe(new PNG()) .on('parsed', function() { @@ -29,7 +29,10 @@ fs.readdir(__dirname + '/img/', function(err, files) { } } - this.pack(); - }).pipe(fs.createWriteStream(__dirname + '/out/' + files[i])); - } + this.pack() + .pipe(fs.createWriteStream(__dirname + '/out/' + file)); + + }); + + }); }); diff --git a/lib/chunkstream.js b/lib/chunkstream.js new file mode 100755 index 0000000..a84f753 --- /dev/null +++ b/lib/chunkstream.js @@ -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(); + } +}; diff --git a/lib/compress.js b/lib/compress.js deleted file mode 100644 index f110de6..0000000 --- a/lib/compress.js +++ /dev/null @@ -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; -}; diff --git a/lib/constants.js b/lib/constants.js new file mode 100755 index 0000000..1e74b79 --- /dev/null +++ b/lib/constants.js @@ -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 +}; diff --git a/lib/crc.js b/lib/crc.js new file mode 100755 index 0000000..a3ac042 --- /dev/null +++ b/lib/crc.js @@ -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; +} diff --git a/lib/filter.js b/lib/filter.js old mode 100644 new mode 100755 index 97cbc07..2fb1662 --- a/lib/filter.js +++ b/lib/filter.js @@ -22,17 +22,21 @@ var util = require('util'), zlib = require('zlib'), - events = require('events'); + ChunkStream = require('./chunkstream'); -var Filter = module.exports = function(options) { - events.EventEmitter.call(this); +var Filter = module.exports = function(width, height, Bpp, data, options) { + ChunkStream.call(this); + this._width = width; + this._height = height; + this._Bpp = Bpp; + this._data = data; this._options = options; - options.filterType = 'filterType' in options ? options.filterType : -1; - this._width = 0; - this._height = 0; + this._line = 0; + + options.filterType = 'filterType' in options ? options.filterType : -1; this._filters = { 0: this._filterNone.bind(this), @@ -42,134 +46,135 @@ var Filter = module.exports = function(options) { 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) { - - if (type != 0) - throw new Error('Unsupported filter method'); - - this.width = width; - this.height = height; +var pixelBppMap = { + 1: { // L + 0: 0, + 1: 0, + 2: 0, + 3: 0xff, + }, + 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); + var pxData = this._data, + pxLineLength = this._width << 2, + pxRowPos = this._line * pxLineLength, + filter = rawData[0]; - for (var y = 0; y < this.height; y++) { + if (filter == 0) { + for (var x = 0; x < this._width; x++) { + var pxPos = pxRowPos + (x << 2), + rawPos = 1 + x * this._Bpp; - var rawRowPos = rawLineLength * y + 1, - pxRowPos = y * pxLineLength, - filter = rawData[rawRowPos - 1]; + for (var i = 0; i < 4; i++) { + var idx = pixelBppMap[this._Bpp][i]; + pxData[pxPos + i] = idx != 0xff ? rawData[rawPos + idx] : 0xff; + } + } + } else if (filter == 1) { + for (var x = 0; x < this._width; x++) { + var pxPos = pxRowPos + (x << 2), + rawPos = 1 + x * this._Bpp; - if (filter == 0) { - for (var x = 0; x < this.width; x++) { - var pxPos = pxRowPos + (x << 2), - rawPos = rawRowPos + x * Bpp; + for (var i = 0; i < 4; i++) { + var idx = pixelBppMap[this._Bpp][i], + left = x > 0 ? pxData[pxPos + i - 4] : 0; - for (var i = 0; i < Bpp; i++) - pxData[pxPos + i] = rawData[rawPos + i]; + pxData[pxPos + i] = idx != 0xff ? rawData[rawPos + idx] + left : 0xff; + } + } + + } else if (filter == 2) { + for (var x = 0; x < this._width; x++) { + var pxPos = pxRowPos + (x << 2), + rawPos = 1 + x * this._Bpp; + + for (var i = 0; i < 4; i++) { + var idx = pixelBppMap[this._Bpp][i], + up = this._line > 0 ? pxData[pxPos - pxLineLength + i] : 0; + + pxData[pxPos + i] = idx != 0xff ? rawData[rawPos + idx] + up : 0xff; } - } else if (filter == 1) { - for (var x = 0; x < this.width; x++) { - var pxPos = pxRowPos + (x << 2), - rawPos = rawRowPos + x * Bpp; + } - for (var i = 0; i < Bpp; i++) { - var left = x > 0 ? pxData[pxPos + i - 4] : 0; - pxData[pxPos + i] = rawData[rawPos + i] + left; - } + } else if (filter == 3) { + for (var x = 0; x < this._width; x++) { + var pxPos = pxRowPos + (x << 2), + rawPos = 1 + x * this._Bpp; + + for (var i = 0; i < 4; i++) { + var idx = pixelBppMap[this._Bpp][i], + 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] = idx != 0xff ? rawData[rawPos + idx] + add : 0xff; } - } else if (filter == 2) { - for (var x = 0; x < this.width; x++) { - var pxPos = pxRowPos + (x << 2), - rawPos = rawRowPos + x * Bpp; + } - for (var i = 0; i < Bpp; i++) { - var up = y > 0 ? pxData[pxPos - pxLineLength + i] : 0; - pxData[pxPos + i] = rawData[rawPos + i] + up; - } - } + } else if (filter == 4) { + for (var x = 0; x < this._width; x++) { + var pxPos = pxRowPos + (x << 2), + rawPos = 1 + x * this._Bpp; - } else if (filter == 3) { - for (var x = 0; x < this.width; x++) { - var pxPos = pxRowPos + (x << 2), - rawPos = rawRowPos + x * Bpp; + for (var i = 0; i < 4; i++) { + var idx = pixelBppMap[this._Bpp][i], + left = x > 0 ? pxData[pxPos + i - 4] : 0, + up = this._line > 0 ? pxData[pxPos - pxLineLength + i] : 0, + upLeft = x > 0 && this._line > 0 + ? pxData[pxPos - pxLineLength + i - 4] : 0, + add = PaethPredictor(left, up, upLeft); - for (var i = 0; i < Bpp; i++) { - var left = x > 0 ? pxData[pxPos + i - 4] : 0, - up = y > 0 ? pxData[pxPos - pxLineLength + i] : 0; - - pxData[pxPos + i] = rawData[rawPos + i] - + Math.floor((left + up) / 2); - } - } - - } else if (filter == 4) { - for (var x = 0; x < this.width; x++) { - var pxPos = pxRowPos + (x << 2), - rawPos = rawRowPos + x * Bpp; - - for (var i = 0; i < Bpp; i++) { - var left = x > 0 ? pxData[pxPos + i - 4] : 0, - up = y > 0 ? pxData[pxPos - pxLineLength + i] : 0, - upLeft = x > 0 && y > 0 - ? pxData[pxPos - pxLineLength + i - 4] : 0; - - pxData[pxPos + i] = rawData[rawPos + i] - + PaethPredictor(left, up, upLeft) - } + pxData[pxPos + i] = idx != 0xff ? rawData[rawPos + idx] + add : 0xff; } } } - // expand data to 32 bit - for (var y = 0; y < this.height; y++) { - var pxRowPos = y * pxLineLength; - if (Bpp == 1) { // L - for (var x = 0; x < this.width; x++) { - var pxPos = pxRowPos + (x << 2); + this._line++; - pxData[pxPos + 1] = pxData[pxPos + 2] = pxData[pxPos]; - pxData[pxPos + 3] = 0xff; - } - - } 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; + if (this._line < this._height) + this.read(this._width * this._Bpp + 1, this._reverseFilterLine.bind(this)); + else + this.emit('complete', this._data, this._width, this._height); }; -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) if (this._options.filterType == -1) { @@ -177,7 +182,7 @@ Filter.prototype.filter = function(pxData, width, height) { sel = 0; 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) { sel = f; min = sum; @@ -187,14 +192,14 @@ Filter.prototype.filter = function(pxData, width, height) { } else { sel = this._options.filterType; } - this._filters[sel](pxData, y, width, height, rawData); + this._filters[sel](pxData, y, 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, sum = 0; @@ -210,9 +215,9 @@ Filter.prototype._filterNone = function(pxData, y, width, height, rawData) { 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, sum = 0; @@ -230,9 +235,9 @@ Filter.prototype._filterSub = function(pxData, y, width, height, rawData) { 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, sum = 0; @@ -250,9 +255,9 @@ Filter.prototype._filterUp = function(pxData, y, width, height, rawData) { 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, sum = 0; @@ -271,9 +276,9 @@ Filter.prototype._filterAvg = function(pxData, y, width, height, rawData) { 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, sum = 0; diff --git a/lib/packer.js b/lib/packer.js new file mode 100755 index 0000000..aa3b811 --- /dev/null +++ b/lib/packer.js @@ -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); +}; diff --git a/lib/parser.js b/lib/parser.js old mode 100644 new mode 100755 index 08117d7..f4eef7b --- a/lib/parser.js +++ b/lib/parser.js @@ -22,27 +22,15 @@ var util = require('util'), - Stream = require('stream'), - Compress = require('./compress'), + zlib = require('zlib'), + CrcStream = require('./crc'), + ChunkStream = require('./chunkstream'), + constants = require('./constants'), 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) { - Stream.call(this); + ChunkStream.call(this); this._options = options; options.checkCRC = options.checkCRC !== false; @@ -50,190 +38,135 @@ var Parser = module.exports = function(options) { this._hasIHDR = false; this._hasIEND = false; - // input flags + this._inflate = null; + this._filter = null; + this._crc = null; + + // input flags/metadata this._palette = []; this._colorType = 0; this._chunks = {}; - this._chunks[TYPE_IHDR] = this._parseIHDR.bind(this); - this._chunks[TYPE_IEND] = this._parseIEND.bind(this); - this._chunks[TYPE_IDAT] = this._parseIDAT.bind(this); - this._chunks[TYPE_PLTE] = this._parsePLTE.bind(this); - this._chunks[TYPE_tRNS] = this._parseTRNS.bind(this); - this._chunks[TYPE_gAMA] = this._parseGAMA.bind(this); + this._chunks[constants.TYPE_IHDR] = this._handleIHDR.bind(this); + this._chunks[constants.TYPE_IEND] = this._handleIEND.bind(this); + this._chunks[constants.TYPE_IDAT] = this._handleIDAT.bind(this); + this._chunks[constants.TYPE_PLTE] = this._handlePLTE.bind(this); + this._chunks[constants.TYPE_tRNS] = this._handleTRNS.bind(this); + this._chunks[constants.TYPE_gAMA] = this._handleGAMA.bind(this); - this._compress = new Compress(options); - this._filter = new Filter(options); + this.writable = true; - 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._compress.on('inflated', this._unfilter.bind(this)); + this.writable = false; + + this.destroy(); + + if (this._inflate) + this._inflate.destroy(); }; -Parser.prototype._parse = function(data) { +Parser.prototype._handleSignature = function() { + this.read(constants.PNG_SIGNATURE.length, + this._parseSignature.bind(this) + ); +}; - var idx = 0; +Parser.prototype._parseSignature = function(data) { - 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'); + var signature = constants.PNG_SIGNATURE; - // iterate chunks - while (idx < data.length) { - idx = this._parseChunk(data, idx); + for (var i = 0; i < signature.length; i++) { + if (data[i] != signature[i]) { + this.emit('error', new Error('Invalid file signature')); + return; } } - catch(err) { - this.emit('error', err); - } + this.read(8, this._parseChunkBegin.bind(this)); }; -Parser.prototype._pack = function(width, height, data) { +Parser.prototype._parseChunkBegin = function(data) { - // Signature - this.emit('data', new Buffer(signature)); - this.emit('data', this._packIHDR(width, height)); - - // filter pixel data - var data = this._filter.filter(data, width, height); - - // compress it - 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]; - } - } - - } 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._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 content length + var length = data.readUInt32BE(0); // chunk type - var type = data.readUInt32BE(idx), - ancillary = !!(data[idx] & 0x20), // or critical - priv = !!(data[idx+1] & 0x20), // or public - safeToCopy = !!(data[idx+3] & 0x20), // or unsafe + var type = data.readUInt32BE(4), name = ''; - for (var i = 0; i < 4; i++) - name += String.fromCharCode(data[idx+i]); - idx += 4; + for (var i = 4; i < 8; i++) + name += String.fromCharCode(data[i]); // console.log('chunk ', name, length); - // calc CRC (of chunk type and content) - var calcCrc = crc32(data.slice(idx - 4, idx + length)), - content = data.slice(idx, idx + length); - idx += length; + // chunk flags + var ancillary = !!(data[4] & 0x20), // or critical + priv = !!(data[5] & 0x20), // or public + safeToCopy = !!(data[7] & 0x20); // or unsafe - // read CRC - var fileCrc = data.readInt32BE(idx); - idx += 4; + if (!this._hasIHDR && type != constants.TYPE_IHDR) { + this.emit('error', new Error('Expected IHDR on beggining')); + return; + } - // and check CRC - if (this._options.checkCRC && calcCrc != fileCrc) - throw new Error('Crc error'); - - - if (!this._hasIHDR && type != TYPE_IHDR) - throw new Error('Expected IHDR on beggining'); + this._crc = new CrcStream(); + this._crc.write(new Buffer(name)); if (this._chunks[type]) { - this._chunks[type](content); + return this._chunks[type](length); - } else if (!ancillary) - throw new Error('Unsupported critical chunk type ' + name); - // else - // console.log('Ignoring chunk', name, type.toString(16)); - - return idx; + } else if (!ancillary) { + this.emit('error', new Error('Unsupported critical chunk type ' + name)); + return; + } else { + this.read(length + 4, this._skipChunk.bind(this)); + } }; -Parser.prototype._packChunk = function(type, data) { +Parser.prototype._skipChunk = function(data) { + this.read(8, this._parseChunkBegin.bind(this)); +}; - var len = (data ? data.length : 0), - buf = new Buffer(len + 12); +Parser.prototype._handleChunkEnd = function() { + this.read(4, this._parseChunkEnd.bind(this)); +}; - buf.writeUInt32BE(len, 0); - buf.writeUInt32BE(type, 4); +Parser.prototype._parseChunkEnd = function(data) { - 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); - return buf; + // check CRC + 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) { + this._crc.write(data); + var width = data.readUInt32BE(0), height = data.readUInt32BE(4), 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], filter = data[11], interlace = data[12]; @@ -243,38 +176,59 @@ Parser.prototype._parseIHDR = function(data) { // 'compr', compr, 'filter', filter, 'interlace', interlace // ); - if (depth != 8) - throw new Error('Unsupported bit depth ' + depth); - if (interlace != 0) - throw new Error('Unsupported interlace method'); + if (depth != 8) { + this.emit('error', new Error('Unsupported bit depth ' + depth)); + return; + } + 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._compress.prepareInflate(compr); - this._filter.prepare(width, height, filter); + this._data = new Buffer(width * height * 4); + this._filter = new Filter( + width, height, + colorTypeToBppMap[this._colorType], + this._data, + this._options + ); 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) { - - 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); + this._handleChunkEnd(); }; +Parser.prototype._handlePLTE = function(length) { + this.read(length, this._parsePLTE.bind(this)); +}; Parser.prototype._parsePLTE = function(data) { + this._crc.write(data); + var entries = Math.floor(data.length / 3); // console.log('Palette:', entries); @@ -286,83 +240,120 @@ Parser.prototype._parsePLTE = function(data) { 0xff ]); } + + this._handleChunkEnd(); }; +Parser.prototype._handleTRNS = function(length) { + this.read(length, this._parseTRNS.bind(this)); +}; Parser.prototype._parseTRNS = function(data) { + this._crc.write(data); + // palette if (this._colorType == 3) { - if (this._palette.length == 0) - throw new Error('Transparency chunk must be after palette'); - - if (data.length > this._palette.length) - throw new Error('More transparent colors than palette size'); - - for (var i = 0; i < this._palette.length; i++) + if (this._palette.length == 0) { + this.emit('error', new Error('Transparency chunk must be after palette')); + return; + } + if (data.length > this._palette.length) { + this.emit('error', new Error('More transparent colors than palette size')); + return; + } + for (var i = 0; i < this._palette.length; i++) { this._palette[i][3] = i < data.length ? data.readUInt8(i) : 0xff; + } } // for colorType 0 (grayscale) and 2 (rgb) // 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) { + + this._crc.write(data); 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) throw new Error('Expected palette not found'); - this._compress.writeInflate(data); -}; + if (!this._inflate) { + this._inflate = zlib.createInflate(); -Parser.prototype._packIDAT = function(data) { - return this._packChunk(TYPE_IDAT, data); + this._inflate.on('error', this.emit.bind(this, 'error')); + 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) { + this._crc.write(data); + // no more data to inflate - this._compress.endInflate(); + this._inflate.end(); this._hasIEND = true; -}; - -Parser.prototype._packIEND = function() { - return this._packChunk(TYPE_IEND, null); + this._handleChunkEnd(); }; +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 -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; + for (var i = 0; i < 4; i++) + data[pxPos + i] = color[i]; + } } } - 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); +}; diff --git a/lib/png.js b/lib/png.js old mode 100644 new mode 100755 index 87e2f47..1b0a2a6 --- a/lib/png.js +++ b/lib/png.js @@ -22,11 +22,15 @@ var util = require('util'), - Parser = require('./parser'); + Stream = require('stream'), + Parser = require('./parser'), + Packer = require('./packer'); var PNG = exports.PNG = function(options) { - Parser.call(this, options = options || {}); + Stream.call(this); + + options = options || {}; this.width = options.width || 0; this.height = options.height || 0; @@ -35,23 +39,34 @@ var PNG = exports.PNG = function(options) { ? new Buffer(4 * this.width * this.height) : null; this.gamma = 0; - this.readable = this.writable = true; - this._buffers = []; - this._buffLen = 0; - this.on('metadata', this._metadata.bind(this)); - this.on('gamma', this._gamma.bind(this)); + this._parser = new Parser(options || {}); - 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.emit('parsed', data); }.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() { - this._pack(this.width, this.height, this.data); + + process.nextTick(function() { + this._packer.pack(this.data, this.width, this.height); + }.bind(this)); + return this; }; @@ -76,27 +91,23 @@ PNG.prototype.parse = function(data, callback) { }.bind(this)); } - this._parse(data); + this.end(data); return this; }; PNG.prototype.write = function(data) { - this._buffers.push(data); - this._buffLen += data.length; + this._parser.write(data); return true; }; PNG.prototype.end = function(data) { - if (data) this.write(data); - this._parse(Buffer.concat(this._buffers, this._buffLen)); - this._buffers = []; - this._buffLen = 0; + this._parser.end(data); }; -PNG.prototype._metadata = function(width, height) { - this.width = width; - this.height = height; - this.data = null; +PNG.prototype._metadata = function(metadata) { + this.width = metadata.width; + this.height = metadata.height; + this.data = metadata.data; }; PNG.prototype._gamma = function(gamma) {