UNPKG

7.72 kBJavaScriptView Raw
1"use strict";
2
3let constants = require("./constants");
4let CrcCalculator = require("./crc");
5
6let Parser = (module.exports = function (options, dependencies) {
7 this._options = options;
8 options.checkCRC = options.checkCRC !== false;
9
10 this._hasIHDR = false;
11 this._hasIEND = false;
12 this._emittedHeadersFinished = false;
13
14 // input flags/metadata
15 this._palette = [];
16 this._colorType = 0;
17
18 this._chunks = {};
19 this._chunks[constants.TYPE_IHDR] = this._handleIHDR.bind(this);
20 this._chunks[constants.TYPE_IEND] = this._handleIEND.bind(this);
21 this._chunks[constants.TYPE_IDAT] = this._handleIDAT.bind(this);
22 this._chunks[constants.TYPE_PLTE] = this._handlePLTE.bind(this);
23 this._chunks[constants.TYPE_tRNS] = this._handleTRNS.bind(this);
24 this._chunks[constants.TYPE_gAMA] = this._handleGAMA.bind(this);
25
26 this.read = dependencies.read;
27 this.error = dependencies.error;
28 this.metadata = dependencies.metadata;
29 this.gamma = dependencies.gamma;
30 this.transColor = dependencies.transColor;
31 this.palette = dependencies.palette;
32 this.parsed = dependencies.parsed;
33 this.inflateData = dependencies.inflateData;
34 this.finished = dependencies.finished;
35 this.simpleTransparency = dependencies.simpleTransparency;
36 this.headersFinished = dependencies.headersFinished || function () {};
37});
38
39Parser.prototype.start = function () {
40 this.read(constants.PNG_SIGNATURE.length, this._parseSignature.bind(this));
41};
42
43Parser.prototype._parseSignature = function (data) {
44 let signature = constants.PNG_SIGNATURE;
45
46 for (let i = 0; i < signature.length; i++) {
47 if (data[i] !== signature[i]) {
48 this.error(new Error("Invalid file signature"));
49 return;
50 }
51 }
52 this.read(8, this._parseChunkBegin.bind(this));
53};
54
55Parser.prototype._parseChunkBegin = function (data) {
56 // chunk content length
57 let length = data.readUInt32BE(0);
58
59 // chunk type
60 let type = data.readUInt32BE(4);
61 let name = "";
62 for (let i = 4; i < 8; i++) {
63 name += String.fromCharCode(data[i]);
64 }
65
66 //console.log('chunk ', name, length);
67
68 // chunk flags
69 let ancillary = Boolean(data[4] & 0x20); // or critical
70 // priv = Boolean(data[5] & 0x20), // or public
71 // safeToCopy = Boolean(data[7] & 0x20); // or unsafe
72
73 if (!this._hasIHDR && type !== constants.TYPE_IHDR) {
74 this.error(new Error("Expected IHDR on beggining"));
75 return;
76 }
77
78 this._crc = new CrcCalculator();
79 this._crc.write(Buffer.from(name));
80
81 if (this._chunks[type]) {
82 return this._chunks[type](length);
83 }
84
85 if (!ancillary) {
86 this.error(new Error("Unsupported critical chunk type " + name));
87 return;
88 }
89
90 this.read(length + 4, this._skipChunk.bind(this));
91};
92
93Parser.prototype._skipChunk = function (/*data*/) {
94 this.read(8, this._parseChunkBegin.bind(this));
95};
96
97Parser.prototype._handleChunkEnd = function () {
98 this.read(4, this._parseChunkEnd.bind(this));
99};
100
101Parser.prototype._parseChunkEnd = function (data) {
102 let fileCrc = data.readInt32BE(0);
103 let calcCrc = this._crc.crc32();
104
105 // check CRC
106 if (this._options.checkCRC && calcCrc !== fileCrc) {
107 this.error(new Error("Crc error - " + fileCrc + " - " + calcCrc));
108 return;
109 }
110
111 if (!this._hasIEND) {
112 this.read(8, this._parseChunkBegin.bind(this));
113 }
114};
115
116Parser.prototype._handleIHDR = function (length) {
117 this.read(length, this._parseIHDR.bind(this));
118};
119Parser.prototype._parseIHDR = function (data) {
120 this._crc.write(data);
121
122 let width = data.readUInt32BE(0);
123 let height = data.readUInt32BE(4);
124 let depth = data[8];
125 let colorType = data[9]; // bits: 1 palette, 2 color, 4 alpha
126 let compr = data[10];
127 let filter = data[11];
128 let interlace = data[12];
129
130 // console.log(' width', width, 'height', height,
131 // 'depth', depth, 'colorType', colorType,
132 // 'compr', compr, 'filter', filter, 'interlace', interlace
133 // );
134
135 if (
136 depth !== 8 &&
137 depth !== 4 &&
138 depth !== 2 &&
139 depth !== 1 &&
140 depth !== 16
141 ) {
142 this.error(new Error("Unsupported bit depth " + depth));
143 return;
144 }
145 if (!(colorType in constants.COLORTYPE_TO_BPP_MAP)) {
146 this.error(new Error("Unsupported color type"));
147 return;
148 }
149 if (compr !== 0) {
150 this.error(new Error("Unsupported compression method"));
151 return;
152 }
153 if (filter !== 0) {
154 this.error(new Error("Unsupported filter method"));
155 return;
156 }
157 if (interlace !== 0 && interlace !== 1) {
158 this.error(new Error("Unsupported interlace method"));
159 return;
160 }
161
162 this._colorType = colorType;
163
164 let bpp = constants.COLORTYPE_TO_BPP_MAP[this._colorType];
165
166 this._hasIHDR = true;
167
168 this.metadata({
169 width: width,
170 height: height,
171 depth: depth,
172 interlace: Boolean(interlace),
173 palette: Boolean(colorType & constants.COLORTYPE_PALETTE),
174 color: Boolean(colorType & constants.COLORTYPE_COLOR),
175 alpha: Boolean(colorType & constants.COLORTYPE_ALPHA),
176 bpp: bpp,
177 colorType: colorType,
178 });
179
180 this._handleChunkEnd();
181};
182
183Parser.prototype._handlePLTE = function (length) {
184 this.read(length, this._parsePLTE.bind(this));
185};
186Parser.prototype._parsePLTE = function (data) {
187 this._crc.write(data);
188
189 let entries = Math.floor(data.length / 3);
190 // console.log('Palette:', entries);
191
192 for (let i = 0; i < entries; i++) {
193 this._palette.push([data[i * 3], data[i * 3 + 1], data[i * 3 + 2], 0xff]);
194 }
195
196 this.palette(this._palette);
197
198 this._handleChunkEnd();
199};
200
201Parser.prototype._handleTRNS = function (length) {
202 this.simpleTransparency();
203 this.read(length, this._parseTRNS.bind(this));
204};
205Parser.prototype._parseTRNS = function (data) {
206 this._crc.write(data);
207
208 // palette
209 if (this._colorType === constants.COLORTYPE_PALETTE_COLOR) {
210 if (this._palette.length === 0) {
211 this.error(new Error("Transparency chunk must be after palette"));
212 return;
213 }
214 if (data.length > this._palette.length) {
215 this.error(new Error("More transparent colors than palette size"));
216 return;
217 }
218 for (let i = 0; i < data.length; i++) {
219 this._palette[i][3] = data[i];
220 }
221 this.palette(this._palette);
222 }
223
224 // for colorType 0 (grayscale) and 2 (rgb)
225 // there might be one gray/color defined as transparent
226 if (this._colorType === constants.COLORTYPE_GRAYSCALE) {
227 // grey, 2 bytes
228 this.transColor([data.readUInt16BE(0)]);
229 }
230 if (this._colorType === constants.COLORTYPE_COLOR) {
231 this.transColor([
232 data.readUInt16BE(0),
233 data.readUInt16BE(2),
234 data.readUInt16BE(4),
235 ]);
236 }
237
238 this._handleChunkEnd();
239};
240
241Parser.prototype._handleGAMA = function (length) {
242 this.read(length, this._parseGAMA.bind(this));
243};
244Parser.prototype._parseGAMA = function (data) {
245 this._crc.write(data);
246 this.gamma(data.readUInt32BE(0) / constants.GAMMA_DIVISION);
247
248 this._handleChunkEnd();
249};
250
251Parser.prototype._handleIDAT = function (length) {
252 if (!this._emittedHeadersFinished) {
253 this._emittedHeadersFinished = true;
254 this.headersFinished();
255 }
256 this.read(-length, this._parseIDAT.bind(this, length));
257};
258Parser.prototype._parseIDAT = function (length, data) {
259 this._crc.write(data);
260
261 if (
262 this._colorType === constants.COLORTYPE_PALETTE_COLOR &&
263 this._palette.length === 0
264 ) {
265 throw new Error("Expected palette not found");
266 }
267
268 this.inflateData(data);
269 let leftOverLength = length - data.length;
270
271 if (leftOverLength > 0) {
272 this._handleIDAT(leftOverLength);
273 } else {
274 this._handleChunkEnd();
275 }
276};
277
278Parser.prototype._handleIEND = function (length) {
279 this.read(length, this._parseIEND.bind(this));
280};
281Parser.prototype._parseIEND = function (data) {
282 this._crc.write(data);
283
284 this._hasIEND = true;
285 this._handleChunkEnd();
286
287 if (this.finished) {
288 this.finished();
289 }
290};