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)
795 lines • 32.5 kB
JavaScript
import { inflate } from '../packer/packer.js';
import { Crc32 } from '../common/crc32.js';
import { InputBuffer } from '../common/input-buffer.js';
import { ArrayUtils } from '../common/array-utils.js';
import { StringUtils } from '../common/string-utils.js';
import { LibError } from '../error/lib-error.js';
import { PngFrame } from './png/png-frame.js';
import { PngInfo } from './png/png-info.js';
import { PngColorType } from './png/png-color-type.js';
import { PngDisposeMode } from './png/png-dispose-mode.js';
import { PngBlendMode } from './png/png-blend-mode.js';
import { ColorRgba8 } from '../color/color-rgba8.js';
import { ColorRgb8 } from '../color/color-rgb8.js';
import { MemoryImage } from '../image/image.js';
import { PaletteUint8 } from '../image/palette-uint8.js';
import { Format } from '../color/format.js';
import { IccProfile } from '../image/icc-profile.js';
import { IccProfileCompression } from '../image/icc-profile-compression.js';
import { Draw } from '../draw/draw.js';
import { BlendMode } from '../draw/blend-mode.js';
import { PngFilterType } from './png/png-filter-type.js';
import { ImageFormat } from './image-format.js';
import { Rectangle } from '../common/rectangle.js';
import { PngPhysicalPixelDimensions } from './png/png-physical-pixel-dimensions.js';
export class PngDecoder {
constructor() {
this._info = new PngInfo();
this._progressY = 0;
this._bitBuffer = 0;
this._bitBufferLen = 0;
}
get input() {
return this._input;
}
get info() {
return this._info;
}
get progressY() {
return this._progressY;
}
get bitBuffer() {
return this._bitBuffer;
}
get bitBufferLen() {
return this._bitBufferLen;
}
get format() {
return ImageFormat.png;
}
get numFrames() {
return this._info.numFrames;
}
static unfilter(filterType, bpp, row, prevRow) {
const rowBytes = row.length;
switch (filterType) {
case PngFilterType.none:
break;
case PngFilterType.sub:
for (let x = bpp; x < rowBytes; ++x) {
row[x] = (row[x] + row[x - bpp]) & 0xff;
}
break;
case PngFilterType.up:
for (let x = 0; x < rowBytes; ++x) {
const b = prevRow !== undefined ? prevRow[x] : 0;
row[x] = (row[x] + b) & 0xff;
}
break;
case PngFilterType.average:
for (let x = 0; x < rowBytes; ++x) {
const a = x < bpp ? 0 : row[x - bpp];
const b = prevRow !== undefined ? prevRow[x] : 0;
row[x] = (row[x] + ((a + b) >>> 1)) & 0xff;
}
break;
case PngFilterType.paeth:
for (let x = 0; x < rowBytes; ++x) {
const a = x < bpp ? 0 : row[x - bpp];
const b = prevRow !== undefined ? prevRow[x] : 0;
const c = x < bpp || prevRow === undefined ? 0 : prevRow[x - bpp];
const p = a + b - c;
const pa = Math.abs(p - a);
const pb = Math.abs(p - b);
const pc = Math.abs(p - c);
let paeth = 0;
if (pa <= pb && pa <= pc) {
paeth = a;
}
else if (pb <= pc) {
paeth = b;
}
else {
paeth = c;
}
row[x] = (row[x] + paeth) & 0xff;
}
break;
default:
throw new LibError(`Invalid filter value: ${filterType}`);
}
}
static crc(type, bytes) {
const typeCodeUnits = StringUtils.getCodePoints(type);
const crc = Crc32.getChecksum({
buffer: typeCodeUnits,
});
return Crc32.getChecksum({
buffer: bytes,
baseCrc: crc,
});
}
processPass(input, image, xOffset, yOffset, xStep, yStep, passWidth, passHeight) {
let channels = 1;
if (this._info.colorType === PngColorType.grayscaleAlpha) {
channels = 2;
}
else if (this._info.colorType === PngColorType.rgb) {
channels = 3;
}
else if (this._info.colorType === PngColorType.rgba) {
channels = 4;
}
const pixelDepth = channels * this._info.bits;
const bpp = (pixelDepth + 7) >>> 3;
const rowBytes = (pixelDepth * passWidth + 7) >>> 3;
const inData = [undefined, undefined];
const pixel = [0, 0, 0, 0];
for (let srcY = 0, dstY = yOffset, ri = 0; srcY < passHeight; ++srcY, dstY += yStep, ri = 1 - ri, this._progressY++) {
const filterType = input.read();
inData[ri] = input.readRange(rowBytes).toUint8Array();
const row = inData[ri];
const prevRow = inData[1 - ri];
PngDecoder.unfilter(filterType, bpp, row, prevRow);
this.resetBits();
const rowInput = new InputBuffer({
buffer: row,
bigEndian: true,
});
const blockHeight = xStep;
const blockWidth = xStep - xOffset;
for (let srcX = 0, dstX = xOffset; srcX < passWidth; ++srcX, dstX += xStep) {
this.readPixel(rowInput, pixel);
this.setPixel(image.getPixel(dstX, dstY), pixel);
if (blockWidth > 1 || blockHeight > 1) {
for (let i = 0; i < blockHeight; ++i) {
for (let j = 0; j < blockWidth; ++j) {
this.setPixel(image.getPixelSafe(dstX + j, dstY + i), pixel);
}
}
}
}
}
}
process(input, image) {
let channels = 1;
if (this._info.colorType === PngColorType.grayscaleAlpha) {
channels = 2;
}
else if (this._info.colorType === PngColorType.rgb) {
channels = 3;
}
else if (this._info.colorType === PngColorType.rgba) {
channels = 4;
}
const pixelDepth = channels * this._info.bits;
const w = this._info.width;
const h = this._info.height;
const rowBytes = (w * pixelDepth + 7) >>> 3;
const bpp = (pixelDepth + 7) >>> 3;
const line = new Uint8Array(rowBytes);
const inData = [line, line];
const pixel = [0, 0, 0, 0];
const pIter = image[Symbol.iterator]();
let pIterRes = pIter.next();
for (let y = 0, ri = 0; y < h; ++y, ri = 1 - ri) {
const filterType = input.read();
inData[ri] = input.readRange(rowBytes).toUint8Array();
const row = inData[ri];
const prevRow = inData[1 - ri];
PngDecoder.unfilter(filterType, bpp, row, prevRow);
this.resetBits();
const rowInput = new InputBuffer({
buffer: inData[ri],
bigEndian: true,
});
for (let x = 0; x < w; ++x) {
this.readPixel(rowInput, pixel);
this.setPixel(pIterRes.value, pixel);
pIterRes = pIter.next();
}
}
}
resetBits() {
this._bitBuffer = 0;
this._bitBufferLen = 0;
}
readBits(input, numBits) {
if (numBits === 0) {
return 0;
}
if (numBits === 8) {
return input.read();
}
if (numBits === 16) {
return input.readUint16();
}
while (this._bitBufferLen < numBits) {
if (input.isEOS) {
throw new LibError('Invalid PNG data.');
}
const octet = input.read();
this._bitBuffer = octet << this._bitBufferLen;
this._bitBufferLen += 8;
}
let mask = 0;
switch (numBits) {
case 1:
mask = 1;
break;
case 2:
mask = 3;
break;
case 4:
mask = 0xf;
break;
case 8:
mask = 0xff;
break;
case 16:
mask = 0xffff;
break;
default:
mask = 0;
break;
}
const octet = (this._bitBuffer >>> (this._bitBufferLen - numBits)) & mask;
this._bitBufferLen -= numBits;
return octet;
}
readPixel(input, pixel) {
switch (this._info.colorType) {
case PngColorType.grayscale:
pixel[0] = this.readBits(input, this._info.bits);
return;
case PngColorType.rgb:
pixel[0] = this.readBits(input, this._info.bits);
pixel[1] = this.readBits(input, this._info.bits);
pixel[2] = this.readBits(input, this._info.bits);
return;
case PngColorType.indexed:
pixel[0] = this.readBits(input, this._info.bits);
return;
case PngColorType.grayscaleAlpha:
pixel[0] = this.readBits(input, this._info.bits);
pixel[1] = this.readBits(input, this._info.bits);
return;
case PngColorType.rgba:
pixel[0] = this.readBits(input, this._info.bits);
pixel[1] = this.readBits(input, this._info.bits);
pixel[2] = this.readBits(input, this._info.bits);
pixel[3] = this.readBits(input, this._info.bits);
return;
}
throw new LibError(`Invalid color type: ${this._info.colorType}.`);
}
setPixel(p, raw) {
switch (this._info.colorType) {
case PngColorType.grayscale:
if (this._info.transparency !== undefined && this._info.bits > 8) {
const t = this._info.transparency;
const a = ((t[0] & 0xff) << 24) | (t[1] & 0xff);
const g = raw[0];
p.setRgba(g, g, g, g !== a ? p.maxChannelValue : 0);
return;
}
p.setRgb(raw[0], 0, 0);
return;
case PngColorType.rgb:
{
const r = raw[0];
const g = raw[1];
const b = raw[2];
if (this._info.transparency !== undefined) {
const t = this._info.transparency;
const tr = ((t[0] & 0xff) << 8) | (t[1] & 0xff);
const tg = ((t[2] & 0xff) << 8) | (t[3] & 0xff);
const tb = ((t[4] & 0xff) << 8) | (t[5] & 0xff);
if (raw[0] !== tr || raw[1] !== tg || raw[2] !== tb) {
p.setRgba(r, g, b, p.maxChannelValue);
return;
}
}
p.setRgb(r, g, b);
}
return;
case PngColorType.indexed:
p.index = raw[0];
return;
case PngColorType.grayscaleAlpha:
p.setRgb(raw[0], raw[1], 0);
return;
case PngColorType.rgba:
p.setRgba(raw[0], raw[1], raw[2], raw[3]);
return;
}
throw new LibError(`Invalid color type: ${this._info.colorType}.`);
}
isValidFile(bytes) {
this._input = new InputBuffer({
buffer: bytes,
bigEndian: true,
});
const headerBytes = this._input.readRange(8);
const expectedHeaderBytes = [137, 80, 78, 71, 13, 10, 26, 10];
for (let i = 0; i < 8; ++i) {
if (headerBytes.get(i) !== expectedHeaderBytes[i]) {
return false;
}
}
return true;
}
startDecode(bytes) {
if (!this.isValidFile(bytes) || this._input === undefined) {
return undefined;
}
while (true) {
const inputPos = this._input.position;
let chunkSize = this._input.readUint32();
const chunkType = this._input.readString(4);
switch (chunkType) {
case 'tEXt':
{
const txtData = this._input.readRange(chunkSize).toUint8Array();
const l = txtData.length;
for (let i = 0; i < l; ++i) {
if (txtData[i] === 0) {
const key = StringUtils.latin1Decoder.decode(ArrayUtils.copyUint8(txtData, 0, i));
const text = StringUtils.latin1Decoder.decode(ArrayUtils.copyUint8(txtData, i + 1));
this._info.textData.set(key, text);
break;
}
}
this._input.skip(4);
}
break;
case 'pHYs': {
const physData = InputBuffer.from(this._input.readRange(chunkSize));
const x = physData.readUint32();
const y = physData.readUint32();
const unit = physData.read();
this._info.pixelDimensions = new PngPhysicalPixelDimensions(x, y, unit);
this._input.skip(4);
break;
}
case 'IHDR': {
const hdr = InputBuffer.from(this._input.readRange(chunkSize));
const hdrBytes = hdr.toUint8Array();
this._info.width = hdr.readUint32();
this._info.height = hdr.readUint32();
this._info.bits = hdr.read();
this._info.colorType = hdr.read();
this._info.compressionMethod = hdr.read();
this._info.filterMethod = hdr.read();
this._info.interlaceMethod = hdr.read();
if (this._info.filterMethod !== 0) {
return undefined;
}
switch (this._info.colorType) {
case PngColorType.grayscale:
if (![1, 2, 4, 8, 16].includes(this._info.bits)) {
return undefined;
}
break;
case PngColorType.rgb:
if (![8, 16].includes(this._info.bits)) {
return undefined;
}
break;
case PngColorType.indexed:
if (![1, 2, 4, 8].includes(this._info.bits)) {
return undefined;
}
break;
case PngColorType.grayscaleAlpha:
if (![8, 16].includes(this._info.bits)) {
return undefined;
}
break;
case PngColorType.rgba:
if (![8, 16].includes(this._info.bits)) {
return undefined;
}
break;
default:
return undefined;
}
const crc = this._input.readUint32();
const computedCrc = PngDecoder.crc(chunkType, hdrBytes);
if (crc !== computedCrc) {
throw new LibError(`Invalid ${chunkType} checksum`);
}
break;
}
case 'PLTE': {
this._info.palette = this._input.readRange(chunkSize).toUint8Array();
const crc = this._input.readUint32();
const computedCrc = PngDecoder.crc(chunkType, this._info.palette);
if (crc !== computedCrc) {
throw new LibError(`Invalid ${chunkType} checksum`);
}
break;
}
case 'tRNS': {
this._info.transparency = this._input
.readRange(chunkSize)
.toUint8Array();
const crc = this._input.readUint32();
const computedCrc = PngDecoder.crc(chunkType, this._info.transparency);
if (crc !== computedCrc) {
throw new LibError(`Invalid ${chunkType} checksum`);
}
break;
}
case 'IEND': {
this._input.skip(4);
break;
}
case 'gAMA': {
if (chunkSize !== 4) {
throw new LibError('Invalid gAMA chunk');
}
const gammaInt = this._input.readUint32();
this._input.skip(4);
if (gammaInt !== 100000) {
this._info.gamma = gammaInt / 100000.0;
}
break;
}
case 'IDAT': {
this._info.idat.push(inputPos);
this._input.skip(chunkSize);
this._input.skip(4);
break;
}
case 'acTL': {
this._info.numFrames = this._input.readUint32();
this._info.repeat = this._input.readUint32();
this._input.skip(4);
break;
}
case 'fcTL': {
const sequenceNumber = this._input.readUint32();
const width = this._input.readUint32();
const height = this._input.readUint32();
const xOffset = this._input.readUint32();
const yOffset = this._input.readUint32();
const delayNum = this._input.readUint16();
const delayDen = this._input.readUint16();
const dispose = this._input.read();
const blend = this._input.read();
this._input.skip(4);
const frame = new PngFrame({
sequenceNumber: sequenceNumber,
width: width,
height: height,
xOffset: xOffset,
yOffset: yOffset,
delayNum: delayNum,
delayDen: delayDen,
dispose: dispose,
blend: blend,
});
this._info.frames.push(frame);
break;
}
case 'fdAT': {
const sequenceNumber = this._input.readUint32();
const frame = this._info.frames[this._info.frames.length - 1];
frame.fdat.push(inputPos);
this._input.skip(chunkSize - 4);
this._input.skip(4);
break;
}
case 'bKGD': {
if (this._info.colorType === PngColorType.indexed) {
const paletteIndex = this._input.read();
chunkSize--;
const p3 = paletteIndex * 3;
const r = this._info.palette[p3];
const g = this._info.palette[p3 + 1];
const b = this._info.palette[p3 + 2];
if (this._info.transparency !== undefined) {
const isTransparent = this._info.transparency.includes(paletteIndex);
this._info.backgroundColor = new ColorRgba8(r, g, b, isTransparent ? 0 : 255);
}
else {
this._info.backgroundColor = new ColorRgb8(r, g, b);
}
}
else if (this._info.colorType === PngColorType.grayscale ||
this._info.colorType === PngColorType.grayscaleAlpha) {
this._input.readUint16();
chunkSize -= 2;
}
else if (this._info.colorType === PngColorType.rgb ||
this._info.colorType === PngColorType.rgba) {
this._input.readUint16();
this._input.readUint16();
this._input.readUint16();
chunkSize -= 24;
}
if (chunkSize > 0) {
this._input.skip(chunkSize);
}
this._input.skip(4);
break;
}
case 'iCCP': {
this._info.iccpName = this._input.readString();
this._info.iccpCompression = this._input.read();
chunkSize -= this._info.iccpName.length + 2;
const profile = this._input.readRange(chunkSize);
this._info.iccpData = profile.toUint8Array();
this._input.skip(4);
break;
}
default: {
this._input.skip(chunkSize);
this._input.skip(4);
break;
}
}
if (chunkType === 'IEND') {
break;
}
if (this._input.isEOS) {
return undefined;
}
}
return this._info;
}
decodeFrame(frameIndex) {
if (this._input === undefined) {
return undefined;
}
let imageData = undefined;
let width = this._info.width;
let height = this._info.height;
if (!this._info.isAnimated || frameIndex === 0) {
let totalSize = 0;
const len = this._info.idat.length;
const dataBlocks = new Array();
for (let i = 0; i < len; ++i) {
this._input.offset = this._info.idat[i];
const chunkSize = this._input.readUint32();
const chunkType = this._input.readString(4);
const data = this._input.readRange(chunkSize).toUint8Array();
totalSize += data.length;
dataBlocks.push(data);
const crc = this._input.readUint32();
const computedCrc = PngDecoder.crc(chunkType, data);
if (crc !== computedCrc) {
throw new LibError(`Invalid ${chunkType} checksum`);
}
}
imageData = new Uint8Array(totalSize);
let offset = 0;
for (const data of dataBlocks) {
imageData.set(data, offset);
offset += data.length;
}
}
else {
if (frameIndex < 0 || frameIndex >= this._info.frames.length) {
throw new LibError(`Invalid Frame Number: ${frameIndex}`);
}
const f = this._info.frames[frameIndex];
width = f.width;
height = f.height;
let totalSize = 0;
const dataBlocks = new Array();
for (let i = 0; i < f.fdat.length; ++i) {
this._input.offset = f.fdat[i];
const chunkSize = this._input.readUint32();
this._input.readString(4);
this._input.skip(4);
const data = this._input.readRange(chunkSize - 4).toUint8Array();
totalSize += data.length;
dataBlocks.push(data);
}
imageData = new Uint8Array(totalSize);
let offset = 0;
for (const data of dataBlocks) {
imageData.set(data, offset);
offset += data.length;
}
}
let numChannels = this._info.colorType === PngColorType.indexed
? 1
: this._info.colorType === PngColorType.grayscale
? 1
: this._info.colorType === PngColorType.grayscaleAlpha
? 2
: this._info.colorType === PngColorType.rgba
? 4
: 3;
let uncompressed = undefined;
try {
uncompressed = inflate(imageData);
}
catch (error) {
console.error(error);
return undefined;
}
const input = new InputBuffer({
buffer: uncompressed,
bigEndian: true,
});
this.resetBits();
let palette = undefined;
if (this._info.colorType === PngColorType.indexed) {
if (this._info.palette !== undefined) {
const p = this._info.palette;
const numColors = Math.trunc(p.length / 3);
const t = this._info.transparency;
const tl = t !== undefined ? t.length : 0;
const nc = t !== undefined ? 4 : 3;
palette = new PaletteUint8(numColors, nc);
for (let i = 0, pi = 0; i < numColors; ++i, pi += 3) {
let a = 255;
if (nc === 4 && i < tl) {
a = t[i];
}
palette.setRgba(i, p[pi], p[pi + 1], p[pi + 2], a);
}
}
}
if (this._info.colorType === PngColorType.grayscale &&
this._info.transparency !== undefined &&
palette === undefined &&
this._info.bits <= 8) {
const t = this._info.transparency;
const nt = t.length;
const numColors = 1 << this._info.bits;
palette = new PaletteUint8(numColors, 4);
const to8bit = this._info.bits === 1
? 255
: this._info.bits === 2
? 85
: this._info.bits === 4
? 17
: 1;
for (let i = 0; i < numColors; ++i) {
const g = i * to8bit;
palette.setRgba(i, g, g, g, 255);
}
for (let i = 0; i < nt; i += 2) {
const ti = ((t[i] & 0xff) << 8) | (t[i + 1] & 0xff);
if (ti < numColors) {
palette.set(ti, 3, 0);
}
}
}
const format = this._info.bits === 1
? Format.uint1
: this._info.bits === 2
? Format.uint2
: this._info.bits === 4
? Format.uint4
: this._info.bits === 16
? Format.uint16
: Format.uint8;
if (this._info.colorType === PngColorType.grayscale &&
this._info.transparency !== undefined &&
this._info.bits > 8) {
numChannels = 4;
}
if (this._info.colorType === PngColorType.rgb &&
this._info.transparency !== undefined) {
numChannels = 4;
}
const opt = {
width: width,
height: height,
numChannels: numChannels,
palette: palette,
format: format,
};
if (this._info.iccpData !== undefined) {
opt.iccProfile = new IccProfile(this._info.iccpName, IccProfileCompression.deflate, this._info.iccpData);
}
if (this._info.textData.size > 0) {
opt.textData = new Map(this._info.textData);
}
const image = new MemoryImage(opt);
const origW = this._info.width;
const origH = this._info.height;
this._info.width = width;
this._info.height = height;
const w = width;
const h = height;
this._progressY = 0;
if (this._info.interlaceMethod !== 0) {
this.processPass(input, image, 0, 0, 8, 8, (w + 7) >>> 3, (h + 7) >>> 3);
this.processPass(input, image, 4, 0, 8, 8, (w + 3) >>> 3, (h + 7) >>> 3);
this.processPass(input, image, 0, 4, 4, 8, (w + 3) >>> 2, (h + 3) >>> 3);
this.processPass(input, image, 2, 0, 4, 4, (w + 1) >>> 2, (h + 3) >>> 2);
this.processPass(input, image, 0, 2, 2, 4, (w + 1) >>> 1, (h + 1) >>> 2);
this.processPass(input, image, 1, 0, 2, 2, w >>> 1, (h + 1) >>> 1);
this.processPass(input, image, 0, 1, 1, 2, w, h >>> 1);
}
else {
this.process(input, image);
}
this._info.width = origW;
this._info.height = origH;
return image;
}
decode(opt) {
var _a, _b;
const bytes = opt.bytes;
if (this.startDecode(bytes) === undefined) {
return undefined;
}
if (!this._info.isAnimated || opt.frameIndex !== undefined) {
return this.decodeFrame((_a = opt.frameIndex) !== null && _a !== void 0 ? _a : 0);
}
let firstImage = undefined;
let lastImage = undefined;
for (let i = 0; i < this._info.numFrames; ++i) {
const frame = this._info.frames[i];
const image = this.decodeFrame(i);
if (image === undefined) {
continue;
}
if (firstImage === undefined || lastImage === undefined) {
firstImage = image.convert({
numChannels: image.numChannels,
});
lastImage = firstImage;
lastImage.frameDuration = Math.trunc(frame.delay * 1000);
continue;
}
const prevFrame = this._info.frames[i - 1];
if (image.width === lastImage.width &&
image.height === lastImage.height &&
frame.xOffset === 0 &&
frame.yOffset === 0 &&
frame.blend === PngBlendMode.source) {
lastImage = image;
lastImage.frameDuration = Math.trunc(frame.delay * 1000);
firstImage.addFrame(lastImage);
continue;
}
lastImage = MemoryImage.from(firstImage.getFrame(i - 1));
const dispose = prevFrame.dispose;
if (dispose === PngDisposeMode.background) {
Draw.fillRect({
image: lastImage,
rect: new Rectangle(prevFrame.xOffset, prevFrame.yOffset, prevFrame.xOffset + prevFrame.width - 1, prevFrame.yOffset + prevFrame.height - 1),
color: (_b = this._info.backgroundColor) !== null && _b !== void 0 ? _b : new ColorRgba8(0, 0, 0, 0),
alphaBlend: false,
});
}
else if (dispose === PngDisposeMode.previous && i > 1) {
const prevImage = firstImage.getFrame(i - 2);
lastImage = Draw.compositeImage({
dst: lastImage,
src: prevImage,
dstX: prevFrame.xOffset,
dstY: prevFrame.yOffset,
dstW: prevFrame.width,
dstH: prevFrame.height,
srcX: prevFrame.xOffset,
srcY: prevFrame.yOffset,
srcW: prevFrame.width,
srcH: prevFrame.height,
});
}
lastImage.frameDuration = Math.trunc(frame.delay * 1000);
lastImage = Draw.compositeImage({
dst: lastImage,
src: image,
dstX: frame.xOffset,
dstY: frame.yOffset,
blend: frame.blend === PngBlendMode.over
? BlendMode.alpha
: BlendMode.direct,
});
firstImage.addFrame(lastImage);
}
return firstImage;
}
}
//# sourceMappingURL=png-decoder.js.map