png-async
Version:
A simple and non-blocking PNG encoder / decoder.
253 lines • 8.64 kB
JavaScript
"use strict";
const zlib = require("zlib");
const constants = require("./constants");
const CrcStream = require("./crc");
const ChunkStream = require("./chunk-stream");
const Filter = require("./filter");
const colorTypeToBppMap = {
0: 1,
2: 3,
3: 1,
4: 2,
6: 4
};
class Parser extends ChunkStream {
constructor(option) {
super();
this._option = option;
option.checkCRC = option.checkCRC !== false;
this._hasIHDR = false;
this._hasIEND = false;
this._inflate = null;
this._filter = null;
this._crc = null;
// 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.writable = true;
this.on("error", this._handleError.bind(this));
this._handleSignature();
}
_handleError() {
this.writable = false;
this.destroy();
}
_handleSignature() {
this.read(constants.PNG_SIGNATURE.length, this._parseSignature.bind(this));
}
_parseSignature(data) {
const signature = constants.PNG_SIGNATURE;
for (let i = 0; i < signature.length; i++) {
if (data[i] !== signature[i]) {
this.emit("error", new Error("Invalid file signature"));
return;
}
}
this.read(8, this._parseChunkBegin.bind(this));
}
_parseChunkBegin(data) {
// chunk content length
const length = data.readUInt32BE(0);
// chunk type
const 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
const ancillary = !!(data[4] & 0x20); // or critical
//const priv = !!(data[5] & 0x20); // or public
//const safeToCopy = !!(data[7] & 0x20); // or unsafe
if (!this._hasIHDR && type !== constants.TYPE_IHDR) {
this.emit("error", new Error("Expected IHDR on beginning"));
return;
}
this._crc = new CrcStream();
this._crc.write(Buffer.from(name));
if (this._chunks[type]) {
return this._chunks[type](length);
}
else if (!ancillary) {
this.emit("error", new Error("Unsupported critical chunk type " + name));
return;
}
else {
this.read(length + 4, this._skipChunk.bind(this));
}
}
_skipChunk(data) {
this.read(8, this._parseChunkBegin.bind(this));
}
_handleChunkEnd() {
this.read(4, this._parseChunkEnd.bind(this));
}
_parseChunkEnd(data) {
const fileCrc = data.readInt32BE(0);
const calcCrc = this._crc.crc32;
// check CRC
if (this._option.checkCRC && calcCrc !== fileCrc) {
this.emit("error", new Error("Crc error"));
return;
}
if (this._hasIEND) {
this.destroySoon();
}
else {
this.read(8, this._parseChunkBegin.bind(this));
}
}
_handleIHDR(length) {
this.read(length, this._parseIHDR.bind(this));
}
_parseIHDR(data) {
this._crc.write(data);
const width = data.readUInt32BE(0);
const height = data.readUInt32BE(4);
const depth = data[8];
const colorType = data[9]; // bits: 1 palette, 2 color, 4 alpha
const compr = data[10];
const filter = data[11];
const interlace = data[12];
if (depth !== 8) {
this.emit("error", new Error("Unsupported bit depth " + depth));
return;
}
if (!(colorType in colorTypeToBppMap)) {
this.emit("error", new Error("Unsupported color type"));
return;
}
if (compr !== 0) {
this.emit("error", new Error("Unsupported compression method"));
return;
}
if (filter !== 0) {
this.emit("error", new Error("Unsupported filter method"));
return;
}
if (interlace !== 0) {
this.emit("error", new Error("Unsupported interlace method"));
return;
}
this._colorType = colorType;
this._data = Buffer.alloc(width * height * 4);
this._filter = new Filter(width, height, colorTypeToBppMap[this._colorType], this._data, this._option);
this._hasIHDR = true;
this.emit("metadata", {
width: width,
height: height,
palette: !!(colorType & constants.COLOR_PALETTE),
color: !!(colorType & constants.COLOR_COLOR),
alpha: !!(colorType & constants.COLOR_ALPHA),
data: this._data
});
this._handleChunkEnd();
}
_handlePLTE(length) {
this.read(length, this._parsePLTE.bind(this));
}
_parsePLTE(data) {
this._crc.write(data);
const entries = Math.floor(data.length / 3);
for (let i = 0; i < entries; i++) {
this._palette.push([
data.readUInt8(i * 3),
data.readUInt8(i * 3 + 1),
data.readUInt8(i * 3 + 2),
0xff
]);
}
this._handleChunkEnd();
}
_handleTRNS(length) {
this.read(length, this._parseTRNS.bind(this));
}
_parseTRNS(data) {
this._crc.write(data);
// palette
if (this._colorType === 3) {
if (this._palette.length === 0) {
this.emit("error", new Error("Transparency chunk must be after palette"));
return;
}
if (data.length > this._palette.length) {
this.emit("error", new Error("More transparent colors than palette size"));
return;
}
for (let i = 0; i < this._palette.length; i++) {
this._palette[i][3] = i < data.length ? data.readUInt8(i) : 0xff;
}
}
// for colorType 0 (grayscale) and 2 (rgb)
// there might be one gray/color defined as transparent
this._handleChunkEnd();
}
_handleGAMA(length) {
this.read(length, this._parseGAMA.bind(this));
}
_parseGAMA(data) {
this._crc.write(data);
this.emit("gamma", data.readUInt32BE(0) / 100000);
this._handleChunkEnd();
}
_handleIDAT(length) {
this.read(-length, this._parseIDAT.bind(this, length));
}
_parseIDAT(length, data) {
this._crc.write(data);
if (this._colorType === 3 && this._palette.length === 0) {
throw new Error("Expected palette not found");
}
if (!this._inflate) {
this._inflate = zlib.createInflate();
this._inflate.on("error", this.emit.bind(this, "error"));
this._filter.on("complete", this._reverseFiltered.bind(this));
this._inflate.pipe(this._filter);
}
this._inflate.write(data);
length -= data.length;
if (length > 0) {
this._handleIDAT(length);
}
else {
this._handleChunkEnd();
}
}
_handleIEND(length) {
this.read(length, this._parseIEND.bind(this));
}
_parseIEND(data) {
this._crc.write(data);
// no more data to inflate
this._inflate.end();
this._hasIEND = true;
this._handleChunkEnd();
}
_reverseFiltered(data, width, height) {
if (this._colorType === 3) { // paletted
let i, y, x, pxRowPos, pxPos, color;
// use values from palette
const pxLineLength = width << 2;
for (y = 0; y < height; y++) {
pxRowPos = y * pxLineLength;
for (x = 0; x < width; x++) {
pxPos = pxRowPos + (x << 2),
color = this._palette[data[pxPos]];
for (i = 0; i < 4; i++) {
data[pxPos + i] = color[i];
}
}
}
}
this.emit("parsed", data);
}
}
module.exports = Parser;
//# sourceMappingURL=parser.js.map