added support for all color types (but still only 8 bits/sample)
1
.gitignore
vendored
|
|
@ -1,2 +1 @@
|
|||
node_modules
|
||||
*.png
|
||||
|
|
|
|||
9
examples/test/img/PngSuite.LICENSE
Normal file
|
|
@ -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
|
||||
|
||||
25
examples/test/img/PngSuite.README
Normal file
|
|
@ -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
|
||||
|
||||
BIN
examples/test/img/basn0g08.png
Normal file
|
After Width: | Height: | Size: 138 B |
BIN
examples/test/img/basn2c08.png
Normal file
|
After Width: | Height: | Size: 145 B |
BIN
examples/test/img/basn3p08.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
examples/test/img/basn4a08.png
Normal file
|
After Width: | Height: | Size: 126 B |
BIN
examples/test/img/basn6a08.png
Normal file
|
After Width: | Height: | Size: 184 B |
BIN
examples/test/img/f00n0g08.png
Normal file
|
After Width: | Height: | Size: 319 B |
BIN
examples/test/img/f00n2c08.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
examples/test/img/f01n0g08.png
Normal file
|
After Width: | Height: | Size: 321 B |
BIN
examples/test/img/f01n2c08.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
examples/test/img/f02n0g08.png
Normal file
|
After Width: | Height: | Size: 355 B |
BIN
examples/test/img/f02n2c08.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
examples/test/img/f03n0g08.png
Normal file
|
After Width: | Height: | Size: 389 B |
BIN
examples/test/img/f03n2c08.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
examples/test/img/f04n0g08.png
Normal file
|
After Width: | Height: | Size: 269 B |
BIN
examples/test/img/f04n2c08.png
Normal file
|
After Width: | Height: | Size: 985 B |
BIN
examples/test/img/tbrn2c08.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
examples/test/img/tp0n0g08.png
Normal file
|
After Width: | Height: | Size: 719 B |
BIN
examples/test/img/tp0n2c08.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
examples/test/img/tp0n3p08.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
examples/test/img/tp1n3p08.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
38
examples/test/list.html
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>PNG Test</title>
|
||||
<style>
|
||||
body { background: #0f0; font: 12px Arial; margin: 10px; }
|
||||
img { margin: 2px; }
|
||||
h3 { margin: 10px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h3>Basic</h3>
|
||||
<img src="img/basn0g08.png"> <img src="out/basn0g08.png"> grayscale<br>
|
||||
<img src="img/basn2c08.png"> <img src="out/basn2c08.png"> color<br>
|
||||
<img src="img/basn3p08.png"> <img src="out/basn3p08.png"> paletted<br>
|
||||
<img src="img/basn4a08.png"> <img src="out/basn4a08.png"> grayscale + alpha<br>
|
||||
<img src="img/basn6a08.png"> <img src="out/basn6a08.png"> color + alpha<br>
|
||||
|
||||
<h3>Image filtering</h3>
|
||||
<img src="img/f00n0g08.png"> <img src="out/f00n0g08.png"> grayscale, filter 0<br>
|
||||
<img src="img/f00n2c08.png"> <img src="out/f00n2c08.png"> color, filter 0<br>
|
||||
<img src="img/f01n0g08.png"> <img src="out/f01n0g08.png"> grayscale, filter 1<br>
|
||||
<img src="img/f01n2c08.png"> <img src="out/f01n2c08.png"> color, filter 1<br>
|
||||
<img src="img/f02n0g08.png"> <img src="out/f02n0g08.png"> grayscale, filter 2<br>
|
||||
<img src="img/f02n2c08.png"> <img src="out/f02n2c08.png"> color, filter 2<br>
|
||||
<img src="img/f03n0g08.png"> <img src="out/f03n0g08.png"> grayscale, filter 3<br>
|
||||
<img src="img/f03n2c08.png"> <img src="out/f03n2c08.png"> color, filter 3<br>
|
||||
<img src="img/f04n0g08.png"> <img src="out/f04n0g08.png"> grayscale, filter 4<br>
|
||||
<img src="img/f04n2c08.png"> <img src="out/f04n2c08.png"> color, filter 4<br>
|
||||
|
||||
<h3>Transparency</h3>
|
||||
<img src="img/tp0n0g08.png"> <img src="out/tp0n0g08.png"> grayscale, not transparent<br>
|
||||
<img src="img/tp0n2c08.png"> <img src="out/tp0n2c08.png"> color, not transparent<br>
|
||||
<img src="img/tp0n3p08.png"> <img src="out/tp0n3p08.png"> paletted, not transparent<br>
|
||||
<img src="img/tbrn2c08.png"> <img src="out/tbrn2c08.png"> color, transparent<br>
|
||||
<img src="img/tp1n3p08.png"> <img src="out/tp1n3p08.png"> paletted, transparent<br>
|
||||
</body>
|
||||
</html>
|
||||
36
examples/test/test.js
Normal file
|
|
@ -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]));
|
||||
}
|
||||
});
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
108
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);
|
||||
};
|
||||
|
||||
|
|
|
|||
13
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');
|
||||
|
||||
|
|
|
|||