3 var constants = require('./constants');
4 var CrcCalculator = require('./crc');
7 var Parser = module.exports = function(options, dependencies) {
9 this._options = options;
10 options.checkCRC = options.checkCRC !== false;
12 this._hasIHDR = false;
13 this._hasIEND = false;
14 this._emittedHeadersFinished = false;
16 // input flags/metadata
21 this._chunks[constants.TYPE_IHDR] = this._handleIHDR.bind(this);
22 this._chunks[constants.TYPE_IEND] = this._handleIEND.bind(this);
23 this._chunks[constants.TYPE_IDAT] = this._handleIDAT.bind(this);
24 this._chunks[constants.TYPE_PLTE] = this._handlePLTE.bind(this);
25 this._chunks[constants.TYPE_tRNS] = this._handleTRNS.bind(this);
26 this._chunks[constants.TYPE_gAMA] = this._handleGAMA.bind(this);
28 this.read = dependencies.read;
29 this.error = dependencies.error;
30 this.metadata = dependencies.metadata;
31 this.gamma = dependencies.gamma;
32 this.transColor = dependencies.transColor;
33 this.palette = dependencies.palette;
34 this.parsed = dependencies.parsed;
35 this.inflateData = dependencies.inflateData;
36 this.finished = dependencies.finished;
37 this.simpleTransparency = dependencies.simpleTransparency;
38 this.headersFinished = dependencies.headersFinished || function() {};
41 Parser.prototype.start = function() {
42 this.read(constants.PNG_SIGNATURE.length,
43 this._parseSignature.bind(this)
47 Parser.prototype._parseSignature = function(data) {
49 var signature = constants.PNG_SIGNATURE;
51 for (var i = 0; i < signature.length; i++) {
52 if (data[i] !== signature[i]) {
53 this.error(new Error('Invalid file signature'));
57 this.read(8, this._parseChunkBegin.bind(this));
60 Parser.prototype._parseChunkBegin = function(data) {
62 // chunk content length
63 var length = data.readUInt32BE(0);
66 var type = data.readUInt32BE(4);
68 for (var i = 4; i < 8; i++) {
69 name += String.fromCharCode(data[i]);
72 //console.log('chunk ', name, length);
75 var ancillary = Boolean(data[4] & 0x20); // or critical
76 // priv = Boolean(data[5] & 0x20), // or public
77 // safeToCopy = Boolean(data[7] & 0x20); // or unsafe
79 if (!this._hasIHDR && type !== constants.TYPE_IHDR) {
80 this.error(new Error('Expected IHDR on beggining'));
84 this._crc = new CrcCalculator();
85 this._crc.write(new Buffer(name));
87 if (this._chunks[type]) {
88 return this._chunks[type](length);
92 this.error(new Error('Unsupported critical chunk type ' + name));
96 this.read(length + 4, this._skipChunk.bind(this));
99 Parser.prototype._skipChunk = function(/*data*/) {
100 this.read(8, this._parseChunkBegin.bind(this));
103 Parser.prototype._handleChunkEnd = function() {
104 this.read(4, this._parseChunkEnd.bind(this));
107 Parser.prototype._parseChunkEnd = function(data) {
109 var fileCrc = data.readInt32BE(0);
110 var calcCrc = this._crc.crc32();
113 if (this._options.checkCRC && calcCrc !== fileCrc) {
114 this.error(new Error('Crc error - ' + fileCrc + ' - ' + calcCrc));
118 if (!this._hasIEND) {
119 this.read(8, this._parseChunkBegin.bind(this));
123 Parser.prototype._handleIHDR = function(length) {
124 this.read(length, this._parseIHDR.bind(this));
126 Parser.prototype._parseIHDR = function(data) {
128 this._crc.write(data);
130 var width = data.readUInt32BE(0);
131 var height = data.readUInt32BE(4);
133 var colorType = data[9]; // bits: 1 palette, 2 color, 4 alpha
134 var compr = data[10];
135 var filter = data[11];
136 var interlace = data[12];
138 // console.log(' width', width, 'height', height,
139 // 'depth', depth, 'colorType', colorType,
140 // 'compr', compr, 'filter', filter, 'interlace', interlace
143 if (depth !== 8 && depth !== 4 && depth !== 2 && depth !== 1 && depth !== 16) {
144 this.error(new Error('Unsupported bit depth ' + depth));
147 if (!(colorType in constants.COLORTYPE_TO_BPP_MAP)) {
148 this.error(new Error('Unsupported color type'));
152 this.error(new Error('Unsupported compression method'));
156 this.error(new Error('Unsupported filter method'));
159 if (interlace !== 0 && interlace !== 1) {
160 this.error(new Error('Unsupported interlace method'));
164 this._colorType = colorType;
166 var bpp = constants.COLORTYPE_TO_BPP_MAP[this._colorType];
168 this._hasIHDR = true;
174 interlace: Boolean(interlace),
175 palette: Boolean(colorType & constants.COLORTYPE_PALETTE),
176 color: Boolean(colorType & constants.COLORTYPE_COLOR),
177 alpha: Boolean(colorType & constants.COLORTYPE_ALPHA),
182 this._handleChunkEnd();
186 Parser.prototype._handlePLTE = function(length) {
187 this.read(length, this._parsePLTE.bind(this));
189 Parser.prototype._parsePLTE = function(data) {
191 this._crc.write(data);
193 var entries = Math.floor(data.length / 3);
194 // console.log('Palette:', entries);
196 for (var i = 0; i < entries; i++) {
205 this.palette(this._palette);
207 this._handleChunkEnd();
210 Parser.prototype._handleTRNS = function(length) {
211 this.simpleTransparency();
212 this.read(length, this._parseTRNS.bind(this));
214 Parser.prototype._parseTRNS = function(data) {
216 this._crc.write(data);
219 if (this._colorType === constants.COLORTYPE_PALETTE_COLOR) {
220 if (this._palette.length === 0) {
221 this.error(new Error('Transparency chunk must be after palette'));
224 if (data.length > this._palette.length) {
225 this.error(new Error('More transparent colors than palette size'));
228 for (var i = 0; i < data.length; i++) {
229 this._palette[i][3] = data[i];
231 this.palette(this._palette);
234 // for colorType 0 (grayscale) and 2 (rgb)
235 // there might be one gray/color defined as transparent
236 if (this._colorType === constants.COLORTYPE_GRAYSCALE) {
238 this.transColor([data.readUInt16BE(0)]);
240 if (this._colorType === constants.COLORTYPE_COLOR) {
241 this.transColor([data.readUInt16BE(0), data.readUInt16BE(2), data.readUInt16BE(4)]);
244 this._handleChunkEnd();
247 Parser.prototype._handleGAMA = function(length) {
248 this.read(length, this._parseGAMA.bind(this));
250 Parser.prototype._parseGAMA = function(data) {
252 this._crc.write(data);
253 this.gamma(data.readUInt32BE(0) / constants.GAMMA_DIVISION);
255 this._handleChunkEnd();
258 Parser.prototype._handleIDAT = function(length) {
259 if (!this._emittedHeadersFinished) {
260 this._emittedHeadersFinished = true;
261 this.headersFinished();
263 this.read(-length, this._parseIDAT.bind(this, length));
265 Parser.prototype._parseIDAT = function(length, data) {
267 this._crc.write(data);
269 if (this._colorType === constants.COLORTYPE_PALETTE_COLOR && this._palette.length === 0) {
270 throw new Error('Expected palette not found');
273 this.inflateData(data);
274 var leftOverLength = length - data.length;
276 if (leftOverLength > 0) {
277 this._handleIDAT(leftOverLength);
280 this._handleChunkEnd();
284 Parser.prototype._handleIEND = function(length) {
285 this.read(length, this._parseIEND.bind(this));
287 Parser.prototype._parseIEND = function(data) {
289 this._crc.write(data);
291 this._hasIEND = true;
292 this._handleChunkEnd();