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/constants.js b/lib/constants.js old mode 100644 new mode 100755 diff --git a/lib/helpers.js b/lib/crc.js old mode 100644 new mode 100755 similarity index 70% rename from lib/helpers.js rename to lib/crc.js index 4634bf5..a3ac042 --- a/lib/helpers.js +++ b/lib/crc.js @@ -20,6 +20,49 @@ '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 = []; @@ -34,12 +77,3 @@ for (var i = 0; i < 256; i++) { } crcTable[i] = c; } - -exports.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; -}; 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 old mode 100644 new mode 100755 index 9d39d5d..aa3b811 --- a/lib/packer.js +++ b/lib/packer.js @@ -23,45 +23,55 @@ var util = require('util'), Stream = require('stream'), - Compress = require('./compress'), - Filter = require('./filter'); + zlib = require('zlib'), + Filter = require('./filter'), + CrcStream = require('./crc'), + constants = require('./constants'); -var Parser = module.exports = function(options) { +var Packer = module.exports = function(options) { Stream.call(this); this._options = options; - // this._compress = new Compress(options); - // this._compress.on('error', this.emit.bind(this, 'error')); - // this._compress.on('deflated', this._packData.bind(this)); + options.deflateChunkSize = options.deflateChunkSize || 32 * 1024; + options.deflateLevel = options.deflateLevel || 9; + + this.readable = true; }; -util.inherits(Parser, Stream); +util.inherits(Packer, Stream); -Parser.prototype._pack = function(width, height, data) { +Packer.prototype.pack = function(data, width, height) { // Signature - this.emit('data', new Buffer(signature)); + this.emit('data', new Buffer(constants.PNG_SIGNATURE)); this.emit('data', this._packIHDR(width, height)); // filter pixel data - var data = this._filter.filter(data, width, height); + var filter = new Filter(width, height, 4, data, this._options); + var data = filter.filter(); // compress it - this._compress.deflate(data); + 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); }; -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._packChunk = function(type, data) { +Packer.prototype._packChunk = function(type, data) { var len = (data ? data.length : 0), buf = new Buffer(len + 12); @@ -71,11 +81,11 @@ Parser.prototype._packChunk = function(type, data) { if (data) data.copy(buf, 8); - buf.writeInt32BE(crc32(buf.slice(4, buf.length - 4)), buf.length - 4); + buf.writeInt32BE(CrcStream.crc32(buf.slice(4, buf.length - 4)), buf.length - 4); return buf; }; -Parser.prototype._packIHDR = function(width, height) { +Packer.prototype._packIHDR = function(width, height) { var buf = new Buffer(13); buf.writeUInt32BE(width, 0); @@ -86,13 +96,13 @@ Parser.prototype._packIHDR = function(width, height) { buf[11] = 0; // filter buf[12] = 0; // interlace - return this._packChunk(TYPE_IHDR, buf); + return this._packChunk(constants.TYPE_IHDR, buf); }; -Parser.prototype._packIDAT = function(data) { - return this._packChunk(TYPE_IDAT, data); +Packer.prototype._packIDAT = function(data) { + return this._packChunk(constants.TYPE_IDAT, data); }; -Parser.prototype._packIEND = function() { - return this._packChunk(TYPE_IEND, null); +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 f64e716..f4eef7b --- a/lib/parser.js +++ b/lib/parser.js @@ -22,15 +22,15 @@ var util = require('util'), - Stream = require('stream'), zlib = require('zlib'), - helpers = require('./helpers'), + CrcStream = require('./crc'), + ChunkStream = require('./chunkstream'), constants = require('./constants'), Filter = require('./filter'); var Parser = module.exports = function(options) { - Stream.call(this); + ChunkStream.call(this); this._options = options; options.checkCRC = options.checkCRC !== false; @@ -38,14 +38,13 @@ var Parser = module.exports = function(options) { this._hasIHDR = false; this._hasIEND = false; - this._stream = new ReadStream(); this._inflate = null; + this._filter = null; + this._crc = null; // input flags/metadata this._palette = []; this._colorType = 0; - this._width = 0; - this._height = 0; this._chunks = {}; this._chunks[constants.TYPE_IHDR] = this._handleIHDR.bind(this); @@ -58,31 +57,23 @@ var Parser = module.exports = function(options) { this.writable = true; this.on('error', this._handleError.bind(this)); - this._handleSignature(); }; -util.inherits(Parser, Stream); +util.inherits(Parser, ChunkStream); -Parser.prototype.write = function(data) { - this._stream.write(data); -}; - -Parser.prototype.end = function(data) { - this._stream.end(data); -}; - Parser.prototype._handleError = function() { this.writable = false; - this._stream.destroy(); + + this.destroy(); if (this._inflate) this._inflate.destroy(); }; Parser.prototype._handleSignature = function() { - this._stream.read(constants.PNG_SIGNATURE.length, + this.read(constants.PNG_SIGNATURE.length, this._parseSignature.bind(this) ); }; @@ -91,13 +82,13 @@ Parser.prototype._parseSignature = function(data) { var signature = constants.PNG_SIGNATURE; - for (var i = 0; i < signature.length; i++) + for (var i = 0; i < signature.length; i++) { if (data[i] != signature[i]) { this.emit('error', new Error('Invalid file signature')); return; } } - this._stream.read(8, this._parseChunkBegin.bind(this)); + this.read(8, this._parseChunkBegin.bind(this)); }; Parser.prototype._parseChunkBegin = function(data) { @@ -111,6 +102,8 @@ Parser.prototype._parseChunkBegin = function(data) { for (var i = 4; i < 8; i++) name += String.fromCharCode(data[i]); + // console.log('chunk ', name, length); + // chunk flags var ancillary = !!(data[4] & 0x20), // or critical priv = !!(data[5] & 0x20), // or public @@ -121,52 +114,55 @@ Parser.prototype._parseChunkBegin = function(data) { return; } + this._crc = new CrcStream(); + this._crc.write(new Buffer(name)); + if (this._chunks[type]) { return this._chunks[type](length); } 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._skipChunk = function(data) { + this.read(8, this._parseChunkBegin.bind(this)); +}; + Parser.prototype._handleChunkEnd = function() { - this._stream.read(4, this._parseChunkEnd.bind(this)); + this.read(4, this._parseChunkEnd.bind(this)); }; Parser.prototype._parseChunkEnd = function(data) { - var fileCrc = data.readInt32BE(0); + var fileCrc = data.readInt32BE(0), + calcCrc = this._crc.crc32(); -// // calc CRC (of chunk type and content) -// var calcCrc = helpers.crc32(data.slice(idx - 4, idx + length)), -// content = data.slice(idx, idx + length); -// idx += length; - -// // read CRC -// var fileCrc = data.readInt32BE(idx); -// idx += 4; - -// // and check CRC -// if (this._options.checkCRC && calcCrc != fileCrc) -// throw new Error('Crc error'); - - // TODO: calc crc on stream + // check CRC + if (this._options.checkCRC && calcCrc != fileCrc) { + this.emit('error', new Error('Crc error')); + return; + } if (this._hasIEND) { - this._stream.destroy(); + this.destroySoon(); } else { - this._stream.read(8, this._parseChunkBegin.bind(this)); + this.read(8, this._parseChunkBegin.bind(this)); } }; Parser.prototype._handleIHDR = function(length) { - this._stream.read(length, this._parseIHDR.bind(this)); + 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], @@ -175,15 +171,19 @@ Parser.prototype._parseIHDR = function(data) { filter = data[11], interlace = data[12]; - console.log(' width', width, 'height', height, - 'depth', depth, 'colorType', colorType, - 'compr', compr, 'filter', filter, 'interlace', interlace - ); + // console.log(' width', width, 'height', height, + // 'depth', depth, 'colorType', colorType, + // 'compr', compr, 'filter', filter, 'interlace', interlace + // ); 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; @@ -198,8 +198,15 @@ Parser.prototype._parseIHDR = function(data) { } this._colorType = colorType; - this._width = width; - this._height = height; + + 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', { @@ -207,7 +214,8 @@ Parser.prototype._parseIHDR = function(data) { height: height, palette: !!(colorType & constants.COLOR_PALETTE), color: !!(colorType & constants.COLOR_COLOR), - alpha: !!(colorType & constants.COLOR_ALPHA) + alpha: !!(colorType & constants.COLOR_ALPHA), + data: this._data }); this._handleChunkEnd(); @@ -215,12 +223,14 @@ Parser.prototype._parseIHDR = function(data) { Parser.prototype._handlePLTE = function(length) { - this._stream.read(length, this._parsePLTE.bind(this)); + 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); + // console.log('Palette:', entries); for (var i = 0; i < entries; i++) { this._palette.push([ @@ -235,10 +245,12 @@ Parser.prototype._parsePLTE = function(data) { }; Parser.prototype._handleTRNS = function(length) { - this._stream.read(length, this._parseTRNS.bind(this)); + 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) { @@ -261,19 +273,22 @@ Parser.prototype._parseTRNS = function(data) { }; Parser.prototype._handleGAMA = function(length) { - this._stream.read(length, this._parseGAMA.bind(this)); + 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._handleIDAT = function(length) { - this._stream.read(-length, this._parseIDAT.bind(this, length)); + this.read(-length, this._parseIDAT.bind(this, length)); }; -Parser.prototype._parseIDAT = function(lenght, data) { +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'); @@ -282,7 +297,9 @@ Parser.prototype._parseIDAT = function(lenght, data) { this._inflate = zlib.createInflate(); this._inflate.on('error', this.emit.bind(this, 'error')); - //this._inflate.pipe(filter); + this._filter.on('complete', this._reverseFiltered.bind(this)); + + this._inflate.pipe(this._filter); } this._inflate.write(data); @@ -296,10 +313,12 @@ Parser.prototype._parseIDAT = function(lenght, data) { Parser.prototype._handleIEND = function(length) { - this._stream.read(length, this._parseIEND.bind(this)); + this.read(length, this._parseIEND.bind(this)); }; Parser.prototype._parseIEND = function(data) { + this._crc.write(data); + // no more data to inflate this._inflate.end(); @@ -308,26 +327,25 @@ Parser.prototype._parseIEND = function(data) { }; +var colorTypeToBppMap = { + 0: 1, + 2: 3, + 3: 1, + 4: 2, + 6: 4 +}; -Parser.prototype._unfilter = function(data) { +Parser.prototype._reverseFiltered = function(data, width, height) { - // expand data to 32 bit depending on colorType - if (this._colorType == 0) { // L - data = this._filter.unfilter(data, 1); // 1 Bpp + if (this._colorType == 3) { // paletted - } else if (this._colorType == 2) { // RGB - data = this._filter.unfilter(data, 3); // 3 Bpp + // use values from palette + var pxLineLength = width << 2; - } 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++) { + for (var y = 0; y < height; y++) { var pxRowPos = y * pxLineLength; - for (var x = 0; x < this.width; x++) { + for (var x = 0; x < width; x++) { var pxPos = pxRowPos + (x << 2), color = this._palette[data[pxPos]]; @@ -335,14 +353,7 @@ Parser.prototype._unfilter = function(data) { 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); }; 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) { diff --git a/lib/readstream.js b/lib/readstream.js deleted file mode 100644 index 83f5aab..0000000 --- a/lib/readstream.js +++ /dev/null @@ -1,55 +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'), - Stream = require('stream'); - - -var ReadStream = module.exports = function() { - Stream.call(this); - - - - this.writeable = true; -}; -util.inherits(ReadStream, Stream); - - -// length < 0 -> at most this length -ReadStream.prototype.read = function(length, callback) { - -}; - -ReadStream.prototype.destroy = function() { - -}; - - -ReadStream.prototype.write = function(data) { - -}; - -ReadStream.prototype.end = function(data) { - -}; -