Tidy up filtering.

This commit is contained in:
Luke Page 2015-08-07 07:59:57 +01:00
parent 6607d71c04
commit adf5216a49
12 changed files with 394 additions and 331 deletions

View file

@ -4,8 +4,8 @@ clone_depth: 10
environment:
matrix:
- nodejs_version: 0.12
- nodejs_version: 2.x
- nodejs_version: '0.12'
- nodejs_version: ''
install:
- ps: Install-Product node $env:nodejs_version

186
lib/filter-pack.js Normal file
View file

@ -0,0 +1,186 @@
'use strict';
var paethPredictor = require('./paeth-predictor');
function filterNone(pxData, pxPos, byteWidth, rawData, rawPos) {
pxData.copy(rawData, rawPos, pxPos, pxPos + byteWidth);
}
function filterSumNone(pxData, pxPos, byteWidth) {
var sum = 0;
var length = pxPos + byteWidth;
for (var i = pxPos; i < length; i++) {
sum += Math.abs(pxData[i]);
}
return sum;
}
function filterSub(pxData, pxPos, byteWidth, rawData, rawPos) {
for (var x = 0; x < byteWidth; x++) {
var left = x >= 4 ? pxData[pxPos + x - 4] : 0;
var val = pxData[pxPos + x] - left;
rawData[rawPos + x] = val;
}
}
function filterSumSub(pxData, pxPos, byteWidth) {
var sum = 0;
for (var x = 0; x < byteWidth; x++) {
var left = x >= 4 ? pxData[pxPos + x - 4] : 0;
var val = pxData[pxPos + x] - left;
sum += Math.abs(val);
}
return sum;
}
function filterUp(pxData, pxPos, byteWidth, rawData, rawPos) {
for (var x = 0; x < byteWidth; x++) {
var up = pxPos > 0 ? pxData[pxPos + x - byteWidth] : 0;
var val = pxData[pxPos + x] - up;
rawData[rawPos + x] = val;
}
}
function filterSumUp(pxData, pxPos, byteWidth) {
var sum = 0;
var length = pxPos + byteWidth;
for (var x = pxPos; x < length; x++) {
var up = pxPos > 0 ? pxData[x - byteWidth] : 0;
var val = pxData[x] - up;
sum += Math.abs(val);
}
return sum;
}
function filterAvg(pxData, pxPos, byteWidth, rawData, rawPos) {
for (var x = 0; x < byteWidth; x++) {
var left = x >= 4 ? pxData[pxPos + x - 4] : 0;
var up = pxPos > 0 ? pxData[pxPos + x - byteWidth] : 0;
var val = pxData[pxPos + x] - ((left + up) >> 1);
rawData[rawPos + x] = val;
}
}
function filterSumAvg(pxData, pxPos, byteWidth) {
var sum = 0;
for (var x = 0; x < byteWidth; x++) {
var left = x >= 4 ? pxData[pxPos + x - 4] : 0;
var up = pxPos > 0 ? pxData[pxPos + x - byteWidth] : 0;
var val = pxData[pxPos + x] - ((left + up) >> 1);
sum += Math.abs(val);
}
return sum;
}
function filterPaeth(pxData, pxPos, byteWidth, rawData, rawPos) {
for (var x = 0; x < byteWidth; x++) {
var left = x >= 4 ? pxData[pxPos + x - 4] : 0;
var up = pxPos > 0 ? pxData[pxPos + x - byteWidth] : 0;
var upleft = pxPos > 0 && x >= 4 ? pxData[pxPos + x - (byteWidth + 4)] : 0;
var val = pxData[pxPos + x] - paethPredictor(left, up, upleft);
rawData[rawPos + x] = val;
}
}
function filterSumPaeth(pxData, pxPos, byteWidth) {
var sum = 0;
for (var x = 0; x < byteWidth; x++) {
var left = x >= 4 ? pxData[pxPos + x - 4] : 0;
var up = pxPos > 0 ? pxData[pxPos + x - byteWidth] : 0;
var upleft = pxPos > 0 && x >= 4 ? pxData[pxPos + x - (byteWidth + 4)] : 0;
var val = pxData[pxPos + x] - paethPredictor(left, up, upleft);
sum += Math.abs(val);
}
return sum;
}
var filters = {
0: filterNone,
1: filterSub,
2: filterUp,
3: filterAvg,
4: filterPaeth
};
var filterSums = {
0: filterSumNone,
1: filterSumSub,
2: filterSumUp,
3: filterSumAvg,
4: filterSumPaeth
};
module.exports = function(pxData, width, height, options) {
var filterTypes;
if (!('filterType' in options) || options.filterType === -1) {
filterTypes = [0, 1, 2, 3, 4];
}
else if (typeof options.filterType === 'number') {
filterTypes = [options.filterType];
}
else {
throw new Error('unrecognised filter types');
}
var byteWidth = width << 2;
var rawPos = 0;
var pxPos = 0;
var rawData = new Buffer((byteWidth + 1) * height);
var sel = filterTypes[0];
for (var y = 0; y < height; y++) {
if (filterTypes.length > 1) {
// find best filter for this line (with lowest sum of values)
var min = Infinity;
for (var i = 0; i < filterTypes.length; i++) {
var sum = filterSums[filterTypes[i]](pxData, pxPos, byteWidth);
if (sum < min) {
sel = filterTypes[i];
min = sum;
}
}
}
rawData[rawPos] = sel;
rawPos++;
filters[sel](pxData, pxPos, byteWidth, rawData, rawPos);
rawPos += byteWidth;
pxPos += byteWidth;
}
return rawData;
};

View file

@ -2,15 +2,15 @@
var util = require('util');
var ChunkStream = require('./chunkstream');
var Filter = require('./filter');
var Filter = require('./filter-parse');
var FilterAsync = module.exports = function(width, height, Bpp, depth, interlace, options) {
var FilterAsync = module.exports = function(width, height, Bpp, depth, interlace) {
ChunkStream.call(this);
var buffers = [];
var that = this;
this._filter = new Filter(width, height, Bpp, depth, interlace, options, {
this._filter = new Filter(width, height, Bpp, depth, interlace, {
read: this.read.bind(this),
complete: function() {
that.emit('complete', Buffer.concat(buffers), width, height);

View file

@ -1,14 +1,14 @@
'use strict';
var SyncReader = require('./sync-reader');
var Filter = require('./filter');
var Filter = require('./filter-parse');
exports.process = function(inBuffer, width, height, Bpp, depth, interlace, options) {
exports.process = function(inBuffer, width, height, Bpp, depth, interlace) {
var outBuffers = [];
var reader = new SyncReader(inBuffer);
var filter = new Filter(width, height, Bpp, depth, interlace, options, {
var filter = new Filter(width, height, Bpp, depth, interlace, {
read: reader.read.bind(reader),
write: function(bufferPart) {
outBuffers.push(bufferPart);

169
lib/filter-parse.js Normal file
View file

@ -0,0 +1,169 @@
'use strict';
var interlaceUtils = require('./interlace');
var paethPredictor = require('./paeth-predictor');
function getByteWidth(width, bpp, depth) {
var byteWidth = width * bpp;
if (depth !== 8) {
byteWidth = Math.ceil(byteWidth / (8 / depth));
}
return byteWidth;
}
var Filter = module.exports = function(width, height, Bpp, depth, interlace, dependencies) {
this._width = width;
this._height = height;
this._Bpp = Bpp; //TODO rename
this._depth = depth;
this.read = dependencies.read;
this.write = dependencies.write;
this.complete = dependencies.complete;
this._imageIndex = 0;
this._images = [];
if (interlace) {
var passes = interlaceUtils.getImagePasses(width, height);
for (var i = 0; i < passes.length; i++) {
this._images.push({
byteWidth: getByteWidth(passes[i].width, Bpp, depth),
height: passes[i].height,
lineIndex: 0
});
}
}
else {
this._images.push({
byteWidth: getByteWidth(width, Bpp, depth),
height: height,
lineIndex: 0
});
}
// when filtering the line we look at the pixel to the left
// the spec also says it is done on a byte level regardless of the number of pixels
// so if the depth is byte compatible (8 or 16) we subtract the bpp in order to compare back
// a pixel rather than just a different byte part. However if we are sub byte, we ignore.
if (depth === 8) {
this._xComparison = Bpp;
}
else if (depth === 16) {
this._xComparison = this._Bpp * 2;
}
else {
this._xComparison = 1;
}
};
Filter.prototype.start = function() {
this.read(this._images[this._imageIndex].byteWidth + 1, this._reverseFilterLine.bind(this));
};
Filter.prototype._unFilterType1 = function(rawData, unfilteredLine, byteWidth) {
var xComparison = this._xComparison;
var xBiggerThan = xComparison - 1;
for (var x = 0; x < byteWidth; x++) {
var rawByte = rawData[1 + x];
var f1Left = x > xBiggerThan ? unfilteredLine[x - xComparison] : 0;
unfilteredLine[x] = rawByte + f1Left;
}
};
Filter.prototype._unFilterType2 = function(rawData, unfilteredLine, byteWidth) {
var lastLine = this._lastLine;
for (var x = 0; x < byteWidth; x++) {
var rawByte = rawData[1 + x];
var f2Up = lastLine ? lastLine[x] : 0;
unfilteredLine[x] = rawByte + f2Up;
}
};
Filter.prototype._unFilterType3 = function(rawData, unfilteredLine, byteWidth) {
var xComparison = this._xComparison;
var xBiggerThan = xComparison - 1;
var lastLine = this._lastLine;
for (var x = 0; x < byteWidth; x++) {
var rawByte = rawData[1 + x];
var f3Up = lastLine ? lastLine[x] : 0;
var f3Left = x > xBiggerThan ? unfilteredLine[x - xComparison] : 0;
var f3Add = Math.floor((f3Left + f3Up) / 2);
unfilteredLine[x] = rawByte + f3Add;
}
};
Filter.prototype._unFilterType4 = function(rawData, unfilteredLine, byteWidth) {
var xComparison = this._xComparison;
var xBiggerThan = xComparison - 1;
var lastLine = this._lastLine;
for (var x = 0; x < byteWidth; x++) {
var rawByte = rawData[1 + x];
var f4Up = lastLine ? lastLine[x] : 0;
var f4Left = x > xBiggerThan ? unfilteredLine[x - xComparison] : 0;
var f4UpLeft = x > xBiggerThan && lastLine ? lastLine[x - xComparison] : 0;
var f4Add = paethPredictor(f4Left, f4Up, f4UpLeft);
unfilteredLine[x] = rawByte + f4Add;
}
};
Filter.prototype._reverseFilterLine = function(rawData) {
var filter = rawData[0];
var unfilteredLine;
var currentImage = this._images[this._imageIndex];
var byteWidth = currentImage.byteWidth;
if (filter === 0) {
unfilteredLine = rawData.slice(1, byteWidth + 1);
}
else {
unfilteredLine = new Buffer(byteWidth);
switch (filter) {
case 1:
this._unFilterType1(rawData, unfilteredLine, byteWidth);
break;
case 2:
this._unFilterType2(rawData, unfilteredLine, byteWidth);
break;
case 3:
this._unFilterType3(rawData, unfilteredLine, byteWidth);
break;
case 4:
this._unFilterType4(rawData, unfilteredLine, byteWidth);
break;
default:
throw new Error('Unrecognised filter type - ' + filter);
}
}
this.write(unfilteredLine);
currentImage.lineIndex++;
if (currentImage.lineIndex >= currentImage.height) {
this._lastLine = null;
this._imageIndex++;
currentImage = this._images[this._imageIndex];
}
else {
this._lastLine = unfilteredLine;
}
if (currentImage) {
// read, using the byte width that may be from the new current image
this.read(currentImage.byteWidth + 1, this._reverseFilterLine.bind(this));
}
else {
this.complete(this._width, this._height);
}
};

View file

@ -1,308 +0,0 @@
'use strict';
var interlaceUtils = require('./interlace');
function getByteWidth(width, bpp, depth) {
var byteWidth = width * bpp;
if (depth !== 8) {
byteWidth = Math.ceil(byteWidth / (8 / depth));
}
return byteWidth;
}
function paethPredictor(left, above, upLeft) {
var paeth = left + above - upLeft;
var pLeft = Math.abs(paeth - left);
var pAbove = Math.abs(paeth - above);
var pUpLeft = Math.abs(paeth - upLeft);
if (pLeft <= pAbove && pLeft <= pUpLeft) {
return left;
}
if (pAbove <= pUpLeft) {
return above;
}
return upLeft;
}
var Filter = module.exports = function(width, height, Bpp, depth, interlace, options, dependencies) {
this._width = width;
this._height = height;
this._Bpp = Bpp; //TODO rename
this._depth = depth;
this._options = options;
if (!('filterType' in options) || options.filterType === -1) {
options.filterType = [0, 1, 2, 3, 4];
}
else if (typeof options.filterType === 'number') {
options.filterType = [options.filterType];
}
this._filters = {
0: this._filterNone.bind(this),
1: this._filterSub.bind(this),
2: this._filterUp.bind(this),
3: this._filterAvg.bind(this),
4: this._filterPaeth.bind(this)
};
this.read = dependencies.read;
this.write = dependencies.write;
this.complete = dependencies.complete;
this._imageIndex = 0;
this._images = [];
if (interlace) {
var passes = interlaceUtils.getImagePasses(width, height);
for (var i = 0; i < passes.length; i++) {
this._images.push({
byteWidth: getByteWidth(passes[i].width, Bpp, depth),
height: passes[i].height,
lineIndex: 0
});
}
}
else {
this._images.push({
byteWidth: getByteWidth(width, Bpp, depth),
height: height,
lineIndex: 0
});
}
};
Filter.prototype.start = function() {
this.read(this._images[this._imageIndex].byteWidth + 1, this._reverseFilterLine.bind(this));
};
Filter.prototype._reverseFilterLine = function(rawData) {
var currentImage = this._images[this._imageIndex];
var line = new Buffer(currentImage.byteWidth);
var filter = rawData[0];
// when filtering the line we look at the pixel to the left
// the spec also says it is done on a byte level regardless of the number of pixels
// so if the depth is byte compatible (8 or 16) we subtract the bpp in order to compare back
// a pixel rather than just a different byte part. However if we are sub byte, we ignore.
var xComparison;
if (this._depth === 8) {
xComparison = this._Bpp;
}
else if (this._depth === 16) {
xComparison = this._Bpp * 2;
}
else {
xComparison = 1;
}
var xBiggerThan = xComparison - 1;
for (var x = 0; x < currentImage.byteWidth; x++) {
var rawByte = rawData[1 + x];
switch (filter) {
case 0:
line[x] = rawByte;
break;
case 1:
var f1Left = x > xBiggerThan ? line[x - xComparison] : 0;
line[x] = rawByte + f1Left;
break;
case 2:
var f2Up = this._lastLine ? this._lastLine[x] : 0;
line[x] = rawByte + f2Up;
break;
case 3:
var f3Up = this._lastLine ? this._lastLine[x] : 0;
var f3Left = x > xBiggerThan ? line[x - xComparison] : 0;
var f3Add = Math.floor((f3Left + f3Up) / 2);
line[x] = rawByte + f3Add;
break;
case 4:
var f4Up = this._lastLine ? this._lastLine[x] : 0;
var f4Left = x > xBiggerThan ? line[x - xComparison] : 0;
var f4UpLeft = x > xBiggerThan && this._lastLine
? this._lastLine[x - xComparison] : 0;
var f4Add = paethPredictor(f4Left, f4Up, f4UpLeft);
line[x] = rawByte + f4Add;
break;
default:
throw new Error('Unrecognised filter type - ' + filter);
}
//if (x === 5) {
// console.log("R", line[3], "G", line[4], "B", line[5]);
//}
}
this.write(line);
currentImage.lineIndex++;
if (currentImage.lineIndex >= currentImage.height) {
this._lastLine = null;
this._imageIndex++;
currentImage = this._images[this._imageIndex];
}
else {
this._lastLine = line;
}
if (currentImage) {
this.read(currentImage.byteWidth + 1, this._reverseFilterLine.bind(this));
}
else {
this.complete(this._width, this._height);
}
};
//TODO pull out
Filter.prototype.filter = function(pxData) {
var 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)
var filterTypes = this._options.filterType;
var min = Infinity;
var sel = 0;
for (var i = 0; i < filterTypes.length; i++) {
var sum = this._filters[filterTypes[i]](pxData, y, null);
if (sum < min) {
sel = filterTypes[i];
min = sum;
}
}
this._filters[sel](pxData, y, rawData);
}
return rawData;
};
Filter.prototype._filterNone = function(pxData, y, rawData) {
var pxRowLength = this._width << 2;
var rawRowLength = pxRowLength + 1;
var sum = 0;
if (!rawData) {
for (var x = 0; x < pxRowLength; x++) {
sum += Math.abs(pxData[y * pxRowLength + x]);
}
}
else {
rawData[y * rawRowLength] = 0;
pxData.copy(rawData, rawRowLength * y + 1, pxRowLength * y, pxRowLength * (y + 1));
}
return sum;
};
Filter.prototype._filterSub = function(pxData, y, rawData) {
var pxRowLength = this._width << 2;
var rawRowLength = pxRowLength + 1;
var sum = 0;
if (rawData) {
rawData[y * rawRowLength] = 1;
}
for (var x = 0; x < pxRowLength; x++) {
var left = x >= 4 ? pxData[y * pxRowLength + x - 4] : 0;
var val = pxData[y * pxRowLength + x] - left;
if (!rawData) {
sum += Math.abs(val);
}
else {
rawData[y * rawRowLength + 1 + x] = val;
}
}
return sum;
};
Filter.prototype._filterUp = function(pxData, y, rawData) {
var pxRowLength = this._width << 2;
var rawRowLength = pxRowLength + 1;
var sum = 0;
if (rawData) {
rawData[y * rawRowLength] = 2;
}
for (var x = 0; x < pxRowLength; x++) {
var up = y > 0 ? pxData[(y - 1) * pxRowLength + x] : 0;
var val = pxData[y * pxRowLength + x] - up;
if (!rawData) {
sum += Math.abs(val);
}
else {
rawData[y * rawRowLength + 1 + x] = val;
}
}
return sum;
};
Filter.prototype._filterAvg = function(pxData, y, rawData) {
var pxRowLength = this._width << 2;
var rawRowLength = pxRowLength + 1;
var sum = 0;
if (rawData) {
rawData[y * rawRowLength] = 3;
}
for (var x = 0; x < pxRowLength; x++) {
var left = x >= 4 ? pxData[y * pxRowLength + x - 4] : 0;
var up = y > 0 ? pxData[(y - 1) * pxRowLength + x] : 0;
var val = pxData[y * pxRowLength + x] - ((left + up) >> 1);
if (!rawData) {
sum += Math.abs(val);
}
else {
rawData[y * rawRowLength + 1 + x] = val;
}
}
return sum;
};
Filter.prototype._filterPaeth = function(pxData, y, rawData) {
var pxRowLength = this._width << 2;
var rawRowLength = pxRowLength + 1;
var sum = 0;
if (rawData) {
rawData[y * rawRowLength] = 4;
}
for (var x = 0; x < pxRowLength; x++) {
var left = x >= 4 ? pxData[y * pxRowLength + x - 4] : 0;
var up = y > 0 ? pxData[(y - 1) * pxRowLength + x] : 0;
var upLeft = x >= 4 && y > 0 ? pxData[(y - 1) * pxRowLength + x - 4] : 0;
var val = pxData[y * pxRowLength + x] - paethPredictor(left, up, upLeft);
if (!rawData) {
sum += Math.abs(val);
}
else {
rawData[y * rawRowLength + 1 + x] = val;
}
}
return sum;
};

View file

@ -4,7 +4,7 @@
var util = require('util');
var Stream = require('stream');
var zlib = require('zlib');
var Filter = require('./filter');
var filter = require('./filter-pack');
var CrcStream = require('./crc');
var constants = require('./constants');
@ -34,9 +34,7 @@ Packer.prototype.pack = function(data, width, height, gamma) {
}
// filter pixel data
//TODO {}
var filter = new Filter(width, height, 4, 8, false, this._options, {});
var filteredData = filter.filter(data);
var filteredData = filter(data, width, height, this._options);
// compress it
var deflate = zlib.createDeflate({

17
lib/paeth-predictor.js Normal file
View file

@ -0,0 +1,17 @@
'use strict';
module.exports = function paethPredictor(left, above, upLeft) {
var paeth = left + above - upLeft;
var pLeft = Math.abs(paeth - left);
var pAbove = Math.abs(paeth - above);
var pUpLeft = Math.abs(paeth - upLeft);
if (pLeft <= pAbove && pLeft <= pUpLeft) {
return left;
}
if (pAbove <= pUpLeft) {
return above;
}
return upLeft;
};

View file

@ -3,7 +3,7 @@
var util = require('util');
var zlib = require('zlib');
var ChunkStream = require('./chunkstream');
var FilterAsync = require('./filter-async');
var FilterAsync = require('./filter-parse-async');
var Parser = require('./parser');
var bitmapper = require('./bitmapper');
var formatNormaliser = require('./format-normaliser');
@ -65,8 +65,7 @@ ParserAsync.prototype._handleBitmapInfo = function(width, height, bpp, depth, in
width, height,
bpp,
depth,
interlace,
this._options
interlace
);
};

View file

@ -3,7 +3,7 @@
var zlib = require('zlib');
var SyncReader = require('./sync-reader');
var FilterSync = require('./filter-sync');
var FilterSync = require('./filter-parse-sync');
var Parser = require('./parser');
var bitmapper = require('./bitmapper');
var formatNormaliser = require('./format-normaliser');
@ -16,7 +16,7 @@ module.exports = function(buffer, options) {
err = _err_;
}
var data, bpp, width, height, depth, interlace;
var bpp, width, height, depth, interlace;
function handleBitmapInfo(_width_, _height_, _bpp_, _depth_, _interlace_) {
bpp = _bpp_;
@ -24,8 +24,6 @@ module.exports = function(buffer, options) {
height = _height_;
depth = _depth_;
interlace = _interlace_;
data = new Buffer(width * height * 4);
}
var metaData;
@ -78,8 +76,7 @@ module.exports = function(buffer, options) {
height,
bpp,
depth,
interlace,
options
interlace
);
inflateData = null;

View file

@ -25,7 +25,7 @@ var PNG = exports.PNG = function(options) {
this.gamma = 0;
this.readable = this.writable = true;
this._parser = new Parser(options || {});
this._parser = new Parser(options);
this._parser.on('error', this.emit.bind(this, 'error'));
this._parser.on('close', this._handleClose.bind(this));

View file

@ -35,7 +35,12 @@ test('outputs background, created from scratch', function (t) {
var out = fs.readFileSync(__dirname + '/bg.png');
var ref = fs.readFileSync(__dirname + '/bg-ref.png');
t.ok(bufferEqual(out, ref), "compares with working file ok");
var isBufferEqual = bufferEqual(out, ref);
t.ok(isBufferEqual, "compares with working file ok");
if (!isBufferEqual) {
console.log(out.length, ref.length);
}
t.end();
});