diff --git a/lib/parser-base.js b/lib/parser-base.js new file mode 100644 index 0000000..51debf1 --- /dev/null +++ b/lib/parser-base.js @@ -0,0 +1,361 @@ +// Copyright (c) 2012 Kuba Niegowski +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +'use strict'; + + +var util = require('util'), + zlib = require('zlib'), + CrcStream = require('./crc'), + ChunkStream = require('./chunkstream'), + constants = require('./constants'), + Filter = require('./filter'); + + +var Parser = module.exports = function(options, dependencies) { + + this._options = options; + options.checkCRC = options.checkCRC !== false; + + this._hasIHDR = false; + this._hasIEND = false; + + this._inflate = null; + this._filter = null; + this._crc = null; + + // input flags/metadata + this._palette = []; + this._colorType = 0; + + this._chunks = {}; + this._chunks[constants.TYPE_IHDR] = this._handleIHDR.bind(this); + this._chunks[constants.TYPE_IEND] = this._handleIEND.bind(this); + this._chunks[constants.TYPE_IDAT] = this._handleIDAT.bind(this); + this._chunks[constants.TYPE_PLTE] = this._handlePLTE.bind(this); + this._chunks[constants.TYPE_tRNS] = this._handleTRNS.bind(this); + this._chunks[constants.TYPE_gAMA] = this._handleGAMA.bind(this); + + this.writable = true; + + //this.on('error', this._handleError.bind(this)); + //this._handleSignature(); + this.read = dependencies.read; + this.error = dependencies.error; +}; +util.inherits(Parser, ChunkStream); + + +Parser.prototype._handleError = function() { + + this.writable = false; + + this.destroy(); + + if (this._inflate) + this._inflate.destroy(); +}; + +Parser.prototype.start = function() { + this.read(constants.PNG_SIGNATURE.length, + this._parseSignature.bind(this) + ); +}; + +Parser.prototype._parseSignature = function(data) { + + var signature = constants.PNG_SIGNATURE; + + for (var i = 0; i < signature.length; i++) { + if (data[i] != signature[i]) { + this.error(new Error('Invalid file signature')); + return; + } + } + this.read(8, this._parseChunkBegin.bind(this)); +}; + +Parser.prototype._parseChunkBegin = function(data) { + + // chunk content length + var length = data.readUInt32BE(0); + + // chunk type + var type = data.readUInt32BE(4), + name = ''; + for (var i = 4; i < 8; i++) + name += String.fromCharCode(data[i]); + + // console.log('chunk ', name, length); + + // chunk flags + var ancillary = !!(data[4] & 0x20), // or critical + priv = !!(data[5] & 0x20), // or public + safeToCopy = !!(data[7] & 0x20); // or unsafe + + if (!this._hasIHDR && type != constants.TYPE_IHDR) { + this.error(new Error('Expected IHDR on beggining')); + return; + } + +// this._crc = new CrcStream(); +// this._crc.write(new Buffer(name)); + + if (this._chunks[type]) { + return this._chunks[type](length); + + } else if (!ancillary) { + this.error(new Error('Unsupported critical chunk type ' + name)); + return; + } else { + this.read(length + 4, this._skipChunk.bind(this)); + } +}; + +Parser.prototype._skipChunk = function(data) { + this.read(8, this._parseChunkBegin.bind(this)); +}; + +Parser.prototype._handleChunkEnd = function() { + this.read(4, this._parseChunkEnd.bind(this)); +}; + +Parser.prototype._parseChunkEnd = function(data) { + + var fileCrc = data.readInt32BE(0), + calcCrc = this._crc.crc32(); + + // check CRC + if (this._options.checkCRC && calcCrc != fileCrc) { + this.error(new Error('Crc error')); + return; + } + + if (this._hasIEND) { + this.destroySoon(); + + } else { + // todo allow _parseSignature to loop chinks? + this.read(8, this._parseChunkBegin.bind(this)); + } +}; + + +Parser.prototype._handleIHDR = function(length) { + this.read(length, this._parseIHDR.bind(this)); +}; +Parser.prototype._parseIHDR = function(data) { + + this._crc.write(data); + + var width = data.readUInt32BE(0), + height = data.readUInt32BE(4), + depth = data[8], + colorType = data[9], // bits: 1 palette, 2 color, 4 alpha + compr = data[10], + filter = data[11], + interlace = data[12]; + + // console.log(' width', width, 'height', height, + // 'depth', depth, 'colorType', colorType, + // 'compr', compr, 'filter', filter, 'interlace', interlace + // ); + + if (depth != 8) { + this.error(new Error('Unsupported bit depth ' + depth)); + return; + } + if (!(colorType in colorTypeToBppMap)) { + this.error(new Error('Unsupported color type')); + return; + } + if (compr != 0) { + this.error(new Error('Unsupported compression method')); + return; + } + if (filter != 0) { + this.error(new Error('Unsupported filter method')); + return; + } + if (interlace != 0) { + this.error(new Error('Unsupported interlace method')); + return; + } + + this._colorType = colorType; + + this._data = new Buffer(width * height * 4); + this._filter = new Filter( + width, height, + colorTypeToBppMap[this._colorType], + this._data, + this._options + ); + + this._hasIHDR = true; + + this.metadata({ + width: width, + height: height, + palette: !!(colorType & constants.COLOR_PALETTE), + color: !!(colorType & constants.COLOR_COLOR), + alpha: !!(colorType & constants.COLOR_ALPHA), + data: this._data + }); + + this._handleChunkEnd(); +}; + + +Parser.prototype._handlePLTE = function(length) { + this.read(length, this._parsePLTE.bind(this)); +}; +Parser.prototype._parsePLTE = function(data) { + + this._crc.write(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 + ]); + } + + this._handleChunkEnd(); +}; + +Parser.prototype._handleTRNS = function(length) { + this.read(length, this._parseTRNS.bind(this)); +}; +Parser.prototype._parseTRNS = function(data) { + + this._crc.write(data); + + // palette + if (this._colorType == 3) { + if (this._palette.length == 0) { + this.error(new Error('Transparency chunk must be after palette')); + return; + } + if (data.length > this._palette.length) { + this.error(new Error('More transparent colors than palette size')); + return; + } + 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 + + this._handleChunkEnd(); +}; + +Parser.prototype._handleGAMA = function(length) { + this.read(length, this._parseGAMA.bind(this)); +}; +Parser.prototype._parseGAMA = function(data) { + + this._crc.write(data); + this.gamma(data.readUInt32BE(0) / 100000); + + this._handleChunkEnd(); +}; + +Parser.prototype._handleIDAT = function(length) { + this.read(-length, this._parseIDAT.bind(this, length)); +}; +Parser.prototype._parseIDAT = function(length, data) { + + this._crc.write(data); + + if (this._colorType == 3 && this._palette.length == 0) + throw new Error('Expected palette not found'); + + if (!this._inflate) { + this._inflate = zlib.createInflate(); + + this._inflate.on('error', this.emit.bind(this, 'error')); + this._filter.on('complete', this._reverseFiltered.bind(this)); + + this._inflate.pipe(this._filter); + } + + this._inflate.write(data); + length -= data.length; + + if (length > 0) + this._handleIDAT(length); + else + this._handleChunkEnd(); +}; + + +Parser.prototype._handleIEND = function(length) { + this.read(length, this._parseIEND.bind(this)); +}; +Parser.prototype._parseIEND = function(data) { + + this._crc.write(data); + + // no more data to inflate + this._inflate.end(); + + this._hasIEND = true; + this._handleChunkEnd(); +}; + + +var colorTypeToBppMap = { + 0: 1, + 2: 3, + 3: 1, + 4: 2, + 6: 4 +}; + +Parser.prototype._reverseFiltered = function(data, width, height) { + + if (this._colorType == 3) { // paletted + + // use values from palette + var pxLineLength = width << 2; + + for (var y = 0; y < height; y++) { + var pxRowPos = y * pxLineLength; + + for (var x = 0; x < width; x++) { + var pxPos = pxRowPos + (x << 2), + color = this._palette[data[pxPos]]; + + for (var i = 0; i < 4; i++) + data[pxPos + i] = color[i]; + } + } + } + + this.emit('parsed', data); +};