parser.js 7.5 KB

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