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
JavaScript
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