diff --git a/lib/bitmapper.js b/lib/bitmapper.js new file mode 100644 index 0000000..924dada --- /dev/null +++ b/lib/bitmapper.js @@ -0,0 +1,120 @@ +function bitRetriever(data, depth) { + + var leftOver = []; + var i = 0; + function split() { + var byte = data[i]; + i++; + switch(depth) { + default: + throw new Error("unrecognised depth"); + break; +/* case 8: + leftOver.push(byte); + break;*/ + case 4: + var byte2 = byte & 0x0f; + var byte1 = byte >> 4; + leftOver.push(byte1, byte2); + break; + case 2: + var byte4 = byte & 3; + var byte3 = byte >> 2 & 3; + var byte2 = byte >> 4 & 3; + var byte1 = byte >> 6 & 3; + leftOver.push(byte1, byte2, byte3, byte4); + break; + case 1: + var byte8 = byte & 1; + var byte7 = byte >> 1 & 1; + var byte6 = byte >> 2 & 1; + var byte5 = byte >> 3 & 1; + var byte4 = byte >> 4 & 1; + var byte3 = byte >> 5 & 1; + var byte2 = byte >> 6 & 1; + var byte1 = byte >> 7 & 1; + leftOver.push(byte1, byte2, byte3, byte4, byte5, byte6, byte7, byte8); + break; + } + } + return { + get: function(count) { + var returner; + if (depth === 8) { + returner = data.slice(i, i + count); + i += count; + return returner; + } + while(leftOver.length < count) { + split(); + } + returner = leftOver.slice(0, count); + leftOver = leftOver.slice(count); + return returner; + }, + resetAfterLine: function() { + leftOver.length = 0; + } + }; +} + +exports.dataToBitMap = function(data, width, height, bpp, depth) { + if (depth !== 8) { + var bits = bitRetriever(data, depth); + } + var pxData = new Buffer(width * height * 4); + var pxPos = 0; + var maxBit = Math.pow(2, depth) - 1; + var rawPos = 0; + var pixelData; + + for(var y = 0; y < height; y++) { + for(var x = 0; x < width; x++) { + if (depth !== 8) { + pixelData = bits.get(bpp); + } + for (var i = 0; i < 4; i++) { + var idx = pixelBppMap[bpp][i]; + if (depth === 8) { + pxData[pxPos] = idx !== 0xff ? data[idx + rawPos] : maxBit; + } else { + pxData[pxPos] = idx !== 0xff ? pixelData[idx] : maxBit; + } + pxPos++; + } + rawPos += bpp; + } + if (depth !== 8) { + bits.resetAfterLine(); + } + } + + return pxData; +}; + +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 + } +}; \ No newline at end of file diff --git a/lib/filter-async.js b/lib/filter-async.js index 6fb7e68..551ecbb 100644 --- a/lib/filter-async.js +++ b/lib/filter-async.js @@ -25,12 +25,19 @@ var util = require('util'), Filter = require('./filter'); -var FilterAsync = module.exports = function(width, height, Bpp, data, options) { +var FilterAsync = module.exports = function(width, height, Bpp, depth, options) { ChunkStream.call(this); - this._filter = new Filter(width, height, Bpp, data, options, { + var buffers = []; + var that = this; + this._filter = new Filter(width, height, Bpp, depth, options, { read: this.read.bind(this), - complete: this.emit.bind(this, 'complete') + complete: function(width, height) { + that.emit('complete', Buffer.concat(buffers), width, height) + }, + write: function(buffer) { + buffers.push(buffer); + } }); this._filter.start(); diff --git a/lib/filter-sync.js b/lib/filter-sync.js index 1ba810b..5f04db9 100644 --- a/lib/filter-sync.js +++ b/lib/filter-sync.js @@ -24,14 +24,20 @@ var SyncReader = require('./sync-reader'), Filter = require('./filter'); -exports.process = function(buffer, width, height, Bpp, outData, options) { +exports.process = function(buffer, width, height, Bpp, depth, options) { + var buffers = []; var reader = new SyncReader(buffer); - var filter = new Filter(width, height, Bpp, outData, options, { + var filter = new Filter(width, height, Bpp, depth, options, { read: reader.read.bind(reader), + write: function(buffer) { + buffers.push(buffer); + }, complete: function(){} }); filter.start(); reader.process(); + + return Buffer.concat(buffers); }; \ No newline at end of file diff --git a/lib/filter.js b/lib/filter.js index 83ad820..15077f4 100755 --- a/lib/filter.js +++ b/lib/filter.js @@ -20,16 +20,15 @@ 'use strict'; -var util = require('util'), - ChunkStream = require('./chunkstream'); +var util = require('util'); -var Filter = module.exports = function(width, height, Bpp, data, options, dependencies) { +var Filter = module.exports = function(width, height, Bpp, depth, options, dependencies) { this._width = width; this._height = height; this._Bpp = Bpp; - this._data = data; + this._depth = depth; this._options = options; this._line = 0; @@ -49,135 +48,80 @@ var Filter = module.exports = function(width, height, Bpp, data, options, depend }; this.read = dependencies.read; + this.write = dependencies.write; this.complete = dependencies.complete; + + var byteWidth = this._width * this._Bpp; + if (this._depth !== 8) { + byteWidth = Math.ceil(byteWidth / (8 / this._depth)); + } + this._byteWidth = byteWidth; }; Filter.prototype.start = function() { - this.read(this._width * this._Bpp + 1, this._reverseFilterLine.bind(this)); -}; - -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 - } + this.read(this._byteWidth + 1, this._reverseFilterLine.bind(this)); }; Filter.prototype._reverseFilterLine = function(rawData) { - var pxData = this._data, - pxLineLength = this._width << 2, - pxRowPos = this._line * pxLineLength, - filter = rawData[0]; + var line = new Buffer(this._byteWidth); - if (filter == 0) { - for (var x = 0; x < this._width; x++) { - var pxPos = pxRowPos + (x << 2), - rawPos = 1 + x * this._Bpp; + var filter = rawData[0]; - for (var i = 0; i < 4; i++) { - var idx = pixelBppMap[this._Bpp][i]; - pxData[pxPos + i] = idx != 0xff ? rawData[rawPos + idx] : 0xff; - } + var xComparison = this._depth === 8 ? this._Bpp : 1; + var xBiggerThan = xComparison - 1; + + for (var x = 0; x < this._byteWidth; x++) { + var rawByte = rawData[1 + x]; + switch(filter) { + case 0: + line[x] = rawByte; + break; + case 1: + var left = x > xBiggerThan ? line[x - xComparison] : 0; + line[x] = rawByte + left; + break; + case 2: + var up = this._lastLine ? this._lastLine[x] : 0; + line[x] = rawByte + up; + break; + case 3: + var up = this._lastLine ? this._lastLine[x] : 0; + var left = x > xBiggerThan ? line[x - xComparison] : 0; + var add = Math.floor((left + up) / 2); + line[x] = rawByte + add; + break; + case 4: + var up = this._lastLine ? this._lastLine[x] : 0; + var left = x > xBiggerThan ? line[x - xComparison] : 0; + var upLeft = x > xBiggerThan && this._lastLine + ? this._lastLine[x - xComparison] : 0; + var add = PaethPredictor(left, up, upLeft); + line[x] = rawByte + add; + break; } - } else if (filter == 1) { - 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; - - 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 == 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 == 4) { - 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, - upLeft = x > 0 && this._line > 0 - ? pxData[pxPos - pxLineLength + i - 4] : 0, - add = PaethPredictor(left, up, upLeft); - - pxData[pxPos + i] = idx != 0xff ? rawData[rawPos + idx] + add : 0xff; - } - } + //if (x === 5) { + // console.log("R", line[3], "G", line[4], "B", line[5]); + //} } - this._line++; + this._lastLine = line; + this.write(line); - if (this._line < this._height) - this.read(this._width * this._Bpp + 1, this._reverseFilterLine.bind(this)); - else - this.complete(this._data, this._width, this._height); + if (this._line < this._height) { + this.read(this._byteWidth + 1, this._reverseFilterLine.bind(this)); + } else { + this._lastLine = null; + this.complete(this._width, this._height); + } }; +Filter.prototype.filter = function(pxData) { - -Filter.prototype.filter = function() { - - var pxData = this._data, - rawData = new Buffer(((this._width << 2) + 1) * this._height); + var rawData = new Buffer(((this._width << 2) + 1) * this._height); for (var y = 0; y < this._height; y++) { diff --git a/lib/packer.js b/lib/packer.js index c8b74a8..96605d4 100755 --- a/lib/packer.js +++ b/lib/packer.js @@ -50,8 +50,9 @@ Packer.prototype.pack = function(data, width, height) { this.emit('data', this._packIHDR(width, height)); // filter pixel data - var filter = new Filter(width, height, 4, data, this._options, {}); - var data = filter.filter(); + //TODO {} + var filter = new Filter(width, height, 4, 8, this._options, {}); + var data = filter.filter(data); // compress it var deflate = zlib.createDeflate({ diff --git a/lib/parser-async.js b/lib/parser-async.js index a9453b8..17d0e9a 100644 --- a/lib/parser-async.js +++ b/lib/parser-async.js @@ -25,7 +25,8 @@ var util = require('util'), zlib = require('zlib'), ChunkStream = require('./chunkstream'), FilterAsync = require('./filter-async'), - Parser = require('./parser'); + Parser = require('./parser'), + bitmapper = require('./bitmapper'); var ParserAsync = module.exports = function(options) { @@ -72,26 +73,36 @@ ParserAsync.prototype._inflateData = function(data) { this._inflate.write(data); }; -ParserAsync.prototype._createData = function(width, height, bpp) { - this._data = new Buffer(width * height * 4); +ParserAsync.prototype._createData = function(width, height, bpp, depth) { + + this._bpp = bpp; + this._depth = depth; + this._filter = new FilterAsync( width, height, bpp, - this._data, + depth, this._options ); - return this._data; }; ParserAsync.prototype._finished = function(data) { - // no more data to inflate - this._inflate.end(); + if (!this._inflate) { + this.emit('error', 'No Inflate block'); + } else { + // no more data to inflate + this._inflate.end(); + } this.destroySoon(); }; ParserAsync.prototype._complete = function(data, width, height) { - data = this._parser.reverseFiltered(data, width, height); + data = bitmapper.dataToBitMap(data, width, height, + this._bpp, + this._depth); + + data = this._parser.reverseFiltered(data, this._depth, width, height); this.emit('parsed', data); }; diff --git a/lib/parser-sync.js b/lib/parser-sync.js index 71bcbdf..e885816 100644 --- a/lib/parser-sync.js +++ b/lib/parser-sync.js @@ -24,7 +24,8 @@ var zlib = require('zlib'), SyncReader = require('./sync-reader'), FilterSync = require('./filter-sync'), - Parser = require('./parser'); + Parser = require('./parser'), + bitmapper = require('./bitmapper'); var ParserSync = module.exports = function(buffer, options) { @@ -51,15 +52,19 @@ var ParserSync = module.exports = function(buffer, options) { var data = zlib.inflateSync(inflateData); - FilterSync.process( + data = FilterSync.process( data, this._width, this._height, this._bpp, - this._data, + this._depth, this._options ); - this.data = this._parser.reverseFiltered(this._data, this._width, this._height); + this._data = bitmapper.dataToBitMap(data, this._width, this._height, + this._bpp, + this._depth); + // todo yuck + this.data = this._parser.reverseFiltered(this._data, this._depth, this._width, this._height); }; ParserSync.prototype._handleError = function(err) { @@ -78,10 +83,11 @@ ParserSync.prototype._inflateData = function(data) { this._inflateDataList.push(data); }; -ParserSync.prototype._createData = function(width, height, bpp) { +ParserSync.prototype._createData = function(width, height, bpp, depth) { this._data = new Buffer(width * height * 4); this._bpp = bpp; this._width = width; this._height = height; + this._depth = depth; return this._data; }; diff --git a/lib/parser.js b/lib/parser.js index 680154f..ac138ef 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -164,7 +164,7 @@ Parser.prototype._parseIHDR = function(data) { // 'compr', compr, 'filter', filter, 'interlace', interlace // ); - if (depth != 8) { + if (depth !== 8 && depth !== 4 && depth !== 2 && depth !== 1) { this.error(new Error('Unsupported bit depth ' + depth)); return; } @@ -187,17 +187,17 @@ Parser.prototype._parseIHDR = function(data) { this._colorType = colorType; - this._data = this.createData(width, height, colorTypeToBppMap[this._colorType]); + this.createData(width, height, colorTypeToBppMap[this._colorType], depth); this._hasIHDR = true; this.metadata({ width: width, height: height, + depth: depth, palette: !!(colorType & constants.COLOR_PALETTE), color: !!(colorType & constants.COLOR_COLOR), - alpha: !!(colorType & constants.COLOR_ALPHA), - data: this._data + alpha: !!(colorType & constants.COLOR_ALPHA) }); this._handleChunkEnd(); @@ -298,10 +298,10 @@ Parser.prototype._parseIEND = function(data) { this.finished(); }; -Parser.prototype.reverseFiltered = function(data, width, height) { +Parser.prototype.reverseFiltered = function(data, depth, width, height) { if (this._colorType == 3) { // paletted - + //console.log("paletted"); // use values from palette var pxLineLength = width << 2; @@ -312,10 +312,31 @@ Parser.prototype.reverseFiltered = function(data, width, height) { var pxPos = pxRowPos + (x << 2), color = this._palette[data[pxPos]]; + if (!color) { + console.error("data - " + data[pxPos] + " got no colour"); + console.log("depth is ", depth); + return; + } + for (var i = 0; i < 4; i++) data[pxPos + i] = color[i]; } } + } else if (depth !== 8) { + //console.log("adjusting"); + var pxLineLength = width << 2; + var maxOutSample = 255; + var maxInSample = Math.pow(2, depth) - 1; + + for (var y = 0; y < height; y++) { + var pxRowPos = y * pxLineLength; + + for (var x = 0; x < width; x++) { + var pxPos = pxRowPos + (x << 2); + for (var i = 0; i < 4; i++) + data[pxPos + i] = Math.floor((data[pxPos + i] * maxOutSample) / maxInSample + 0.5); + } + } } return data; }; diff --git a/lib/png.js b/lib/png.js index 95b1542..0fe8558 100755 --- a/lib/png.js +++ b/lib/png.js @@ -112,9 +112,7 @@ PNG.prototype.end = function(data) { PNG.prototype._metadata = function(metadata) { this.width = metadata.width; this.height = metadata.height; - this.data = metadata.data; - delete metadata.data; this.emit('metadata', metadata); }; diff --git a/test/test.js b/test/test.js index 53b7a40..dadd8d9 100644 --- a/test/test.js +++ b/test/test.js @@ -12,26 +12,36 @@ fs.readdir(__dirname + '/in/', function(err, files) { var expectedError = false; if (file.match(/^x/) || - file.match(/^...i/) || // interlace - file.match(/^......(01|02|04|16)/) || // 1/2/4/16 bit - file.match(/^basn3p(01|02|04)/) // 2/4/16 colour palette + file.match(/^...i/) ||// interlace + file.match(/^......(16)/) // 1/2/4/16 bit ) { expectedError = true; } - if (!expectedError) { - var data = fs.readFileSync(__dirname + '/in/' + file); + var data = fs.readFileSync(__dirname + '/in/' + file); + try { + console.log("Sync: parsing..", file); var png = PNG.sync.read(data); - - var outpng = new PNG(); - PNG.adjustGamma(png); - outpng.data = png.data; - outpng.width = png.width; - outpng.height = png.height; - outpng.pack() - .pipe(fs.createWriteStream(__dirname + '/outsync/' + file)); + } catch (e) { + if (!expectedError) { + console.log(e); + console.log(e.stack); + } + return; } + if (expectedError) { + console.log("Error expected, parsed fine", file); + } + + var outpng = new PNG(); + PNG.adjustGamma(png); + outpng.data = png.data; + outpng.width = png.width; + outpng.height = png.height; + outpng.pack() + .pipe(fs.createWriteStream(__dirname + '/outsync/' + file)); + fs.createReadStream(__dirname + '/in/' + file) .pipe(new PNG()) .on('error', function(err) {