parser.js 7.5 KB

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