| "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 decodeInterlaceAdam7_1 = require("./helpers/decodeInterlaceAdam7"); |
| 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; |
| _apng; |
| _end; |
| _hasPalette; |
| _palette; |
| _hasTransparency; |
| _transparency; |
| _compressionMethod; |
| _filterMethod; |
| _interlaceMethod; |
| _colorType; |
| _isAnimated; |
| _numberOfFrames; |
| _numberOfPlays; |
| _frames; |
| _writingDataChunks; |
| 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._apng = { |
| width: -1, |
| height: -1, |
| channels: -1, |
| depth: 1, |
| numberOfFrames: 1, |
| numberOfPlays: 0, |
| text: {}, |
| frames: [], |
| }; |
| 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; |
| this._isAnimated = false; |
| this._numberOfFrames = 1; |
| this._numberOfPlays = 0; |
| this._frames = []; |
| this._writingDataChunks = false; |
| |
| |
| this.setBigEndian(); |
| } |
| decode() { |
| (0, signature_1.checkSignature)(this); |
| while (!this._end) { |
| const length = this.readUint32(); |
| const type = this.readChars(4); |
| this.decodeChunk(length, type); |
| } |
| this.decodeImage(); |
| return this._png; |
| } |
| decodeApng() { |
| (0, signature_1.checkSignature)(this); |
| while (!this._end) { |
| const length = this.readUint32(); |
| const type = this.readChars(4); |
| this.decodeApngChunk(length, type); |
| } |
| this.decodeApngImage(); |
| return this._apng; |
| } |
| |
| decodeChunk(length, type) { |
| const offset = this.offset; |
| switch (type) { |
| |
| case 'IHDR': |
| this.decodeIHDR(); |
| break; |
| case 'PLTE': |
| this.decodePLTE(length); |
| break; |
| case 'IDAT': |
| this.decodeIDAT(length); |
| break; |
| case 'IEND': |
| this._end = true; |
| break; |
| |
| case 'tRNS': |
| this.decodetRNS(length); |
| break; |
| case 'iCCP': |
| this.decodeiCCP(length); |
| break; |
| case text_1.textChunkName: |
| (0, text_1.decodetEXt)(this._png.text, this, length); |
| break; |
| case 'pHYs': |
| 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); |
| } |
| } |
| decodeApngChunk(length, type) { |
| const offset = this.offset; |
| if (type !== 'fdAT' && type !== 'IDAT' && this._writingDataChunks) { |
| this.pushDataToFrame(); |
| } |
| switch (type) { |
| case 'acTL': |
| this.decodeACTL(); |
| break; |
| case 'fcTL': |
| this.decodeFCTL(); |
| break; |
| case 'fdAT': |
| this.decodeFDAT(length); |
| break; |
| default: |
| this.decodeChunk(length, type); |
| this.offset = offset + 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); |
| } |
| } |
| |
| 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; |
| |
| |
| 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(); |
| } |
| decodeACTL() { |
| this._numberOfFrames = this.readUint32(); |
| this._numberOfPlays = this.readUint32(); |
| this._isAnimated = true; |
| } |
| decodeFCTL() { |
| const image = { |
| sequenceNumber: this.readUint32(), |
| width: this.readUint32(), |
| height: this.readUint32(), |
| xOffset: this.readUint32(), |
| yOffset: this.readUint32(), |
| delayNumber: this.readUint16(), |
| delayDenominator: this.readUint16(), |
| disposeOp: this.readUint8(), |
| blendOp: this.readUint8(), |
| data: new Uint8Array(0), |
| }; |
| this._frames.push(image); |
| } |
| |
| 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()]); |
| } |
| } |
| |
| decodeIDAT(length) { |
| this._writingDataChunks = true; |
| const dataLength = length; |
| const dataOffset = this.offset + this.byteOffset; |
| this._inflator.push(new Uint8Array(this.buffer, dataOffset, dataLength)); |
| if (this._inflator.err) { |
| throw new Error(`Error while decompressing the data: ${this._inflator.err}`); |
| } |
| this.skip(length); |
| } |
| decodeFDAT(length) { |
| this._writingDataChunks = true; |
| let dataLength = length; |
| let dataOffset = this.offset + this.byteOffset; |
| dataOffset += 4; |
| dataLength -= 4; |
| this._inflator.push(new Uint8Array(this.buffer, dataOffset, dataLength)); |
| if (this._inflator.err) { |
| throw new Error(`Error while decompressing the data: ${this._inflator.err}`); |
| } |
| this.skip(length); |
| } |
| |
| 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; |
| } |
| |
| |
| 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}`); |
| } |
| |
| } |
| } |
| |
| 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), |
| }; |
| } |
| |
| decodepHYs() { |
| const ppuX = this.readUint32(); |
| const ppuY = this.readUint32(); |
| const unitSpecifier = this.readByte(); |
| this._png.resolution = { x: ppuX, y: ppuY, unit: unitSpecifier }; |
| } |
| decodeApngImage() { |
| this._apng.width = this._png.width; |
| this._apng.height = this._png.height; |
| this._apng.channels = this._png.channels; |
| this._apng.depth = this._png.depth; |
| this._apng.numberOfFrames = this._numberOfFrames; |
| this._apng.numberOfPlays = this._numberOfPlays; |
| this._apng.text = this._png.text; |
| this._apng.resolution = this._png.resolution; |
| for (let i = 0; i < this._numberOfFrames; i++) { |
| const newFrame = { |
| sequenceNumber: this._frames[i].sequenceNumber, |
| delayNumber: this._frames[i].delayNumber, |
| delayDenominator: this._frames[i].delayDenominator, |
| data: this._apng.depth === 8 |
| ? new Uint8Array(this._apng.width * this._apng.height * this._apng.channels) |
| : new Uint16Array(this._apng.width * this._apng.height * this._apng.channels), |
| }; |
| const frame = this._frames.at(i); |
| if (frame) { |
| frame.data = (0, decodeInterlaceNull_1.decodeInterlaceNull)({ |
| data: frame.data, |
| width: frame.width, |
| height: frame.height, |
| channels: this._apng.channels, |
| depth: this._apng.depth, |
| }); |
| if (this._hasPalette) { |
| this._apng.palette = this._palette; |
| } |
| if (this._hasTransparency) { |
| this._apng.transparency = this._transparency; |
| } |
| if (i === 0 || |
| (frame.xOffset === 0 && |
| frame.yOffset === 0 && |
| frame.width === this._png.width && |
| frame.height === this._png.height)) { |
| newFrame.data = frame.data; |
| } |
| else { |
| const prevFrame = this._apng.frames.at(i - 1); |
| this.disposeFrame(frame, prevFrame, newFrame); |
| this.addFrameDataToCanvas(newFrame, frame); |
| } |
| this._apng.frames.push(newFrame); |
| } |
| } |
| return this._apng; |
| } |
| disposeFrame(frame, prevFrame, imageFrame) { |
| switch (frame.disposeOp) { |
| case internalTypes_1.DisposeOpType.NONE: |
| break; |
| case internalTypes_1.DisposeOpType.BACKGROUND: |
| for (let row = 0; row < this._png.height; row++) { |
| for (let col = 0; col < this._png.width; col++) { |
| const index = (row * frame.width + col) * this._png.channels; |
| for (let channel = 0; channel < this._png.channels; channel++) { |
| imageFrame.data[index + channel] = 0; |
| } |
| } |
| } |
| break; |
| case internalTypes_1.DisposeOpType.PREVIOUS: |
| imageFrame.data.set(prevFrame.data); |
| break; |
| default: |
| throw new Error('Unknown disposeOp'); |
| } |
| } |
| addFrameDataToCanvas(imageFrame, frame) { |
| const maxValue = 1 << this._png.depth; |
| const calculatePixelIndices = (row, col) => { |
| const index = ((row + frame.yOffset) * this._png.width + frame.xOffset + col) * |
| this._png.channels; |
| const frameIndex = (row * frame.width + col) * this._png.channels; |
| return { index, frameIndex }; |
| }; |
| switch (frame.blendOp) { |
| case internalTypes_1.BlendOpType.SOURCE: |
| for (let row = 0; row < frame.height; row++) { |
| for (let col = 0; col < frame.width; col++) { |
| const { index, frameIndex } = calculatePixelIndices(row, col); |
| for (let channel = 0; channel < this._png.channels; channel++) { |
| imageFrame.data[index + channel] = |
| frame.data[frameIndex + channel]; |
| } |
| } |
| } |
| break; |
| |
| case internalTypes_1.BlendOpType.OVER: |
| for (let row = 0; row < frame.height; row++) { |
| for (let col = 0; col < frame.width; col++) { |
| const { index, frameIndex } = calculatePixelIndices(row, col); |
| for (let channel = 0; channel < this._png.channels; channel++) { |
| const sourceAlpha = frame.data[frameIndex + this._png.channels - 1] / maxValue; |
| const foregroundValue = channel % (this._png.channels - 1) === 0 |
| ? 1 |
| : frame.data[frameIndex + channel]; |
| const value = Math.floor(sourceAlpha * foregroundValue + |
| (1 - sourceAlpha) * imageFrame.data[index + channel]); |
| imageFrame.data[index + channel] += value; |
| } |
| } |
| } |
| break; |
| default: |
| throw new Error('Unknown blendOp'); |
| } |
| } |
| decodeImage() { |
| if (this._inflator.err) { |
| throw new Error(`Error while decompressing the data: ${this._inflator.err}`); |
| } |
| const data = this._isAnimated |
| ? (this._frames?.at(0)).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 if (this._interlaceMethod === internalTypes_1.InterlaceMethod.ADAM7) { |
| this._png.data = (0, decodeInterlaceAdam7_1.decodeInterlaceAdam7)({ |
| 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; |
| } |
| } |
| pushDataToFrame() { |
| const result = this._inflator.result; |
| const lastFrame = this._frames.at(-1); |
| if (lastFrame) { |
| lastFrame.data = result; |
| } |
| else { |
| this._frames.push({ |
| sequenceNumber: 0, |
| width: this._png.width, |
| height: this._png.height, |
| xOffset: 0, |
| yOffset: 0, |
| delayNumber: 0, |
| delayDenominator: 0, |
| disposeOp: internalTypes_1.DisposeOpType.NONE, |
| blendOp: internalTypes_1.BlendOpType.SOURCE, |
| data: result, |
| }); |
| } |
| this._inflator = new pako_1.Inflate(); |
| this._writingDataChunks = false; |
| } |
| } |
| 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; |
| } |
| |