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)
287 lines • 10 kB
JavaScript
import { ColorRgba8 } from '../color/color-rgba8.js';
import { InputBuffer } from '../common/input-buffer.js';
import { Draw } from '../draw/draw.js';
import { LibError } from '../error/lib-error.js';
import { MemoryImage } from '../image/image.js';
import { ImageFormat } from './image-format.js';
import { VP8 } from './webp/vp8.js';
import { VP8L } from './webp/vp8l.js';
import { WebPFormat } from './webp/webp-format.js';
import { WebPFrame } from './webp/webp-frame.js';
import { WebPInfoInternal } from './webp/webp-info-internal.js';
export class WebPDecoder {
get info() {
return this._info;
}
get format() {
return ImageFormat.webp;
}
get numFrames() {
return this._info !== undefined ? this._info.numFrames : 0;
}
constructor(bytes) {
if (bytes !== undefined) {
this.startDecode(bytes);
}
}
decodeFrameInternal(input, frameIndex = 0) {
const webp = new WebPInfoInternal();
if (!this.getInfo(input, webp)) {
return undefined;
}
if (webp.format === WebPFormat.undefined) {
return undefined;
}
webp.frame = frameIndex;
webp.frameCount = this._info.numFrames;
if (webp.hasAnimation) {
if (frameIndex >= webp.frames.length || frameIndex < 0) {
return undefined;
}
const f = webp.frames[frameIndex];
const frameData = input.subarray(f.frameSize, f.framePosition);
return this.decodeFrameInternal(frameData, frameIndex);
}
else {
const data = input.subarray(webp.vp8Size, webp.vp8Position);
if (webp.format === WebPFormat.lossless) {
return new VP8L(data, webp).decode();
}
else if (webp.format === WebPFormat.lossy) {
return new VP8(data, webp).decode();
}
}
return undefined;
}
getHeader(input) {
let tag = input.readString(4);
if (tag !== 'RIFF') {
return false;
}
input.readUint32();
tag = input.readString(4);
if (tag !== 'WEBP') {
return false;
}
return true;
}
getInfo(input, webp) {
while (!input.isEOS) {
const tag = input.readString(4);
const size = input.readUint32();
const diskSize = ((size + 1) >>> 1) << 1;
const p = input.position;
switch (tag) {
case 'VP8X':
if (!this.getVp8xInfo(input, webp)) {
return false;
}
break;
case 'VP8 ':
webp.vp8Position = input.position;
webp.vp8Size = size;
webp.format = WebPFormat.lossy;
break;
case 'VP8L':
webp.vp8Position = input.position;
webp.vp8Size = size;
webp.format = WebPFormat.lossless;
break;
case 'ALPH':
webp.alphaData = new InputBuffer({
buffer: input.buffer,
bigEndian: input.bigEndian,
});
webp.alphaData.offset = input.offset;
webp.alphaSize = size;
input.skip(diskSize);
break;
case 'ANIM':
webp.format = WebPFormat.animated;
if (!this.getAnimInfo(input, webp)) {
return false;
}
break;
case 'ANMF':
if (!this.getAnimFrameInfo(input, webp, size)) {
return false;
}
break;
case 'ICCP':
webp.iccpData = input.readRange(size).toUint8Array();
break;
case 'EXIF':
webp.exifData = input.readString(size);
break;
case 'XMP ':
webp.xmpData = input.readString(size);
break;
default:
input.skip(diskSize);
break;
}
const remainder = diskSize - (input.position - p);
if (remainder > 0) {
input.skip(remainder);
}
}
if (!webp.hasAlpha) {
webp.hasAlpha = webp.alphaData !== undefined;
}
return webp.format !== WebPFormat.undefined;
}
getVp8xInfo(input, webp) {
const b = input.read();
if ((b & 0xc0) !== 0) {
return false;
}
const alpha = (b >>> 4) & 0x1;
const anim = (b >>> 1) & 0x1;
if ((b & 0x1) !== 0) {
return false;
}
if (input.readUint24() !== 0) {
return false;
}
const w = input.readUint24() + 1;
const h = input.readUint24() + 1;
webp.width = w;
webp.height = h;
webp.hasAnimation = anim !== 0;
webp.hasAlpha = alpha !== 0;
return true;
}
getAnimInfo(input, webp) {
const c = input.readUint32();
const a = c & 0xff;
const r = (c >>> 8) & 0xff;
const g = (c >>> 16) & 0xff;
const b = (c >>> 24) & 0xff;
webp.backgroundColor = new ColorRgba8(r, g, b, a);
webp.animLoopCount = input.readUint16();
return true;
}
getAnimFrameInfo(input, webp, size) {
const frame = new WebPFrame(input, size);
if (!frame.isValid) {
return false;
}
webp.frames.push(frame);
return true;
}
isValidFile(bytes) {
this._input = new InputBuffer({
buffer: bytes,
});
if (!this.getHeader(this._input)) {
return false;
}
return true;
}
startDecode(bytes) {
this._input = new InputBuffer({
buffer: bytes,
});
if (!this.getHeader(this._input)) {
return undefined;
}
this._info = new WebPInfoInternal();
if (!this.getInfo(this._input, this._info)) {
return undefined;
}
switch (this._info.format) {
case WebPFormat.animated: {
this._info.frameCount = this._info.frames.length;
return this._info;
}
case WebPFormat.lossless: {
this._input.offset = this._info.vp8Position;
const vp8l = new VP8L(this._input, this._info);
if (!vp8l.decodeHeader()) {
return undefined;
}
this._info.frameCount = this._info.frames.length;
return this._info;
}
case WebPFormat.lossy: {
this._input.offset = this._info.vp8Position;
const vp8 = new VP8(this._input, this._info);
if (!vp8.decodeHeader()) {
return undefined;
}
this._info.frameCount = this._info.frames.length;
return this._info;
}
case WebPFormat.undefined:
throw new LibError('Unknown format for WebP');
}
return undefined;
}
decode(opt) {
var _a;
if (this.startDecode(opt.bytes) === undefined) {
return undefined;
}
if (!this._info.hasAnimation || 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) {
this._info.frame = i;
const frame = this._info.frames[i];
const image = this.decodeFrame(i);
if (image === undefined) {
continue;
}
image.frameDuration = frame.duration;
if (firstImage === undefined || lastImage === undefined) {
firstImage = new MemoryImage({
width: this._info.width,
height: this._info.height,
numChannels: image.numChannels,
format: image.format,
frameDuration: image.frameDuration,
});
lastImage = firstImage;
}
else {
lastImage = MemoryImage.from(lastImage);
if (frame.clearFrame) {
lastImage.clear();
}
}
Draw.compositeImage({
dst: lastImage,
src: image,
dstX: frame.x,
dstY: frame.y,
});
firstImage.addFrame(lastImage);
}
return firstImage;
}
decodeFrame(frameIndex) {
if (this._input === undefined || this._info === undefined) {
return undefined;
}
if (this._info.hasAnimation) {
if (frameIndex >= this._info.frames.length || frameIndex < 0) {
return undefined;
}
const f = this._info.frames[frameIndex];
const frameData = this._input.subarray(f.frameSize, f.framePosition);
return this.decodeFrameInternal(frameData, frameIndex);
}
if (this._info.format === WebPFormat.lossless) {
const data = this._input.subarray(this._info.vp8Size, this._info.vp8Position);
return new VP8L(data, this._info).decode();
}
else if (this._info.format === WebPFormat.lossy) {
const data = this._input.subarray(this._info.vp8Size, this._info.vp8Position);
return new VP8(data, this._info).decode();
}
return undefined;
}
}
//# sourceMappingURL=webp-decoder.js.map