import { IOBuffer } from 'iobuffer'; import { deflate } from 'pako'; import { writeCrc } from './helpers/crc'; import { writeSignature } from './helpers/signature'; import { encodetEXt } from './helpers/text'; import { InterlaceMethod, ColorType, CompressionMethod, FilterMethod, } from './internalTypes'; const defaultZlibOptions = { level: 3, }; export default class PngEncoder extends IOBuffer { _png; _zlibOptions; _colorType; _interlaceMethod; constructor(data, options = {}) { super(); this._colorType = ColorType.UNKNOWN; this._zlibOptions = { ...defaultZlibOptions, ...options.zlib }; this._png = this._checkData(data); this._interlaceMethod = (options.interlace === 'Adam7' ? InterlaceMethod.ADAM7 : InterlaceMethod.NO_INTERLACE) ?? InterlaceMethod.NO_INTERLACE; this.setBigEndian(); } encode() { writeSignature(this); this.encodeIHDR(); if (this._png.palette) { this.encodePLTE(); if (this._png.palette[0].length === 4) { this.encodeTRNS(); } } this.encodeData(); if (this._png.text) { for (const [keyword, text] of Object.entries(this._png.text)) { encodetEXt(this, keyword, text); } } this.encodeIEND(); return this.toArray(); } // https://www.w3.org/TR/PNG/#11IHDR encodeIHDR() { this.writeUint32(13); this.writeChars('IHDR'); this.writeUint32(this._png.width); this.writeUint32(this._png.height); this.writeByte(this._png.depth); this.writeByte(this._colorType); this.writeByte(CompressionMethod.DEFLATE); this.writeByte(FilterMethod.ADAPTIVE); this.writeByte(this._interlaceMethod); writeCrc(this, 17); } // https://www.w3.org/TR/PNG/#11IEND encodeIEND() { this.writeUint32(0); this.writeChars('IEND'); writeCrc(this, 4); } encodePLTE() { const paletteLength = this._png.palette?.length * 3; this.writeUint32(paletteLength); this.writeChars('PLTE'); for (const color of this._png.palette) { this.writeByte(color[0]); this.writeByte(color[1]); this.writeByte(color[2]); } writeCrc(this, 4 + paletteLength); } encodeTRNS() { const alpha = this._png.palette.filter((color) => { return color.at(-1) !== 255; }); this.writeUint32(alpha.length); this.writeChars('tRNS'); for (const el of alpha) { this.writeByte(el.at(-1)); } writeCrc(this, 4 + alpha.length); } // https://www.w3.org/TR/PNG/#11IDAT encodeIDAT(data) { this.writeUint32(data.length); this.writeChars('IDAT'); this.writeBytes(data); writeCrc(this, data.length + 4); } encodeData() { const { width, height, channels, depth, data } = this._png; const slotsPerLine = depth <= 8 ? Math.ceil((width * depth) / 8) * channels : Math.ceil((((width * depth) / 8) * channels) / 2); const newData = new IOBuffer().setBigEndian(); let offset = 0; if (this._interlaceMethod === InterlaceMethod.NO_INTERLACE) { for (let i = 0; i < height; i++) { newData.writeByte(0); // no filter if (depth === 16) { offset = writeDataUint16(data, newData, slotsPerLine, offset); } else { offset = writeDataBytes(data, newData, slotsPerLine, offset); } } } else if (this._interlaceMethod === InterlaceMethod.ADAM7) { // Adam7 interlacing offset = writeDataInterlaced(this._png, data, newData, offset); } const buffer = newData.toArray(); const compressed = deflate(buffer, this._zlibOptions); this.encodeIDAT(compressed); } _checkData(data) { const { colorType, channels, depth } = getColorType(data, data.palette); const png = { width: checkInteger(data.width, 'width'), height: checkInteger(data.height, 'height'), channels, data: data.data, depth, text: data.text, palette: data.palette, }; this._colorType = colorType; const expectedSize = depth < 8 ? Math.ceil((png.width * depth) / 8) * png.height * channels : png.width * png.height * channels; if (png.data.length !== expectedSize) { throw new RangeError(`wrong data size. Found ${png.data.length}, expected ${expectedSize}`); } return png; } } function checkInteger(value, name) { if (Number.isInteger(value) && value > 0) { return value; } throw new TypeError(`${name} must be a positive integer`); } function getColorType(data, palette) { const { channels = 4, depth = 8 } = data; if (channels !== 4 && channels !== 3 && channels !== 2 && channels !== 1) { throw new RangeError(`unsupported number of channels: ${channels}`); } const returnValue = { channels, depth, colorType: ColorType.UNKNOWN, }; switch (channels) { case 4: returnValue.colorType = ColorType.TRUECOLOUR_ALPHA; break; case 3: returnValue.colorType = ColorType.TRUECOLOUR; break; case 1: if (palette) { returnValue.colorType = ColorType.INDEXED_COLOUR; } else { returnValue.colorType = ColorType.GREYSCALE; } break; case 2: returnValue.colorType = ColorType.GREYSCALE_ALPHA; break; default: throw new Error('unsupported number of channels'); } return returnValue; } function writeDataBytes(data, newData, slotsPerLine, offset) { for (let j = 0; j < slotsPerLine; j++) { newData.writeByte(data[offset++]); } return offset; } function writeDataInterlaced(imageData, data, newData, offset) { const passes = [ { x: 0, y: 0, xStep: 8, yStep: 8 }, { x: 4, y: 0, xStep: 8, yStep: 8 }, { x: 0, y: 4, xStep: 4, yStep: 8 }, { x: 2, y: 0, xStep: 4, yStep: 4 }, { x: 0, y: 2, xStep: 2, yStep: 4 }, { x: 1, y: 0, xStep: 2, yStep: 2 }, { x: 0, y: 1, xStep: 1, yStep: 2 }, ]; const { width, height, channels, depth } = imageData; let pixelSize = 0; if (depth === 16) { pixelSize = (channels * depth) / 8 / 2; } else { pixelSize = (channels * depth) / 8; } // Process each pass for (let passIndex = 0; passIndex < 7; passIndex++) { const pass = passes[passIndex]; const passWidth = Math.floor((width - pass.x + pass.xStep - 1) / pass.xStep); const passHeight = Math.floor((height - pass.y + pass.yStep - 1) / pass.yStep); if (passWidth <= 0 || passHeight <= 0) continue; const passLineBytes = passWidth * pixelSize; // For each scanline in this pass for (let y = 0; y < passHeight; y++) { const imageY = pass.y + y * pass.yStep; // Extract raw scanline data const rawScanline = depth <= 8 ? new Uint8Array(passLineBytes) : new Uint16Array(passLineBytes); let rawOffset = 0; for (let x = 0; x < passWidth; x++) { const imageX = pass.x + x * pass.xStep; if (imageX < width && imageY < height) { const srcPos = (imageY * width + imageX) * pixelSize; for (let i = 0; i < pixelSize; i++) { rawScanline[rawOffset++] = data[srcPos + i]; } } } newData.writeByte(0); // no filter if (depth === 8) { newData.writeBytes(rawScanline); } else if (depth === 16) { for (const value of rawScanline) { newData.writeByte((value >> 8) & 0xff); // High byte newData.writeByte(value & 0xff); } } } } return offset; } function writeDataUint16(data, newData, slotsPerLine, offset) { for (let j = 0; j < slotsPerLine; j++) { newData.writeUint16(data[offset++]); } return offset; } //# sourceMappingURL=PngEncoder.js.map