UNPKG

image-in-browser

Version:

Package for encoding / decoding images, transforming images, applying filters, drawing primitives on images on the client side (no need for server Node.js)

396 lines 14.5 kB
import { deflate } from '../packer/packer.js'; import { Crc32 } from '../common/crc32.js'; import { OutputBuffer } from '../common/output-buffer.js'; import { StringUtils } from '../common/string-utils.js'; import { PngFilterType } from './png/png-filter-type.js'; import { Format } from '../color/format.js'; import { NeuralQuantizer } from '../image/neural-quantizer.js'; import { PngColorType } from './png/png-color-type.js'; export class PngEncoder { get supportsAnimation() { return this._supportsAnimation; } get pixelDimensions() { return this._pixelDimensions; } constructor(opt) { var _a, _b; this._repeat = 0; this._frames = 0; this._sequenceNumber = 0; this._isAnimated = false; this._supportsAnimation = true; this._filter = (_a = opt === null || opt === void 0 ? void 0 : opt.filter) !== null && _a !== void 0 ? _a : PngFilterType.paeth; this._level = (_b = opt === null || opt === void 0 ? void 0 : opt.level) !== null && _b !== void 0 ? _b : 6; this._pixelDimensions = opt === null || opt === void 0 ? void 0 : opt.pixelDimensions; } static crc(type, bytes) { const typeCodeUnits = StringUtils.getCodePoints(type); const crc = Crc32.getChecksum({ buffer: typeCodeUnits, }); return Crc32.getChecksum({ buffer: bytes, baseCrc: crc, }); } static writeChunk(out, type, chunk) { out.writeUint32(chunk.length); const typeCodeUnits = StringUtils.getCodePoints(type); out.writeBytes(typeCodeUnits); out.writeBytes(chunk); const crc = PngEncoder.crc(type, chunk); out.writeUint32(crc); } static write(bpc, row, ri, out, oi) { let _bpc = bpc; let _oi = oi; _bpc--; while (_bpc >= 0) { out[_oi++] = row[ri + _bpc]; _bpc--; } return _oi; } static filterSub(row, bpc, bpp, out, oi) { let _oi = oi; out[_oi++] = PngFilterType.sub; for (let x = 0; x < bpp; x += bpc) { _oi = PngEncoder.write(bpc, row, x, out, _oi); } const l = row.length; for (let x = bpp; x < l; x += bpc) { for (let c = 0, c2 = bpc - 1; c < bpc; ++c, --c2) { out[_oi++] = (row[x + c2] - row[x + c2 - bpp]) & 0xff; } } return _oi; } static filterUp(row, bpc, out, oi, prevRow) { let _oi = oi; out[_oi++] = PngFilterType.up; const l = row.length; for (let x = 0; x < l; x += bpc) { for (let c = 0, c2 = bpc - 1; c < bpc; ++c, --c2) { const b = prevRow !== undefined ? prevRow[x + c2] : 0; out[_oi++] = (row[x + c2] - b) & 0xff; } } return _oi; } static filterAverage(row, bpc, bpp, out, oi, prevRow) { let _oi = oi; out[_oi++] = PngFilterType.average; const l = row.length; for (let x = 0; x < l; x += bpc) { for (let c = 0, c2 = bpc - 1; c < bpc; ++c, --c2) { const x2 = x + c2; const p1 = x2 < bpp ? 0 : row[x2 - bpp]; const p2 = prevRow === undefined ? 0 : prevRow[x2]; const p3 = row[x2]; out[_oi++] = p3 - ((p1 + p2) >>> 1); } } return _oi; } static paethPredictor(a, b, c) { const p = a + b - c; const pa = p > a ? p - a : a - p; const pb = p > b ? p - b : b - p; const pc = p > c ? p - c : c - p; if (pa <= pb && pa <= pc) { return a; } else if (pb <= pc) { return b; } return c; } static filterPaeth(row, bpc, bpp, out, oi, prevRow) { let _oi = oi; out[_oi++] = PngFilterType.paeth; const l = row.length; for (let x = 0; x < l; x += bpc) { for (let c = 0, c2 = bpc - 1; c < bpc; ++c, --c2) { const x2 = x + c2; const p0 = x2 < bpp ? 0 : row[x2 - bpp]; const p1 = prevRow === undefined ? 0 : prevRow[x2]; const p2 = x2 < bpp || prevRow === undefined ? 0 : prevRow[x2 - bpp]; const p = row[x2]; const pi = PngEncoder.paethPredictor(p0, p1, p2); out[_oi++] = (p - pi) & 0xff; } } return _oi; } static filterNone(rowBytes, bpc, out, oi) { let _oi = oi; out[_oi++] = PngFilterType.none; if (bpc === 1) { const l = rowBytes.length; for (let i = 0; i < l; ++i) { out[_oi++] = rowBytes[i]; } } else { const l = rowBytes.length; for (let i = 0; i < l; i += bpc) { _oi = PngEncoder.write(bpc, rowBytes, i, out, _oi); } } return _oi; } static numChannels(image) { return image.hasPalette ? 1 : image.numChannels; } writeHeader(image) { this._output.writeBytes(new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])); const chunk = new OutputBuffer({ bigEndian: true, }); chunk.writeUint32(image.width); chunk.writeUint32(image.height); chunk.writeByte(image.bitsPerChannel); chunk.writeByte(image.hasPalette ? PngColorType.indexed : image.numChannels === 1 ? PngColorType.grayscale : image.numChannels === 2 ? PngColorType.grayscaleAlpha : image.numChannels === 3 ? PngColorType.rgb : PngColorType.rgba); chunk.writeByte(0); chunk.writeByte(0); chunk.writeByte(0); PngEncoder.writeChunk(this._output, 'IHDR', chunk.getBytes()); } writeICCPChunk(iccp) { const chunk = new OutputBuffer({ bigEndian: true, }); const nameCodeUnits = StringUtils.getCodePoints(iccp.name); chunk.writeBytes(nameCodeUnits); chunk.writeByte(0); chunk.writeByte(0); chunk.writeBytes(iccp.compressed()); PngEncoder.writeChunk(this._output, 'iCCP', chunk.getBytes()); } writeAnimationControlChunk() { const chunk = new OutputBuffer({ bigEndian: true, }); chunk.writeUint32(this._frames); chunk.writeUint32(this._repeat); PngEncoder.writeChunk(this._output, 'acTL', chunk.getBytes()); } writeFrameControlChunk(image) { const chunk = new OutputBuffer({ bigEndian: true, }); chunk.writeUint32(this._sequenceNumber); chunk.writeUint32(image.width); chunk.writeUint32(image.height); chunk.writeUint32(0); chunk.writeUint32(0); chunk.writeUint16(image.frameDuration); chunk.writeUint16(1000); chunk.writeByte(1); chunk.writeByte(0); PngEncoder.writeChunk(this._output, 'fcTL', chunk.getBytes()); } writePalette(palette) { if (palette.format === Format.uint8 && palette.numChannels === 3 && palette.numColors === 256) { PngEncoder.writeChunk(this._output, 'PLTE', palette.toUint8Array()); } else { const chunk = new OutputBuffer({ size: palette.numColors * 3, bigEndian: true, }); const nc = palette.numColors; for (let i = 0; i < nc; ++i) { chunk.writeByte(Math.trunc(palette.getRed(i))); chunk.writeByte(Math.trunc(palette.getGreen(i))); chunk.writeByte(Math.trunc(palette.getBlue(i))); } PngEncoder.writeChunk(this._output, 'PLTE', chunk.getBytes()); } if (palette.numChannels === 4) { const chunk = new OutputBuffer({ size: palette.numColors, bigEndian: true, }); const nc = palette.numColors; for (let i = 0; i < nc; ++i) { const a = Math.trunc(palette.getAlpha(i)); chunk.writeByte(a); } PngEncoder.writeChunk(this._output, 'tRNS', chunk.getBytes()); } } writeTextChunk(keyword, text) { const chunk = new OutputBuffer({ bigEndian: true, }); const keywordBytes = StringUtils.getCodePoints(keyword); const textBytes = StringUtils.getCodePoints(text); chunk.writeBytes(keywordBytes); chunk.writeByte(0); chunk.writeBytes(textBytes); PngEncoder.writeChunk(this._output, 'tEXt', chunk.getBytes()); } filter(image, out) { let oi = 0; const filter = image.hasPalette ? PngFilterType.none : this._filter; const buffer = image.buffer; const rowStride = image.data.rowStride; const nc = PngEncoder.numChannels(image); const bpp = (nc * image.bitsPerChannel + 7) >>> 3; const bpc = (image.bitsPerChannel + 7) >>> 3; let rowOffset = 0; let prevRow = undefined; for (let y = 0; y < image.height; ++y) { const rowBytes = buffer !== undefined ? new Uint8Array(buffer, rowOffset, rowStride) : new Uint8Array(); rowOffset += rowStride; switch (filter) { case PngFilterType.sub: oi = PngEncoder.filterSub(rowBytes, bpc, bpp, out, oi); break; case PngFilterType.up: oi = PngEncoder.filterUp(rowBytes, bpc, out, oi, prevRow); break; case PngFilterType.average: oi = PngEncoder.filterAverage(rowBytes, bpc, bpp, out, oi, prevRow); break; case PngFilterType.paeth: oi = PngEncoder.filterPaeth(rowBytes, bpc, bpp, out, oi, prevRow); break; default: oi = PngEncoder.filterNone(rowBytes, bpc, out, oi); break; } prevRow = rowBytes; } } addFrame(image) { let _image = image; if ((_image.isHdrFormat && _image.format !== Format.uint16) || (_image.bitsPerChannel < 8 && !_image.hasPalette && _image.numChannels > 1)) { _image = _image.convert({ format: Format.uint8, }); } if (this._output === undefined) { this._output = new OutputBuffer({ bigEndian: true, }); this.writeHeader(_image); if (_image.iccProfile !== undefined) { this.writeICCPChunk(_image.iccProfile); } if (_image.hasPalette) { if (this._globalQuantizer !== undefined) { this.writePalette(this._globalQuantizer.palette); } else { this.writePalette(_image.palette); } } if (this._isAnimated) { this.writeAnimationControlChunk(); } } const nc = PngEncoder.numChannels(_image); const channelBytes = _image.format === Format.uint16 ? 2 : 1; const filteredImage = new Uint8Array(_image.width * _image.height * nc * channelBytes + image.height); this.filter(_image, filteredImage); const compressed = deflate(filteredImage, { level: this._level, }); if (_image.textData !== undefined) { for (const [key, value] of _image.textData) { this.writeTextChunk(key, value); } } if (this._pixelDimensions !== undefined) { const phys = new OutputBuffer({ bigEndian: true, }); phys.writeUint32(this._pixelDimensions.xPxPerUnit); phys.writeUint32(this._pixelDimensions.yPxPerUnit); phys.writeByte(this._pixelDimensions.unitSpecifier); PngEncoder.writeChunk(this._output, 'pHYs', phys.getBytes()); } if (this._isAnimated) { this.writeFrameControlChunk(_image); this._sequenceNumber++; } if (this._sequenceNumber <= 1) { PngEncoder.writeChunk(this._output, 'IDAT', compressed); } else { const fdat = new OutputBuffer({ bigEndian: true, }); fdat.writeUint32(this._sequenceNumber); fdat.writeBytes(compressed); PngEncoder.writeChunk(this._output, 'fdAT', fdat.getBytes()); this._sequenceNumber++; } } start(frameCount) { this._frames = frameCount; this._isAnimated = frameCount > 1; } finish() { let bytes = undefined; if (this._output === undefined) { return bytes; } PngEncoder.writeChunk(this._output, 'IEND', new Uint8Array()); this._sequenceNumber = 0; bytes = this._output.getBytes(); this._output = undefined; return bytes; } encode(opt) { var _a; const image = opt.image; const singleFrame = (_a = opt.singleFrame) !== null && _a !== void 0 ? _a : false; if (!image.hasAnimation || singleFrame) { this.start(1); this.addFrame(image); } else { this.start(image.frames.length); this._repeat = image.loopCount; if (image.hasPalette) { const q = new NeuralQuantizer(image); this._globalQuantizer = q; for (const frame of image.frames) { if (frame !== image) { q.addImage(frame); } } } for (const frame of image.frames) { if (this._globalQuantizer !== undefined) { const newImage = this._globalQuantizer.getIndexImage(frame); this.addFrame(newImage); } else { this.addFrame(frame); } } } return this.finish(); } } //# sourceMappingURL=png-encoder.js.map