// 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 constants = require('./constants'), CrcCalculator = require('./crc'); var Parser = module.exports = function(options, dependencies) { this._options = options; options.checkCRC = options.checkCRC !== false; this._hasIHDR = false; this._hasIEND = false; // 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.read = dependencies.read; this.error = dependencies.error; this.metadata = dependencies.metadata; this.gamma = dependencies.gamma; this.parsed = dependencies.parsed; this.createData = dependencies.createData; this.inflateData = dependencies.inflateData; this.finished = dependencies.finished; }; var colorTypeToBppMap = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 }; 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 CrcCalculator(); 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 - ' + fileCrc + " - " + calcCrc)); return; } if (!this._hasIEND) { 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 && depth !== 4 && depth !== 2 && depth !== 1 && depth !== 16) { 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 && interlace !== 1) { this.error(new Error('Unsupported interlace method')); return; } this._colorType = colorType; var bpp = colorTypeToBppMap[this._colorType]; this.createData(width, height, bpp, depth, interlace); this._hasIHDR = true; this.metadata({ width: width, height: height, depth: depth, interlace: interlace, palette: !!(colorType & constants.COLOR_PALETTE), color: !!(colorType & constants.COLOR_COLOR), alpha: !!(colorType & constants.COLOR_ALPHA) }); 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 if (this._colorType === 0) { // grey, 2 bytes this._transColor = [data.readUInt16BE(0)]; } if (this._colorType === 2) { this._transColor = [data.readUInt16BE(0), data.readUInt16BE(2), data.readUInt16BE(4)]; } 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'); this.inflateData(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); this._hasIEND = true; this._handleChunkEnd(); this.finished(); }; Parser.prototype.reverseFiltered = function(indata, depth, width, height) { var outdata = indata; // only different for 16 bits if (this._colorType == 3) { // paletted //console.log("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[indata[pxPos]]; if (!color) { throw new Error("index " + indata[pxPos] + " not in palette"); } for (var i = 0; i < 4; i++) indata[pxPos + i] = color[i]; } } } else { var pxLineLength = width << 2; if (this._transColor) { for (var y = 0; y < height; y++) { var pxRowPos = y * pxLineLength; for (var x = 0; x < width; x++) { var pxPos = pxRowPos + (x << 2); var makeTrans = false; //console.log(pxPos); if (this._transColor.length === 1) { if (this._transColor[0] === indata[pxPos]) { makeTrans = true; } } else if (this._transColor[0] === indata[pxPos] && this._transColor[1] === indata[pxPos + 1] && this._transColor[2] === indata[pxPos + 2]) { makeTrans = true; } if (makeTrans) { for (var i = 0; i < 4; i++) { indata[pxPos + i] = 0; } } } } } if (depth !== 8) { if (depth === 16) { outdata = new Buffer(width * height * 4); } //console.log("adjusting"); 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++) outdata[pxPos + i] = Math.floor((indata[pxPos + i] * maxOutSample) / maxInSample + 0.5); } } } } return outdata; };