291 lines
7.5 KiB
JavaScript
291 lines
7.5 KiB
JavaScript
"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();
|
|
}
|
|
};
|