diff --git a/.gitignore b/.gitignore index 24cd046..3c3629e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ node_modules -*.png diff --git a/examples/test/img/PngSuite.LICENSE b/examples/test/img/PngSuite.LICENSE new file mode 100644 index 0000000..8d4d1d0 --- /dev/null +++ b/examples/test/img/PngSuite.LICENSE @@ -0,0 +1,9 @@ +PngSuite +-------- + +Permission to use, copy, modify and distribute these images for any +purpose and without fee is hereby granted. + + +(c) Willem van Schaik, 1996, 2011 + diff --git a/examples/test/img/PngSuite.README b/examples/test/img/PngSuite.README new file mode 100644 index 0000000..3ef8f24 --- /dev/null +++ b/examples/test/img/PngSuite.README @@ -0,0 +1,25 @@ + PNGSUITE +---------------- + + testset for PNG-(de)coders + created by Willem van Schaik +------------------------------------ + +This is a collection of graphics images created to test the png applications +like viewers, converters and editors. All (as far as that is possible) +formats supported by the PNG standard are represented. + +The suite consists of the following files: + +- PngSuite.README - this file +- PngSuite.LICENSE - the PngSuite is freeware +- PngSuite.png - image with PngSuite logo +- PngSuite.tgz - archive of all PNG testfiles +- PngSuite.zip - same in .zip format for PCs + + +-------- + (c) Willem van Schaik + willem@schaik.com + Calgary, April 2011 + diff --git a/examples/test/img/basn0g08.png b/examples/test/img/basn0g08.png new file mode 100644 index 0000000..23c8237 Binary files /dev/null and b/examples/test/img/basn0g08.png differ diff --git a/examples/test/img/basn2c08.png b/examples/test/img/basn2c08.png new file mode 100644 index 0000000..db5ad15 Binary files /dev/null and b/examples/test/img/basn2c08.png differ diff --git a/examples/test/img/basn3p08.png b/examples/test/img/basn3p08.png new file mode 100644 index 0000000..0ddad07 Binary files /dev/null and b/examples/test/img/basn3p08.png differ diff --git a/examples/test/img/basn4a08.png b/examples/test/img/basn4a08.png new file mode 100644 index 0000000..3e13052 Binary files /dev/null and b/examples/test/img/basn4a08.png differ diff --git a/examples/test/img/basn6a08.png b/examples/test/img/basn6a08.png new file mode 100644 index 0000000..e608738 Binary files /dev/null and b/examples/test/img/basn6a08.png differ diff --git a/examples/test/img/f00n0g08.png b/examples/test/img/f00n0g08.png new file mode 100644 index 0000000..45a0075 Binary files /dev/null and b/examples/test/img/f00n0g08.png differ diff --git a/examples/test/img/f00n2c08.png b/examples/test/img/f00n2c08.png new file mode 100644 index 0000000..d6a1fff Binary files /dev/null and b/examples/test/img/f00n2c08.png differ diff --git a/examples/test/img/f01n0g08.png b/examples/test/img/f01n0g08.png new file mode 100644 index 0000000..4a1107b Binary files /dev/null and b/examples/test/img/f01n0g08.png differ diff --git a/examples/test/img/f01n2c08.png b/examples/test/img/f01n2c08.png new file mode 100644 index 0000000..26fee95 Binary files /dev/null and b/examples/test/img/f01n2c08.png differ diff --git a/examples/test/img/f02n0g08.png b/examples/test/img/f02n0g08.png new file mode 100644 index 0000000..bfe410c Binary files /dev/null and b/examples/test/img/f02n0g08.png differ diff --git a/examples/test/img/f02n2c08.png b/examples/test/img/f02n2c08.png new file mode 100644 index 0000000..e590f12 Binary files /dev/null and b/examples/test/img/f02n2c08.png differ diff --git a/examples/test/img/f03n0g08.png b/examples/test/img/f03n0g08.png new file mode 100644 index 0000000..ed01e29 Binary files /dev/null and b/examples/test/img/f03n0g08.png differ diff --git a/examples/test/img/f03n2c08.png b/examples/test/img/f03n2c08.png new file mode 100644 index 0000000..7581150 Binary files /dev/null and b/examples/test/img/f03n2c08.png differ diff --git a/examples/test/img/f04n0g08.png b/examples/test/img/f04n0g08.png new file mode 100644 index 0000000..663fdae Binary files /dev/null and b/examples/test/img/f04n0g08.png differ diff --git a/examples/test/img/f04n2c08.png b/examples/test/img/f04n2c08.png new file mode 100644 index 0000000..3c8b511 Binary files /dev/null and b/examples/test/img/f04n2c08.png differ diff --git a/examples/test/img/tbrn2c08.png b/examples/test/img/tbrn2c08.png new file mode 100644 index 0000000..5cca0d6 Binary files /dev/null and b/examples/test/img/tbrn2c08.png differ diff --git a/examples/test/img/tp0n0g08.png b/examples/test/img/tp0n0g08.png new file mode 100644 index 0000000..333465f Binary files /dev/null and b/examples/test/img/tp0n0g08.png differ diff --git a/examples/test/img/tp0n2c08.png b/examples/test/img/tp0n2c08.png new file mode 100644 index 0000000..fc6e42c Binary files /dev/null and b/examples/test/img/tp0n2c08.png differ diff --git a/examples/test/img/tp0n3p08.png b/examples/test/img/tp0n3p08.png new file mode 100644 index 0000000..69a69e5 Binary files /dev/null and b/examples/test/img/tp0n3p08.png differ diff --git a/examples/test/img/tp1n3p08.png b/examples/test/img/tp1n3p08.png new file mode 100644 index 0000000..a6c9f35 Binary files /dev/null and b/examples/test/img/tp1n3p08.png differ diff --git a/examples/test/list.html b/examples/test/list.html new file mode 100644 index 0000000..1e3aa12 --- /dev/null +++ b/examples/test/list.html @@ -0,0 +1,38 @@ + + + + PNG Test + + + +

Basic

+ grayscale
+ color
+ paletted
+ grayscale + alpha
+ color + alpha
+ +

Image filtering

+ grayscale, filter 0
+ color, filter 0
+ grayscale, filter 1
+ color, filter 1
+ grayscale, filter 2
+ color, filter 2
+ grayscale, filter 3
+ color, filter 3
+ grayscale, filter 4
+ color, filter 4
+ +

Transparency

+ grayscale, not transparent
+ color, not transparent
+ paletted, not transparent
+ color, transparent
+ paletted, transparent
+ + diff --git a/examples/test/test.js b/examples/test/test.js new file mode 100644 index 0000000..5e59d6d --- /dev/null +++ b/examples/test/test.js @@ -0,0 +1,36 @@ + +var fs = require('fs'), + PNG = require('pngjs').PNG; + + +fs.readdir(__dirname + '/img/', function(err, files) { + if (err) throw err; + + for (var i = 0; i < files.length; i++) { + + if (!files[i].match(/\.png$/i)) + continue; + + fs.createReadStream(__dirname + '/img/' + files[i]) + .pipe(new PNG()) + .on('parsed', function() { + + if (this.gamma) { + for (var y = 0; y < this.height; y++) { + for (var x = 0; x < this.width; x++) { + var idx = (this.width * y + x) << 2; + + for (var i = 0; i < 3; i++) { + var sample = this.data[idx + i] / 255; + sample = Math.pow(sample, 1 / this.gamma); + sample = Math.pow(sample, 1 / 2.2); + this.data[idx + i] = Math.round(sample * 255); + } + } + } + } + + this.pack(); + }).pipe(fs.createWriteStream(__dirname + '/out/' + files[i])); + } +}); diff --git a/examples/test2.js b/examples/test2.js index fe46892..466bd1b 100755 --- a/examples/test2.js +++ b/examples/test2.js @@ -5,7 +5,7 @@ var fs = require('fs'), var png = new PNG({ - filterType: 4 + filterType: -1 }), src = fs.createReadStream(process.argv[2]), dst = fs.createWriteStream(process.argv[3] || 'out.png'); diff --git a/lib/filter.js b/lib/filter.js index 379a65f..3b8ec7e 100644 --- a/lib/filter.js +++ b/lib/filter.js @@ -55,58 +55,112 @@ Filter.prototype.prepare = function(width, height, type) { this.height = height; }; -Filter.prototype.unfilter = function(rawData) { +Filter.prototype.unfilter = function(rawData, Bpp) { var pxLineLength = this.width << 2, - rawLineLength = pxLineLength + 1, + rawLineLength = this.width * Bpp + 1, pxData = new Buffer(pxLineLength * this.height); for (var y = 0; y < this.height; y++) { var rawRowPos = rawLineLength * y + 1, pxRowPos = y * pxLineLength, - pxUpRowPos = pxRowPos - pxLineLength, filter = rawData[rawRowPos - 1]; + if (filter == 0) { - rawData.copy(pxData, pxRowPos, rawRowPos, rawRowPos + pxLineLength); + for (var x = 0; x < this.width; x++) { + var pxPos = pxRowPos + (x << 2), + rawPos = rawRowPos + x * Bpp; + + for (var i = 0; i < Bpp; i++) + pxData[pxPos + i] = rawData[rawPos + i]; + } } else if (filter == 1) { - for (var x = 0; x < pxLineLength; x++) { + for (var x = 0; x < this.width; x++) { + var pxPos = pxRowPos + (x << 2), + rawPos = rawRowPos + x * Bpp; - var left = x >= 4 ? pxData[pxRowPos + x - 4] : 0; - pxData[pxRowPos + x] = rawData[rawRowPos + x] + left; + 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 == 2) { - for (var x = 0; x < pxLineLength; x++) { + for (var x = 0; x < this.width; x++) { + var pxPos = pxRowPos + (x << 2), + rawPos = rawRowPos + x * Bpp; - var up = y > 0 ? pxData[pxUpRowPos + x] : 0; - pxData[pxRowPos + x] = rawData[rawRowPos + x] + up; + 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 == 3) { - for (var x = 0; x < pxLineLength; x++) { + for (var x = 0; x < this.width; x++) { + var pxPos = pxRowPos + (x << 2), + rawPos = rawRowPos + x * Bpp; - var left = x >= 4 ? pxData[pxRowPos + x - 4] : 0, - up = y > 0 ? pxData[pxUpRowPos + x] : 0; + 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[pxRowPos + x] = rawData[rawRowPos + x] - + Math.floor((left + up) / 2); + pxData[pxPos + i] = rawData[rawPos + i] + + Math.floor((left + up) / 2); + } } } else if (filter == 4) { - for (var x = 0; x < pxLineLength; x++) { + for (var x = 0; x < this.width; x++) { + var pxPos = pxRowPos + (x << 2), + rawPos = rawRowPos + x * Bpp; - var left = x >= 4 ? pxData[pxRowPos + x - 4] : 0, - up = y > 0 ? pxData[pxUpRowPos + x] : 0, - upLeft = x >= 4 && y > 0 ? pxData[pxUpRowPos + x - 4] : 0; + 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[pxRowPos + x] = rawData[rawRowPos + x] - + PaethPredictor(left, up, upLeft) + pxData[pxPos + i] = rawData[rawPos + i] + + PaethPredictor(left, up, upLeft) + } } } } + + // expand data to 32 bit + for (var y = 0; y < this.height; y++) { + var pxRowPos = y * pxLineLength; + + if (Bpp == 1) { // L + for (var x = 0; x < this.width; x++) { + var pxPos = pxRowPos + (x << 2); + + 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; }; diff --git a/lib/parser.js b/lib/parser.js index ed7b3bf..08117d7 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -32,6 +32,13 @@ 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) { @@ -43,10 +50,17 @@ var Parser = module.exports = function(options) { this._hasIHDR = false; this._hasIEND = false; + // input flags + 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._compress = new Compress(options); this._filter = new Filter(options); @@ -60,9 +74,7 @@ Parser.prototype._initCompress = function() { this._compress.on('error', this.emit.bind(this, 'error')); this._compress.on('deflated', this._packData.bind(this)); - this._compress.on('inflated', function(data) { - this.emit('parsed', this._filter.unfilter(data)); - }.bind(this)); + this._compress.on('inflated', this._unfilter.bind(this)); }; Parser.prototype._parse = function(data) { @@ -111,6 +123,47 @@ Parser.prototype._packData = function(data) { 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) @@ -154,6 +207,8 @@ Parser.prototype._parseChunk = function(data, idx) { } else if (!ancillary) throw new Error('Unsupported critical chunk type ' + name); + // else + // console.log('Ignoring chunk', name, type.toString(16)); return idx; }; @@ -189,12 +244,12 @@ Parser.prototype._parseIHDR = function(data) { // ); if (depth != 8) - throw new Error('Unsupported bit depth'); - if (colorType != 6) - throw new Error('Unsupported color type'); + throw new Error('Unsupported bit depth ' + depth); if (interlace != 0) throw new Error('Unsupported interlace method'); + this._colorType = colorType; + this._compress.prepareInflate(compr); this._filter.prepare(width, height, filter); @@ -218,7 +273,48 @@ Parser.prototype._packIHDR = function(width, height) { }; +Parser.prototype._parsePLTE = function(data) { + + var entries = Math.floor(data.length / 3); + // console.log('Palette:', entries); + + for (var i = 0; i < entries; i++) { + this._palette.push([ + data.readUInt8(i * 3), + data.readUInt8(i * 3 + 1), + data.readUInt8(i * 3 + 2 ), + 0xff + ]); + } +}; + +Parser.prototype._parseTRNS = function(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++) + 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 +}; + +Parser.prototype._parseGAMA = function(data) { + this.emit('gamma', data.readUInt32BE(0) / 100000); +}; + Parser.prototype._parseIDAT = function(data) { + + if (this._colorType == 3 && this._palette.length == 0) + throw new Error('Expected palette not found'); + this._compress.writeInflate(data); }; diff --git a/lib/png.js b/lib/png.js index 00cf3d1..693db79 100644 --- a/lib/png.js +++ b/lib/png.js @@ -26,7 +26,7 @@ var util = require('util'), var PNG = exports.PNG = function(options) { - Parser.call(this, options); + Parser.call(this, options = options || {}); this.width = options.width || 0; this.height = options.height || 0; @@ -34,12 +34,15 @@ var PNG = exports.PNG = function(options) { this.data = this.width > 0 && this.height > 0 ? new Buffer(4 * this.width * this.height) : null; - this.on('metadata', this._metadata.bind(this)); + 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.on('parsed', function(data) { this.data = data; }.bind(this)); @@ -93,6 +96,10 @@ PNG.prototype._metadata = function(width, height) { this.data = null; }; +PNG.prototype._gamma = function(gamma) { + this.gamma = gamma; +}; + PNG.prototype.bitblt = function(dst, sx, sy, w, h, dx, dy) { var src = this; @@ -100,7 +107,7 @@ PNG.prototype.bitblt = function(dst, sx, sy, w, h, dx, dy) { if (sx > src.width || sy > src.height || sx + w > src.width || sy + h > src.height) throw new Error('bitblt reading outside image'); - if (dy > dst.width || dy > dst.height + if (dx > dst.width || dy > dst.height || dx + w > dst.width || dy + h > dst.height) throw new Error('bitblt writing outside image');