Support for 1,2,4 bits PNG

This commit is contained in:
Luke Page 2015-08-01 11:56:47 +01:00
parent 807ffcd66f
commit d601f60c79
10 changed files with 278 additions and 154 deletions

120
lib/bitmapper.js Normal file
View file

@ -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
}
};

View file

@ -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();

View file

@ -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);
};

View file

@ -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++) {

View file

@ -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({

View file

@ -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);
};

View file

@ -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;
};

View file

@ -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;
};

View file

@ -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);
};

View file

@ -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) {