fast-png
Version:
PNG image decoder and encoder written entirely in JavaScript
262 lines • 9.89 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const iobuffer_1 = require("iobuffer");
const pako_1 = require("pako");
const crc_1 = require("./helpers/crc");
const decodeInterlaceNull_1 = require("./helpers/decodeInterlaceNull");
const signature_1 = require("./helpers/signature");
const text_1 = require("./helpers/text");
const internalTypes_1 = require("./internalTypes");
class PngDecoder extends iobuffer_1.IOBuffer {
_checkCrc;
_inflator;
_png;
_end;
_hasPalette;
_palette;
_hasTransparency;
_transparency;
_compressionMethod;
_filterMethod;
_interlaceMethod;
_colorType;
constructor(data, options = {}) {
super(data);
const { checkCrc = false } = options;
this._checkCrc = checkCrc;
this._inflator = new pako_1.Inflate();
this._png = {
width: -1,
height: -1,
channels: -1,
data: new Uint8Array(0),
depth: 1,
text: {},
};
this._end = false;
this._hasPalette = false;
this._palette = [];
this._hasTransparency = false;
this._transparency = new Uint16Array(0);
this._compressionMethod = internalTypes_1.CompressionMethod.UNKNOWN;
this._filterMethod = internalTypes_1.FilterMethod.UNKNOWN;
this._interlaceMethod = internalTypes_1.InterlaceMethod.UNKNOWN;
this._colorType = internalTypes_1.ColorType.UNKNOWN;
// PNG is always big endian
// https://www.w3.org/TR/PNG/#7Integers-and-byte-order
this.setBigEndian();
}
decode() {
(0, signature_1.checkSignature)(this);
while (!this._end) {
this.decodeChunk();
}
this.decodeImage();
return this._png;
}
// https://www.w3.org/TR/PNG/#5Chunk-layout
decodeChunk() {
const length = this.readUint32();
const type = this.readChars(4);
const offset = this.offset;
switch (type) {
// 11.2 Critical chunks
case 'IHDR': // 11.2.2 IHDR Image header
this.decodeIHDR();
break;
case 'PLTE': // 11.2.3 PLTE Palette
this.decodePLTE(length);
break;
case 'IDAT': // 11.2.4 IDAT Image data
this.decodeIDAT(length);
break;
case 'IEND': // 11.2.5 IEND Image trailer
this._end = true;
break;
// 11.3 Ancillary chunks
case 'tRNS': // 11.3.2.1 tRNS Transparency
this.decodetRNS(length);
break;
case 'iCCP': // 11.3.3.3 iCCP Embedded ICC profile
this.decodeiCCP(length);
break;
case text_1.textChunkName: // 11.3.4.3 tEXt Textual data
(0, text_1.decodetEXt)(this._png.text, this, length);
break;
case 'pHYs': // 11.3.5.3 pHYs Physical pixel dimensions
this.decodepHYs();
break;
default:
this.skip(length);
break;
}
if (this.offset - offset !== length) {
throw new Error(`Length mismatch while decoding chunk ${type}`);
}
if (this._checkCrc) {
(0, crc_1.checkCrc)(this, length + 4, type);
}
else {
this.skip(4);
}
}
// https://www.w3.org/TR/PNG/#11IHDR
decodeIHDR() {
const image = this._png;
image.width = this.readUint32();
image.height = this.readUint32();
image.depth = checkBitDepth(this.readUint8());
const colorType = this.readUint8();
this._colorType = colorType;
let channels;
switch (colorType) {
case internalTypes_1.ColorType.GREYSCALE:
channels = 1;
break;
case internalTypes_1.ColorType.TRUECOLOUR:
channels = 3;
break;
case internalTypes_1.ColorType.INDEXED_COLOUR:
channels = 1;
break;
case internalTypes_1.ColorType.GREYSCALE_ALPHA:
channels = 2;
break;
case internalTypes_1.ColorType.TRUECOLOUR_ALPHA:
channels = 4;
break;
// Kept for exhaustiveness.
// eslint-disable-next-line unicorn/no-useless-switch-case
case internalTypes_1.ColorType.UNKNOWN:
default:
throw new Error(`Unknown color type: ${colorType}`);
}
this._png.channels = channels;
this._compressionMethod = this.readUint8();
if (this._compressionMethod !== internalTypes_1.CompressionMethod.DEFLATE) {
throw new Error(`Unsupported compression method: ${this._compressionMethod}`);
}
this._filterMethod = this.readUint8();
this._interlaceMethod = this.readUint8();
}
// https://www.w3.org/TR/PNG/#11PLTE
decodePLTE(length) {
if (length % 3 !== 0) {
throw new RangeError(`PLTE field length must be a multiple of 3. Got ${length}`);
}
const l = length / 3;
this._hasPalette = true;
const palette = [];
this._palette = palette;
for (let i = 0; i < l; i++) {
palette.push([this.readUint8(), this.readUint8(), this.readUint8()]);
}
}
// https://www.w3.org/TR/PNG/#11IDAT
decodeIDAT(length) {
this._inflator.push(new Uint8Array(this.buffer, this.offset + this.byteOffset, length));
this.skip(length);
}
// https://www.w3.org/TR/PNG/#11tRNS
decodetRNS(length) {
switch (this._colorType) {
case internalTypes_1.ColorType.GREYSCALE:
case internalTypes_1.ColorType.TRUECOLOUR: {
if (length % 2 !== 0) {
throw new RangeError(`tRNS chunk length must be a multiple of 2. Got ${length}`);
}
if (length / 2 > this._png.width * this._png.height) {
throw new Error(`tRNS chunk contains more alpha values than there are pixels (${length / 2} vs ${this._png.width * this._png.height})`);
}
this._hasTransparency = true;
this._transparency = new Uint16Array(length / 2);
for (let i = 0; i < length / 2; i++) {
this._transparency[i] = this.readUint16();
}
break;
}
case internalTypes_1.ColorType.INDEXED_COLOUR: {
if (length > this._palette.length) {
throw new Error(`tRNS chunk contains more alpha values than there are palette colors (${length} vs ${this._palette.length})`);
}
let i = 0;
for (; i < length; i++) {
const alpha = this.readByte();
this._palette[i].push(alpha);
}
for (; i < this._palette.length; i++) {
this._palette[i].push(255);
}
break;
}
// Kept for exhaustiveness.
/* eslint-disable unicorn/no-useless-switch-case */
case internalTypes_1.ColorType.UNKNOWN:
case internalTypes_1.ColorType.GREYSCALE_ALPHA:
case internalTypes_1.ColorType.TRUECOLOUR_ALPHA:
default: {
throw new Error(`tRNS chunk is not supported for color type ${this._colorType}`);
}
/* eslint-enable unicorn/no-useless-switch-case */
}
}
// https://www.w3.org/TR/PNG/#11iCCP
decodeiCCP(length) {
const name = (0, text_1.readKeyword)(this);
const compressionMethod = this.readUint8();
if (compressionMethod !== internalTypes_1.CompressionMethod.DEFLATE) {
throw new Error(`Unsupported iCCP compression method: ${compressionMethod}`);
}
const compressedProfile = this.readBytes(length - name.length - 2);
this._png.iccEmbeddedProfile = {
name,
profile: (0, pako_1.inflate)(compressedProfile),
};
}
// https://www.w3.org/TR/PNG/#11pHYs
decodepHYs() {
const ppuX = this.readUint32();
const ppuY = this.readUint32();
const unitSpecifier = this.readByte();
this._png.resolution = { x: ppuX, y: ppuY, unit: unitSpecifier };
}
decodeImage() {
if (this._inflator.err) {
throw new Error(`Error while decompressing the data: ${this._inflator.err}`);
}
const data = this._inflator.result;
if (this._filterMethod !== internalTypes_1.FilterMethod.ADAPTIVE) {
throw new Error(`Filter method ${this._filterMethod} not supported`);
}
if (this._interlaceMethod === internalTypes_1.InterlaceMethod.NO_INTERLACE) {
this._png.data = (0, decodeInterlaceNull_1.decodeInterlaceNull)({
data: data,
width: this._png.width,
height: this._png.height,
channels: this._png.channels,
depth: this._png.depth,
});
}
else {
throw new Error(`Interlace method ${this._interlaceMethod} not supported`);
}
if (this._hasPalette) {
this._png.palette = this._palette;
}
if (this._hasTransparency) {
this._png.transparency = this._transparency;
}
}
}
exports.default = PngDecoder;
function checkBitDepth(value) {
if (value !== 1 &&
value !== 2 &&
value !== 4 &&
value !== 8 &&
value !== 16) {
throw new Error(`invalid bit depth: ${value}`);
}
return value;
}
//# sourceMappingURL=PngDecoder.js.map