lib/png/processor/filter.js
// Copyright 2015 Yahoo! Inc.
// Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms.
var Interlace = require('./interlace');
var Compressor = require('./compressor');
/**
* @class Filter
* @module PNG
* @submodule PNGCore
* @param {Chunk} headerChunk Header chunk of data stream
* @param {object} [options] Options for the compressor
* @constructor
*/
var Filter = function (headerChunk, options) {
this._headerChunk = headerChunk;
this._options = options || {};
};
/**
* Gets the options
*
* @method getOptions
* @return {object}
*/
Filter.prototype.getOptions = function () {
return this._options;
};
/**
* Gets the header chunk
*
* @method getHeaderChunk
* @return {Chunk}
*/
Filter.prototype.getHeaderChunk = function () {
return this._headerChunk;
};
/**
* Applies filters to the data
*
* @method encode
* @param {Buffer} image
* @return {Buffer}
*/
Filter.prototype.encode = function (image) {
var headerChunk = this.getHeaderChunk(),
interlace = new Interlace(headerChunk),
bytesPerPixel = headerChunk.getBytesPerPixel(),
bytesPerPosition = Math.max(1, bytesPerPixel),
outputData,
length = 0,
info = {},
options = this.getOptions();
// Determine required size of buffer
interlace.processPasses(function (width, height, scanLineLength) {
length += (scanLineLength + 1) * height;
});
outputData = new Buffer(length);
// Process each interlace pass (or only one for non-interlaced images)
interlace.processPasses(function (width, height, scanLineLength) {
info = {
inputData: image,
inputOffset: info.inputOffset || 0,
outputData: outputData,
outputOffset: info.outputOffset || 0,
bytesPerPosition: bytesPerPosition,
scanLineLength: scanLineLength,
scanLines: height,
previousLineOffset: null
};
this._encode(info, options);
}.bind(this));
return outputData;
};
/**
* Applies filters to the data
*
* @method _encode
* @param {object} info Information for filtering process
* @param {object} options Options for encoding
* @private
*/
Filter.prototype._encode = function (info, options) {
var filterType = options.filter || 0,
filterMapping;
// Reverse mapping for filter-types
filterMapping = {
0: this._encodeNone,
1: this._encodeSub,
2: this._encodeUp,
3: this._encodeAverage,
4: this._encodePaeth,
5: this._optimalEncode // Auto
};
// Validate options given
if (!filterMapping[filterType]) {
throw new Error('Unknown filter-type ' + filterType + ' was selected.');
}
// Run through all scanlines
for (var y = 0; y < info.scanLines; y++) {
// Reverse per filter-type
filterMapping[filterType].call(this, info, options);
info.previousLineOffset = info.inputOffset;
info.inputOffset += info.scanLineLength;
info.outputOffset += info.scanLineLength + 1;
}
};
/**
* Applies the optimal filter to the data
*
* Note:
* Since it compresses only a subset of the data, this might not be really the optimal filter.
* But, it is close enough.
*
* @method _optimalEncode
* @param {object} info Information for filtering process
* @param {object} options Options for encoding
* @private
*/
Filter.prototype._optimalEncode = function (info, options) {
var filterList = [4, 3, 2, 1, 0],
filterType,
filterMapping,
compressor = new Compressor(options),
length,
lowestSize = null,
lowestFilter = 0,
// Backup of real data
originalOutputData = info.outputData,
originalOutputOffset = info.outputOffset;
// Reverse mapping for actual filter-types
filterMapping = {
0: this._encodeNone,
1: this._encodeSub,
2: this._encodeUp,
3: this._encodeAverage,
4: this._encodePaeth
};
// Create temp buffer for size testing
info.outputData = new Buffer(info.scanLineLength + 1);
info.outputOffset = 0;
// Walk through all filters to see which one is the best one
while(filterList.length > 0) {
// Use next filter in the list
filterType = filterList.pop();
filterMapping[filterType].call(this, info);
// Trial compress
length = compressor.encode(info.outputData).length;
// Is this the best filter for the compression? (Prefer low complexity filters)
if ((lowestSize === null) || (lowestSize > length)) {
lowestSize = length;
lowestFilter = filterType;
}
}
// Recover original data
info.outputData = originalOutputData;
info.outputOffset = originalOutputOffset;
// Apply optimal filter
filterMapping[lowestFilter].call(this, info);
};
/**
* Applies no filter at all - this is just a pass-through
*
* @method _encodeNone
* @param {object} info
* @private
*/
Filter.prototype._encodeNone = function (info) {
info.outputData[info.outputOffset] = 0;
info.inputData.copy(info.outputData, info.outputOffset + 1, info.inputOffset, info.inputOffset + info.scanLineLength);
};
/**
* Applies the Sub filter
*
* @method _encodeSub
* @param {object} info
* @private
*/
Filter.prototype._encodeSub = function (info) {
info.outputData[info.outputOffset] = 1;
for (var x = 0; x < info.scanLineLength; x++) {
info.outputData[info.outputOffset + x + 1] = this._getInputPixel(info, x) - this._getLeftInputPixel(info, x);
}
};
/**
* Applies the Up filter
*
* @method _encodeUp
* @param {object} info
* @private
*/
Filter.prototype._encodeUp = function (info) {
info.outputData[info.outputOffset] = 2;
for (var x = 0; x < info.scanLineLength; x++) {
info.outputData[info.outputOffset + x + 1] = this._getInputPixel(info, x) - this._getTopInputPixel(info, x);
}
};
/**
* Applies the Average filter
*
* @method _encodeAverage
* @param {object} info
* @private
*/
Filter.prototype._encodeAverage = function (info) {
info.outputData[info.outputOffset] = 3;
for (var x = 0; x < info.scanLineLength; x++) {
info.outputData[info.outputOffset + x + 1] = this._getInputPixel(info, x) - Math.floor((this._getLeftInputPixel(info, x) + this._getTopInputPixel(info, x)) / 2);
}
};
/**
* Applies the Paeth filter
*
* @method _encodePaeth
* @param {object} info
* @private
*/
Filter.prototype._encodePaeth = function (info) {
info.outputData[info.outputOffset] = 4;
for (var x = 0; x < info.scanLineLength; x++) {
info.outputData[info.outputOffset + x + 1] = this._getInputPixel(info, x) - this._paethPredictor(
this._getLeftInputPixel(info, x),
this._getTopInputPixel(info, x),
this._getTopLeftInputPixel(info, x)
);
}
};
/**
* Reverses all filters
*
* @method decode
* @param {Buffer} image
* @return {Buffer} Reversed data
*/
Filter.prototype.decode = function (image) {
var headerChunk = this.getHeaderChunk(),
interlace = new Interlace(headerChunk),
bytesPerPixel = headerChunk.getBytesPerPixel(),
bytesPerPosition = Math.max(1, bytesPerPixel),
outputData,
length = 0,
info = {},
options = this.getOptions();
// Determine required size of buffer
interlace.processPasses(function (width, height, scanLineLength) {
length += scanLineLength * height;
});
outputData = new Buffer(length);
// Process each interlace pass (or only one for non-interlaced images)
interlace.processPasses(function (width, height, scanLineLength) {
info = {
inputData: image,
inputOffset: info.inputOffset || 0,
outputData: outputData,
outputOffset: info.outputOffset || 0,
bytesPerPosition: bytesPerPosition,
scanLineLength: scanLineLength,
scanLines: height,
previousLineOffset: null
};
this._decode(info, options);
}.bind(this));
return outputData;
};
/**
* Reverses all filters
*
* @method _decode
* @param {object} info
* @param {object} options
* @private
*/
Filter.prototype._decode = function (info, options) {
var filterType,
filterMapping;
// Reverse mapping for filter-types
filterMapping = {
0: this._decodeNone,
1: this._decodeSub,
2: this._decodeUp,
3: this._decodeAverage,
4: this._decodePaeth
};
// Run through all scanlines
for (var y = 0; y < info.scanLines; y++) {
// Determine filter-type
filterType = info.inputData[info.inputOffset]; info.inputOffset++;
if ((filterType < 0) || (filterType > 4)) {
throw new Error('Filter: Unknown filter-type ' + filterType);
}
// Reverse per filter-type
filterMapping[filterType].call(this, info);
info.previousLineOffset = info.outputOffset;
info.inputOffset += info.scanLineLength;
info.outputOffset += info.scanLineLength;
}
};
/**
* Reverses nothing at all - this is just a pass-through
*
* @method _decodeNone
* @param {object} info
* @private
*/
Filter.prototype._decodeNone = function (info) {
info.inputData.copy(info.outputData, info.outputOffset, info.inputOffset, info.inputOffset + info.scanLineLength);
};
/**
* Reverses the Sub filter
*
* @method _decodeSub
* @param {object} info
* @private
*/
Filter.prototype._decodeSub = function (info) {
for (var x = 0; x < info.scanLineLength; x++) {
info.outputData[info.outputOffset + x] = (this._getInputPixel(info, x) + this._getLeftOutputPixel(info, x)) & 0xff;
}
};
/**
* Reverses the Up filter
*
* @method _decodeUp
* @param {object} info
* @private
*/
Filter.prototype._decodeUp = function (info) {
for (var x = 0; x < info.scanLineLength; x++) {
info.outputData[info.outputOffset + x] = (this._getInputPixel(info, x) + this._getTopOutputPixel(info, x)) & 0xff;
}
};
/**
* Reverses the Average filter
*
* @method _decodeAverage
* @param {object} info
* @private
*/
Filter.prototype._decodeAverage = function (info) {
for (var x = 0; x < info.scanLineLength; x++) {
info.outputData[info.outputOffset + x] = (this._getInputPixel(info, x) + Math.floor((this._getLeftOutputPixel(info, x) + this._getTopOutputPixel(info, x)) / 2)) & 0xff;
}
};
/**
* Reverses the Paeth filter
*
* @method _decodePaeth
* @param {object} info
* @private
*/
Filter.prototype._decodePaeth = function (info) {
for (var x = 0; x < info.scanLineLength; x++) {
info.outputData[info.outputOffset + x] =
this._getInputPixel(info, x) + this._paethPredictor(
this._getLeftOutputPixel(info, x),
this._getTopOutputPixel(info, x),
this._getTopLeftOutputPixel(info, x)
) & 0xff;
}
};
/**
* Paeth-predictor algorithm
*
* @method _paethPredictor
* @param {int} left Left pixel
* @param {int} top Top pixel
* @param {int} topLeft Top-left pixel
* @return {int} Result of algorithm
* @private
*/
Filter.prototype._paethPredictor = function (left, top, topLeft) {
var p = left + top - topLeft,
pLeft = Math.abs(p - left),
pTop = Math.abs(p - top),
pTopLeft = Math.abs(p - topLeft);
if ((pLeft <= pTop) && (pLeft <= pTopLeft)) {
return left;
} else if (pTop <= pTopLeft) {
return top;
} else {
return topLeft;
}
};
/**
* Gets the current pixel
*
* @method _getInputPixel
* @param {object} info
* @param {int} x X-coordinate in current scanline
* @return {int}
* @private
*/
Filter.prototype._getInputPixel = function (info, x) {
return info.inputData[info.inputOffset + x];
};
/**
* Gets the pixel at the left from the current pixel from the output buffer
*
* @method _getLeftOutputPixel
* @param {object} info
* @param {int} x X-coordinate in current scanline
* @return {int}
* @private
*/
Filter.prototype._getLeftOutputPixel = function (info, x) {
return (x < info.bytesPerPosition) ? 0 : info.outputData[info.outputOffset + x - info.bytesPerPosition];
};
/**
* Gets the pixel at the top from the current pixel from the output buffer
*
* @method _getTopOutputPixel
* @param {object} info
* @param {int} x X-coordinate in current scanline
* @return {int}
* @private
*/
Filter.prototype._getTopOutputPixel = function (info, x) {
return (info.previousLineOffset === null) ? 0 : info.outputData[info.previousLineOffset + x];
};
/**
* Gets the pixel at the top-left from the current pixel from the output buffer
*
* @method _getTopLeftOutputPixel
* @param {object} info
* @param {int} x X-coordinate in current scanline
* @return {int}
* @private
*/
Filter.prototype._getTopLeftOutputPixel = function (info, x) {
return ((info.previousLineOffset === null) || (x < info.bytesPerPosition)) ? 0 : info.outputData[info.previousLineOffset + x - info.bytesPerPosition];
};
/**
* Gets the pixel at the left from the current pixel from the input buffer
*
* @method _getLeftInputPixel
* @param {object} info
* @param {int} x X-coordinate in current scanline
* @return {int}
* @private
*/
Filter.prototype._getLeftInputPixel = function (info, x) {
return (x < info.bytesPerPosition) ? 0 : info.inputData[info.inputOffset + x - info.bytesPerPosition];
};
/**
* Gets the pixel at the top from the current pixel from the input buffer
*
* @method _getTopInputPixel
* @param {object} info
* @param {int} x X-coordinate in current scanline
* @return {int}
* @private
*/
Filter.prototype._getTopInputPixel = function (info, x) {
return (info.previousLineOffset === null) ? 0 : info.inputData[info.previousLineOffset + x];
};
/**
* Gets the pixel at the top-left from the current pixel from the input buffer
*
* @method _getTopLeftInputPixel
* @param {object} info
* @param {int} x X-coordinate in current scanline
* @return {int}
* @private
*/
Filter.prototype._getTopLeftInputPixel = function (info, x) {
return ((info.previousLineOffset === null) || (x < info.bytesPerPosition)) ? 0 : info.inputData[info.previousLineOffset + x - info.bytesPerPosition];
};
module.exports = Filter;