"use strict"; let constants = require("./constants"); let CrcCalculator = require("./crc"); let Parser = (module.exports = function (options, dependencies) { this._options = options; options.checkCRC = options.checkCRC !== false; this._hasIHDR = false; this._hasIEND = false; this._emittedHeadersFinished = 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.transColor = dependencies.transColor; this.palette = dependencies.palette; this.parsed = dependencies.parsed; this.inflateData = dependencies.inflateData; this.finished = dependencies.finished; this.simpleTransparency = dependencies.simpleTransparency; this.headersFinished = dependencies.headersFinished || function () {}; }); Parser.prototype.start = function () { this.read(constants.PNG_SIGNATURE.length, this._parseSignature.bind(this)); }; Parser.prototype._parseSignature = function (data) { let signature = constants.PNG_SIGNATURE; for (let 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 let length = data.readUInt32BE(0); // chunk type let type = data.readUInt32BE(4); let name = ""; for (let i = 4; i < 8; i++) { name += String.fromCharCode(data[i]); } //console.log('chunk ', name, length); // chunk flags let ancillary = Boolean(data[4] & 0x20); // or critical // priv = Boolean(data[5] & 0x20), // or public // safeToCopy = Boolean(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(Buffer.from(name)); if (this._chunks[type]) { return this._chunks[type](length); } if (!ancillary) { this.error(new Error("Unsupported critical chunk type " + name)); return; } 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) { let fileCrc = data.readInt32BE(0); let 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); let width = data.readUInt32BE(0); let height = data.readUInt32BE(4); let depth = data[8]; let colorType = data[9]; // bits: 1 palette, 2 color, 4 alpha let compr = data[10]; let filter = data[11]; let 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 constants.COLORTYPE_TO_BPP_MAP)) { 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; let bpp = constants.COLORTYPE_TO_BPP_MAP[this._colorType]; this._hasIHDR = true; this.metadata({ width: width, height: height, depth: depth, interlace: Boolean(interlace), palette: Boolean(colorType & constants.COLORTYPE_PALETTE), color: Boolean(colorType & constants.COLORTYPE_COLOR), alpha: Boolean(colorType & constants.COLORTYPE_ALPHA), bpp: bpp, colorType: colorType, }); this._handleChunkEnd(); }; Parser.prototype._handlePLTE = function (length) { this.read(length, this._parsePLTE.bind(this)); }; Parser.prototype._parsePLTE = function (data) { this._crc.write(data); let entries = Math.floor(data.length / 3); // console.log('Palette:', entries); for (let i = 0; i < entries; i++) { this._palette.push([data[i * 3], data[i * 3 + 1], data[i * 3 + 2], 0xff]); } this.palette(this._palette); this._handleChunkEnd(); }; Parser.prototype._handleTRNS = function (length) { this.simpleTransparency(); this.read(length, this._parseTRNS.bind(this)); }; Parser.prototype._parseTRNS = function (data) { this._crc.write(data); // palette if (this._colorType === constants.COLORTYPE_PALETTE_COLOR) { 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 (let i = 0; i < data.length; i++) { this._palette[i][3] = data[i]; } this.palette(this._palette); } // for colorType 0 (grayscale) and 2 (rgb) // there might be one gray/color defined as transparent if (this._colorType === constants.COLORTYPE_GRAYSCALE) { // grey, 2 bytes this.transColor([data.readUInt16BE(0)]); } if (this._colorType === constants.COLORTYPE_COLOR) { 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) / constants.GAMMA_DIVISION); this._handleChunkEnd(); }; Parser.prototype._handleIDAT = function (length) { if (!this._emittedHeadersFinished) { this._emittedHeadersFinished = true; this.headersFinished(); } this.read(-length, this._parseIDAT.bind(this, length)); }; Parser.prototype._parseIDAT = function (length, data) { this._crc.write(data); if ( this._colorType === constants.COLORTYPE_PALETTE_COLOR && this._palette.length === 0 ) { throw new Error("Expected palette not found"); } this.inflateData(data); let leftOverLength = length - data.length; if (leftOverLength > 0) { this._handleIDAT(leftOverLength); } 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(); if (this.finished) { this.finished(); } };