1106 lines
27 KiB
TypeScript
1106 lines
27 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
/* eslint-disable @typescript-eslint/no-empty-function */
|
|
// @ts-nocheck
|
|
|
|
/*
|
|
|
|
-- SOURCE: https://github.com/mooyoul/node-webpinfo
|
|
-- LICENSE:
|
|
|
|
The MIT License (MIT)
|
|
Copyright © 2018 MooYeol Prescott Lee, http://debug.so <mooyoul@gmail.com>
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the “Software”), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
THE SOFTWARE.
|
|
|
|
*/
|
|
|
|
import thunks from 'thunks';
|
|
|
|
import { StreamParserWritable } from './stream-parser';
|
|
|
|
const { thunk } = thunks;
|
|
|
|
const BUF_RIFF = Buffer.from('RIFF', 'utf8');
|
|
const BUF_WEBP = Buffer.from('WEBP', 'utf8');
|
|
const BUF_VP8 = Buffer.from('VP8 ', 'utf8');
|
|
const BUF_VP8L = Buffer.from('VP8L', 'utf8');
|
|
const BUF_VP8X = Buffer.from('VP8X', 'utf8');
|
|
const BUF_ANIM = Buffer.from('ANIM', 'utf8');
|
|
const BUF_ANMF = Buffer.from('ANMF', 'utf8');
|
|
const BUF_ALPH = Buffer.from('ALPH', 'utf8');
|
|
const BUF_ICCP = Buffer.from('ICCP', 'utf8');
|
|
const BUF_EXIF = Buffer.from('EXIF', 'utf8');
|
|
const BUF_XMP = Buffer.from('XMP ', 'utf8');
|
|
|
|
interface Callback<T> {
|
|
(e: any): void;
|
|
(e: any, data: T): void;
|
|
}
|
|
|
|
export type Input = Buffer;
|
|
|
|
export type ChunkType =
|
|
| 'VP8'
|
|
| 'VP8L'
|
|
| 'VP8X'
|
|
| 'ANIM'
|
|
| 'ANMF'
|
|
| 'ALPH'
|
|
| 'ICCP'
|
|
| 'EXIF'
|
|
| 'XMP';
|
|
|
|
export enum ANMFBlendingMethod {
|
|
ALPHA_BLENDING = 0,
|
|
DO_NOT_BLEND = 1,
|
|
}
|
|
|
|
export enum ANMFDisposalMethod {
|
|
AS_IS = 0,
|
|
FILL_BACKGROUND_COLOR = 1,
|
|
}
|
|
|
|
export enum ALPHPreProcessing {
|
|
NO_PRE_PROCESSING = 0,
|
|
LEVEL_REDUCTION = 1,
|
|
}
|
|
|
|
export enum ALPHFilteringMethod {
|
|
NONE = 0,
|
|
HORIZONTAL = 1,
|
|
VERTICAL = 2,
|
|
GRADIENT = 3,
|
|
}
|
|
|
|
export enum ALPHCompressionMethod {
|
|
NO_COMPRESSION = 0,
|
|
WEBP_LOSELESS = 1,
|
|
}
|
|
|
|
export interface RIFFContainer {
|
|
size: number;
|
|
}
|
|
|
|
export interface Chunk {
|
|
type: ChunkType;
|
|
offset: number;
|
|
size: number;
|
|
}
|
|
|
|
export interface VP8Bitstream {
|
|
keyframe: number; // 0 for key frames, 1 for interframes
|
|
version: number; // 0 ~ 3
|
|
showFrame: number; // 0 when current frame is not for display, 1 when current frame is for display
|
|
firstPartSize: number;
|
|
width: number;
|
|
horizontalScale: number;
|
|
height: number;
|
|
verticalScale: number;
|
|
}
|
|
|
|
export interface VP8LBitstream {
|
|
width: number;
|
|
height: number;
|
|
alpha: number;
|
|
version: number;
|
|
}
|
|
|
|
export interface VP8XBitstream {
|
|
rsv1: number;
|
|
icc: number;
|
|
alpha: number;
|
|
exif: number;
|
|
xmp: number;
|
|
animation: number;
|
|
rsv2: number;
|
|
rsv3: number;
|
|
canvasWidth: number;
|
|
canvasHeight: number;
|
|
}
|
|
|
|
export interface ANIMBitstream {
|
|
backgroundColor: {
|
|
r: number;
|
|
g: number;
|
|
b: number;
|
|
a: number;
|
|
};
|
|
loopCount: number;
|
|
}
|
|
|
|
export interface ANMFBitstream {
|
|
offsetX: number;
|
|
offsetY: number;
|
|
width: number;
|
|
height: number;
|
|
duration: number;
|
|
rsv1: number;
|
|
blending: number;
|
|
disposal: number;
|
|
}
|
|
|
|
export interface ALPHBitstream {
|
|
rsv1: number;
|
|
preprocessing: number;
|
|
filtering: number;
|
|
compression: number;
|
|
}
|
|
|
|
export interface VP8Chunk extends Chunk {
|
|
type: 'VP8';
|
|
format: 'lossy';
|
|
width: number;
|
|
height: number;
|
|
bitstream: VP8Bitstream;
|
|
}
|
|
|
|
export interface VP8LChunk extends Chunk {
|
|
type: 'VP8L';
|
|
format: 'lossless';
|
|
width: number;
|
|
height: number;
|
|
alpha: boolean;
|
|
bitstream: VP8LBitstream;
|
|
}
|
|
|
|
export interface VP8XChunk extends Chunk {
|
|
type: 'VP8X';
|
|
format: 'extended';
|
|
width: number;
|
|
height: number;
|
|
icc: boolean;
|
|
alpha: boolean;
|
|
exif: boolean;
|
|
xmp: boolean;
|
|
animation: boolean;
|
|
bitstream: VP8XBitstream;
|
|
}
|
|
|
|
export interface ANIMChunk extends Chunk {
|
|
type: 'ANIM';
|
|
backgroundColor: {
|
|
r: number;
|
|
g: number;
|
|
b: number;
|
|
a: number;
|
|
};
|
|
loop: number;
|
|
bitstream: ANIMBitstream;
|
|
}
|
|
|
|
export interface ANMFChunk extends Chunk {
|
|
type: 'ANMF';
|
|
offsetX: number;
|
|
offsetY: number;
|
|
width: number;
|
|
height: number;
|
|
duration: number;
|
|
blend: ANMFBlendingMethod;
|
|
dispose: ANMFDisposalMethod;
|
|
bitstream: ANMFBitstream;
|
|
}
|
|
|
|
export interface ALPHChunk extends Chunk {
|
|
type: 'ALPH';
|
|
preprocessing: ALPHPreProcessing;
|
|
filter: ALPHFilteringMethod;
|
|
compress: ALPHCompressionMethod;
|
|
bitstream: ALPHBitstream;
|
|
}
|
|
|
|
export interface ICCPChunk extends Chunk {
|
|
// tslint:disable-line
|
|
type: 'ICCP';
|
|
}
|
|
|
|
export interface EXIFChunk extends Chunk {
|
|
type: 'EXIF';
|
|
}
|
|
|
|
export interface XMPChunk extends Chunk {
|
|
type: 'XMP';
|
|
}
|
|
|
|
export type WebPChunk =
|
|
| VP8Chunk
|
|
| VP8LChunk
|
|
| VP8XChunk
|
|
| ANIMChunk
|
|
| ANMFChunk
|
|
| ALPHChunk
|
|
| ICCPChunk
|
|
| EXIFChunk
|
|
| XMPChunk;
|
|
|
|
export interface WebP {
|
|
riff: RIFFContainer;
|
|
chunks: WebPChunk[];
|
|
summary: {
|
|
width?: number;
|
|
height?: number;
|
|
isAnimated: boolean;
|
|
isLossless: boolean;
|
|
hasAlpha: boolean;
|
|
frames: number;
|
|
chunks: {
|
|
[K in ChunkType]: number;
|
|
};
|
|
};
|
|
}
|
|
|
|
// tslint:disable:no-bitwise
|
|
export class WebPInfo extends StreamParserWritable {
|
|
public static async isAnimated(input: Input): Promise<boolean> {
|
|
const { summary } = await this.from(input);
|
|
|
|
return summary.isAnimated;
|
|
}
|
|
|
|
public static async isLossless(input: Input): Promise<boolean> {
|
|
const { summary } = await this.from(input);
|
|
|
|
return summary.isLossless;
|
|
}
|
|
|
|
public static from(input: Input): Promise<WebP> {
|
|
return new Promise<WebP>((resolve, reject) => {
|
|
const parser = new this();
|
|
|
|
function onError(e: Error) {
|
|
parser.removeListener('error', onError);
|
|
parser.removeListener('format', onFormat);
|
|
|
|
reject(e);
|
|
}
|
|
|
|
function onFormat(format: WebP) {
|
|
parser.removeListener('error', onError);
|
|
parser.removeListener('format', onFormat);
|
|
|
|
resolve(format);
|
|
}
|
|
|
|
parser.on('error', onError);
|
|
parser.on('format', onFormat);
|
|
|
|
if (Buffer.isBuffer(input)) {
|
|
parser.end(input);
|
|
return;
|
|
}
|
|
});
|
|
}
|
|
|
|
private log(...args: any) {}
|
|
|
|
private readonly FRAME_COUNTABLE_CHUNK_TYPES = new Map<ChunkType, true>(
|
|
['VP8', 'VP8L', 'ANMF'].map((t) => [t, true] as [ChunkType, true]),
|
|
);
|
|
|
|
private offset = 0;
|
|
private maxSeekableOffset = -1; // same as file size - 1
|
|
|
|
private pending?: {
|
|
size: number;
|
|
done: Callback<any>;
|
|
};
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
thunk.seq([
|
|
thunk((done: Callback<RIFFContainer>) => this.readRIFFHeader()(done)),
|
|
thunk((done: Callback<void>) => this.readWebPSignature()(done)),
|
|
thunk((done: Callback<WebPChunk[]>) => this.readWebPChunk()(done)),
|
|
])((e: Error, values: [RIFFContainer, void, WebPChunk[]]) => {
|
|
if (e) {
|
|
this.emit('error', e);
|
|
return this.skipBytes(Infinity);
|
|
}
|
|
|
|
const [riff, , chunks] = values;
|
|
|
|
const { isAnimated, isLossless, hasAlpha, width, height } = chunks.reduce(
|
|
(hash, c, index) => {
|
|
if (
|
|
index === 0 &&
|
|
(c.type === 'VP8' || c.type === 'VP8L' || c.type === 'VP8X')
|
|
) {
|
|
hash.width = c.width;
|
|
hash.height = c.height;
|
|
}
|
|
|
|
if (c.type === 'VP8L') {
|
|
hash.isLossless = true;
|
|
}
|
|
|
|
if (c.type === 'ALPH' || (c.type === 'VP8X' && c.alpha)) {
|
|
hash.hasAlpha = true;
|
|
}
|
|
|
|
if (c.type === 'ANMF') {
|
|
hash.isAnimated = true;
|
|
}
|
|
|
|
return hash;
|
|
},
|
|
{
|
|
isAnimated: false,
|
|
isLossless: false,
|
|
hasAlpha: false,
|
|
} as {
|
|
isAnimated: boolean;
|
|
isLossless: boolean;
|
|
hasAlpha: boolean;
|
|
width?: number;
|
|
height?: number;
|
|
},
|
|
);
|
|
|
|
const summary = {
|
|
isAnimated,
|
|
isLossless,
|
|
hasAlpha,
|
|
width,
|
|
height,
|
|
frames: chunks.filter((c) =>
|
|
this.FRAME_COUNTABLE_CHUNK_TYPES.has(c.type),
|
|
).length,
|
|
chunks: chunks.reduce(
|
|
(hash, chunk) => {
|
|
const type = chunk.type as ChunkType;
|
|
|
|
hash[type]++;
|
|
|
|
return hash;
|
|
},
|
|
{
|
|
VP8: 0,
|
|
VP8L: 0,
|
|
VP8X: 0,
|
|
ALPH: 0,
|
|
ANIM: 0,
|
|
ANMF: 0,
|
|
ICCP: 0,
|
|
EXIF: 0,
|
|
XMP: 0,
|
|
} as {
|
|
[k in ChunkType]: number;
|
|
},
|
|
),
|
|
};
|
|
|
|
this.emit('format', {
|
|
riff,
|
|
chunks,
|
|
summary,
|
|
});
|
|
|
|
return this.skipBytes(Infinity);
|
|
})((e: Error) => {
|
|
if (e) {
|
|
this.emit('error', e);
|
|
}
|
|
});
|
|
}
|
|
|
|
public override end(...args: any[]) {
|
|
if (args.length && typeof args[args.length - 1] === 'function') {
|
|
return super.end(
|
|
...args.slice(0, args.length - 1),
|
|
(...cbArgs: any[]) => {
|
|
this.final();
|
|
args[args.length - 1](...cbArgs);
|
|
},
|
|
);
|
|
}
|
|
|
|
return super.end(...args, () => {
|
|
this.final();
|
|
});
|
|
}
|
|
|
|
private final() {
|
|
if (this.pending) {
|
|
if (this.pending.size !== Infinity) {
|
|
this.pending.done(
|
|
new Error(`stream is closed before read ${this.pending.size} bytes`),
|
|
);
|
|
}
|
|
|
|
this.pending = undefined;
|
|
}
|
|
}
|
|
|
|
private readBytes(size: number) {
|
|
return thunk((done: Callback<Buffer>) => {
|
|
if (this.pending) {
|
|
return done(new Error('there is already readBytes callback set'));
|
|
}
|
|
|
|
this.pending = { size, done };
|
|
this._bytes(size, (buf) => {
|
|
this.pending = undefined;
|
|
this.offset += buf.length;
|
|
|
|
done(null, buf);
|
|
});
|
|
});
|
|
}
|
|
|
|
private skipBytes(size: number) {
|
|
return thunk((done: Callback<void>) => {
|
|
if (this.pending) {
|
|
return done(new Error('there is already readBytes callback set'));
|
|
}
|
|
|
|
this.pending = { size, done };
|
|
this._skipBytes(size, () => {
|
|
this.pending = undefined;
|
|
this.offset += size;
|
|
|
|
done(null);
|
|
});
|
|
});
|
|
}
|
|
|
|
private readRIFFHeader() {
|
|
// 4 bytes - "RIFF" signature
|
|
// 4 bytes - RIFF Payload Size
|
|
return this.readBytes(8)((e: Error, buf: Buffer) => {
|
|
if (e) {
|
|
throw e;
|
|
}
|
|
|
|
this.log('starting parsing RIFF Header');
|
|
|
|
const bufFourCC = buf.slice(0, 4);
|
|
|
|
const isRIFF = Buffer.compare(bufFourCC, BUF_RIFF) === 0;
|
|
|
|
this.log('FourCC: ', bufFourCC.toString('utf8'));
|
|
|
|
if (!isRIFF) {
|
|
throw new Error('Given stream is not RIFF compatible.');
|
|
}
|
|
|
|
const riffChunkSize = buf.readUInt32LE(4);
|
|
this.log('detected RIFF chunk size: %d', riffChunkSize);
|
|
|
|
if (riffChunkSize <= 8) {
|
|
// "WEBP" Signature (4 bytes) + WebP Chunk Type (4 bytes)
|
|
throw new Error('Invalid RIFF Chunk size');
|
|
}
|
|
|
|
const riffContainer: RIFFContainer = { size: riffChunkSize + 8 };
|
|
this.emit('riff', riffContainer);
|
|
this.maxSeekableOffset = riffChunkSize + 8 - 1;
|
|
|
|
return riffContainer;
|
|
});
|
|
}
|
|
|
|
private readWebPSignature() {
|
|
// 4 bytes - "WEBP" signature
|
|
return this.readBytes(4)((e: Error, buf: Buffer) => {
|
|
if (e) {
|
|
throw e;
|
|
}
|
|
|
|
const isWebP = Buffer.compare(buf, BUF_WEBP) === 0;
|
|
|
|
if (!isWebP) {
|
|
throw new Error('Given stream is not WebP');
|
|
}
|
|
});
|
|
}
|
|
|
|
private readWebPChunk(memo: WebPChunk[] = []): any {
|
|
// sadly thunk typing is not good
|
|
if (this.offset > this.maxSeekableOffset) {
|
|
this.log(
|
|
'reached max seekable offset (current: %d, max: %d)',
|
|
this.offset,
|
|
this.maxSeekableOffset,
|
|
);
|
|
|
|
return memo;
|
|
}
|
|
|
|
// 8 bytes - WebP Chunk Header
|
|
// - 4 bytes - FourCC for Chunk
|
|
// - 4 bytes - Chunk Size w/o header
|
|
return this.readBytes(8)((e: Error, buf: Buffer) => {
|
|
if (e) {
|
|
throw e;
|
|
}
|
|
|
|
const bufChunkType = buf.slice(0, 4);
|
|
const chunkPayloadSize = buf.readUInt32LE(4);
|
|
|
|
const chunkOffset = this.offset - buf.length;
|
|
|
|
// If Chunk Size is odd, a single padding byte.
|
|
const chunkSize =
|
|
(chunkPayloadSize % 2 === 0 ? chunkPayloadSize : chunkPayloadSize + 1) +
|
|
8;
|
|
|
|
this.log(
|
|
'detected chunk (%s) payload size: %d, offset: %d',
|
|
bufChunkType.toString('utf8'),
|
|
chunkSize,
|
|
chunkOffset,
|
|
);
|
|
|
|
if (Buffer.compare(BUF_VP8, bufChunkType) === 0) {
|
|
return this.readVP8Bitstream()(
|
|
(err: Error, bitstream: VP8Bitstream): VP8Chunk => {
|
|
if (err) {
|
|
throw err;
|
|
}
|
|
|
|
return {
|
|
type: 'VP8',
|
|
format: 'lossy',
|
|
offset: chunkOffset,
|
|
size: chunkSize,
|
|
width: bitstream.width,
|
|
height: bitstream.height,
|
|
bitstream,
|
|
};
|
|
},
|
|
);
|
|
} else if (Buffer.compare(BUF_VP8L, bufChunkType) === 0) {
|
|
return this.readVP8LBitstream()(
|
|
(err: Error, bitstream: VP8LBitstream): VP8LChunk => {
|
|
if (err) {
|
|
throw err;
|
|
}
|
|
|
|
return {
|
|
type: 'VP8L',
|
|
format: 'lossless',
|
|
offset: chunkOffset,
|
|
size: chunkSize,
|
|
width: bitstream.width,
|
|
height: bitstream.height,
|
|
alpha: !!bitstream.alpha,
|
|
bitstream,
|
|
};
|
|
},
|
|
);
|
|
} else if (Buffer.compare(BUF_VP8X, bufChunkType) === 0) {
|
|
return this.readVP8XBitstream()(
|
|
(err: Error, bitstream: VP8XBitstream): VP8XChunk => {
|
|
if (err) {
|
|
throw err;
|
|
}
|
|
|
|
return {
|
|
type: 'VP8X',
|
|
format: 'extended',
|
|
offset: chunkOffset,
|
|
size: chunkSize,
|
|
width: bitstream.canvasWidth,
|
|
height: bitstream.canvasHeight,
|
|
icc: bitstream.icc === 1,
|
|
alpha: bitstream.alpha === 1,
|
|
exif: bitstream.exif === 1,
|
|
xmp: bitstream.xmp === 1,
|
|
animation: bitstream.animation === 1,
|
|
bitstream,
|
|
};
|
|
},
|
|
);
|
|
} else if (Buffer.compare(BUF_ANIM, bufChunkType) === 0) {
|
|
return this.readANIMBitstream()(
|
|
(err: Error, bitstream: ANIMBitstream): ANIMChunk => {
|
|
if (e) {
|
|
throw e;
|
|
}
|
|
|
|
return {
|
|
type: 'ANIM',
|
|
offset: chunkOffset,
|
|
size: chunkSize,
|
|
backgroundColor: bitstream.backgroundColor,
|
|
loop: bitstream.loopCount === 0 ? Infinity : bitstream.loopCount,
|
|
bitstream,
|
|
};
|
|
},
|
|
);
|
|
} else if (Buffer.compare(BUF_ANMF, bufChunkType) === 0) {
|
|
return this.readANMFBitstream()(
|
|
(err: Error, bitstream: ANMFBitstream): ANMFChunk => {
|
|
if (e) {
|
|
throw e;
|
|
}
|
|
|
|
return {
|
|
type: 'ANMF',
|
|
offset: chunkOffset,
|
|
size: chunkSize,
|
|
width: bitstream.width,
|
|
height: bitstream.height,
|
|
duration: bitstream.duration,
|
|
offsetX: bitstream.offsetX,
|
|
offsetY: bitstream.offsetY,
|
|
dispose:
|
|
bitstream.disposal === 0
|
|
? ANMFDisposalMethod.AS_IS
|
|
: ANMFDisposalMethod.FILL_BACKGROUND_COLOR,
|
|
blend:
|
|
bitstream.blending === 0
|
|
? ANMFBlendingMethod.ALPHA_BLENDING
|
|
: ANMFBlendingMethod.DO_NOT_BLEND,
|
|
bitstream,
|
|
};
|
|
},
|
|
);
|
|
} else if (Buffer.compare(BUF_ALPH, bufChunkType) === 0) {
|
|
return this.readALPHBitstream()(
|
|
(err: Error, bitstream: ALPHBitstream): ALPHChunk => {
|
|
if (e) {
|
|
throw e;
|
|
}
|
|
|
|
return {
|
|
type: 'ALPH',
|
|
offset: chunkOffset,
|
|
size: chunkSize,
|
|
preprocessing:
|
|
bitstream.preprocessing === 0
|
|
? ALPHPreProcessing.NO_PRE_PROCESSING
|
|
: ALPHPreProcessing.LEVEL_REDUCTION,
|
|
filter: (() => {
|
|
switch (bitstream.filtering) {
|
|
case ALPHFilteringMethod.HORIZONTAL:
|
|
return ALPHFilteringMethod.HORIZONTAL;
|
|
case ALPHFilteringMethod.VERTICAL:
|
|
return ALPHFilteringMethod.VERTICAL;
|
|
case ALPHFilteringMethod.GRADIENT:
|
|
return ALPHFilteringMethod.HORIZONTAL;
|
|
default:
|
|
return ALPHFilteringMethod.NONE;
|
|
}
|
|
})(),
|
|
compress:
|
|
bitstream.compression === 0
|
|
? ALPHCompressionMethod.NO_COMPRESSION
|
|
: ALPHCompressionMethod.WEBP_LOSELESS,
|
|
bitstream,
|
|
};
|
|
},
|
|
);
|
|
} else if (Buffer.compare(BUF_ICCP, bufChunkType) === 0) {
|
|
// @todo Handle ICC Profile Payload
|
|
const chunk: ICCPChunk = {
|
|
type: 'ICCP',
|
|
offset: chunkOffset,
|
|
size: chunkSize,
|
|
};
|
|
|
|
return thunk(chunk);
|
|
} else if (Buffer.compare(BUF_EXIF, bufChunkType) === 0) {
|
|
// @todo Handle EXIF
|
|
const chunk: EXIFChunk = {
|
|
type: 'EXIF',
|
|
offset: chunkOffset,
|
|
size: chunkSize,
|
|
};
|
|
|
|
return thunk(chunk);
|
|
} else if (Buffer.compare(BUF_XMP, bufChunkType) === 0) {
|
|
// @todo Handle XMP
|
|
const chunk: XMPChunk = {
|
|
type: 'XMP',
|
|
offset: chunkOffset,
|
|
size: chunkSize,
|
|
};
|
|
|
|
return thunk(chunk);
|
|
} else {
|
|
const chunkTypeHex = bufChunkType.toString('hex');
|
|
const chunkType = bufChunkType.toString('utf8');
|
|
|
|
throw new Error(
|
|
`Unsupported WebP chunk type ${chunkType} (0x${chunkTypeHex})`,
|
|
);
|
|
}
|
|
})((e: Error, chunk: WebPChunk) => {
|
|
if (e) {
|
|
throw e;
|
|
}
|
|
|
|
// emit chunk event and collect them
|
|
this.emit('chunk', chunk);
|
|
memo.push(chunk);
|
|
|
|
// skip remaining chunk bytes, if exists.
|
|
const remainingBytes = chunk.offset + chunk.size - this.offset;
|
|
|
|
if (remainingBytes > 0) {
|
|
return this.skipBytes(remainingBytes);
|
|
}
|
|
})((e: Error) => {
|
|
if (e) {
|
|
throw e;
|
|
}
|
|
|
|
return this.readWebPChunk(memo);
|
|
});
|
|
}
|
|
|
|
private readVP8Bitstream() {
|
|
// 3 bytes (24 bits) - "Uncompressed Data Chunk" on VP8 Bitstream
|
|
return this.readBytes(3)((e: Error, bufFrameHeader: Buffer) => {
|
|
if (e) {
|
|
throw e;
|
|
}
|
|
|
|
const frameTag =
|
|
bufFrameHeader.readUInt8(0) |
|
|
(bufFrameHeader.readUInt8(1) << 8) |
|
|
(bufFrameHeader.readUInt8(2) << 16);
|
|
|
|
const keyframe = frameTag & 0x01;
|
|
const version = (frameTag >> 1) & 0x7;
|
|
const showFrame = (frameTag >> 4) & 0x1;
|
|
const firstPartSize = (frameTag >> 5) & 0x7ffff;
|
|
|
|
this.log(
|
|
'Parsed VP8 Bitstream Header: keyframe: %s, version: %s, showFrame: %s, firstPartSize: %s',
|
|
keyframe,
|
|
version,
|
|
showFrame,
|
|
firstPartSize,
|
|
);
|
|
|
|
if (keyframe === 1) {
|
|
throw new Error(
|
|
"expected VP8 bitstream has keyframe, but couldn't be found",
|
|
);
|
|
}
|
|
|
|
// If given frame is Keyframe (Intraframe), It has 7 bytes data for additional information
|
|
return this.readBytes(7)((err: Error, bufKeyframeTag: Buffer) => {
|
|
if (err) {
|
|
throw err;
|
|
}
|
|
|
|
const hasValidStartCode =
|
|
Buffer.compare(
|
|
bufKeyframeTag.slice(0, 3),
|
|
Buffer.from([0x9d, 0x01, 0x2a]),
|
|
) === 0;
|
|
|
|
if (!hasValidStartCode) {
|
|
this.log(bufKeyframeTag.slice(0, 3));
|
|
throw new Error(
|
|
"expected VP8 intraframe start code, but couldn't be found",
|
|
);
|
|
}
|
|
|
|
const horizontalSizeCode = bufKeyframeTag.readUInt16LE(3);
|
|
|
|
const width = horizontalSizeCode & 0x3fff;
|
|
const horizontalScale = horizontalSizeCode >> 14;
|
|
|
|
const verticalSizeCode = bufKeyframeTag.readUInt16LE(5);
|
|
const height = verticalSizeCode & 0x3fff;
|
|
const verticalScale = verticalSizeCode >> 14;
|
|
|
|
this.log(
|
|
'width: %d, horizontalScale: %d, height: %d, verticalScale: %d',
|
|
width,
|
|
horizontalScale,
|
|
height,
|
|
verticalScale,
|
|
);
|
|
|
|
return {
|
|
keyframe,
|
|
version,
|
|
showFrame,
|
|
firstPartSize,
|
|
width,
|
|
horizontalScale,
|
|
height,
|
|
verticalScale,
|
|
};
|
|
});
|
|
});
|
|
}
|
|
|
|
private readVP8LBitstream() {
|
|
// 1 byte - One byte signature 0x2f.
|
|
return this.readBytes(1)((e: Error, bufVP8LSignature: Buffer) => {
|
|
if (e) {
|
|
throw e;
|
|
}
|
|
|
|
const hasValidVP8LSignature = bufVP8LSignature.readUInt8(0) === 0x2f;
|
|
|
|
if (!hasValidVP8LSignature) {
|
|
throw new Error('expected VP8L signature, but could not found');
|
|
}
|
|
|
|
// The first 28 bits of the bitstream specify the width and height of the image.
|
|
// Width and height are decoded as 14-bit integers
|
|
// last 4 bits are alpha hint (1 bit) + version (3 bit)
|
|
return this.readBytes(4);
|
|
})((e: Error, bufBitstreamHead: Buffer): VP8LBitstream => {
|
|
if (e) {
|
|
throw e;
|
|
}
|
|
|
|
const bits = bufBitstreamHead.readUInt32LE(0);
|
|
|
|
this.log('buf: ', bufBitstreamHead);
|
|
|
|
const width = (bits & 0x3fff) + 1;
|
|
const height = ((bits >> 14) & 0x3fff) + 1;
|
|
const alpha = (bits >> 28) & 0x01;
|
|
const version = (bits >> 29) & 0x7;
|
|
|
|
// The version_number is a 3 bit code that must be set to 0.
|
|
// Any other value should be treated as an error. [AMENDED]
|
|
if (version !== 0) {
|
|
throw new Error(
|
|
'expected VP8L bitstream version to 0, but got non-zero value',
|
|
);
|
|
}
|
|
|
|
this.log(
|
|
'width: %d, height: %d, alpha: %s, version: %d',
|
|
width,
|
|
height,
|
|
alpha,
|
|
version,
|
|
);
|
|
|
|
return {
|
|
width,
|
|
height,
|
|
alpha,
|
|
version,
|
|
};
|
|
});
|
|
}
|
|
|
|
private readVP8XBitstream() {
|
|
// 80 bits
|
|
//
|
|
// 2 bits - Reserved (1)
|
|
// 1 bit - ICC Profile flag
|
|
// 1 bit - Alpha flag
|
|
// 1 bit - EXIF metadata flag
|
|
// 1 bit - XMP metadata flag
|
|
// 1 bit - Animation flag
|
|
// 1 bit - Reserved (2)
|
|
// 24 bits - Reserved (3)
|
|
// 24 bits - Canvas Width (actual canvas width should add 1 to read value)
|
|
// 24 bits - Canvas Height (actual canvas height should add 1 to read value)
|
|
return this.readBytes(10)((e: Error, buf: Buffer): VP8XBitstream => {
|
|
if (e) {
|
|
throw e;
|
|
}
|
|
|
|
const flagBits = buf.readUInt8(0);
|
|
|
|
this.log('flagBits: ', flagBits.toString(2));
|
|
|
|
const rsv1 = flagBits >> 6;
|
|
|
|
if (rsv1 !== 0) {
|
|
throw new Error('First reserved bits (2 bits) should be zero');
|
|
}
|
|
|
|
const icc = (flagBits >> 5) & 0x01;
|
|
const alpha = (flagBits >> 4) & 0x01;
|
|
const exif = (flagBits >> 3) & 0x01;
|
|
const xmp = (flagBits >> 2) & 0x01;
|
|
const animation = (flagBits >> 1) & 0x01;
|
|
const rsv2 = flagBits & 0x01;
|
|
|
|
this.log(
|
|
'rsv1: %d, icc: %d, alpha: %d, exif: %d, xmp: %d, animation: %d, rsv2: %d',
|
|
rsv1,
|
|
icc,
|
|
alpha,
|
|
exif,
|
|
xmp,
|
|
animation,
|
|
rsv2,
|
|
);
|
|
|
|
if (rsv2 !== 0) {
|
|
throw new Error('Second reserved bit (1 bit) should be zero');
|
|
}
|
|
|
|
const rsv3 =
|
|
buf.readUInt8(1) | (buf.readUInt8(2) << 8) | (buf.readUInt8(3) << 16);
|
|
|
|
if (rsv3 !== 0) {
|
|
throw new Error('Third reserved bit (24 bits) should be zero');
|
|
}
|
|
|
|
const canvasWidth =
|
|
(buf.readUInt8(4) |
|
|
(buf.readUInt8(5) << 8) |
|
|
(buf.readUInt8(6) << 16)) +
|
|
1;
|
|
|
|
const canvasHeight =
|
|
(buf.readUInt8(7) |
|
|
(buf.readUInt8(8) << 8) |
|
|
(buf.readUInt8(9) << 16)) +
|
|
1;
|
|
|
|
this.log(
|
|
'canvas width: %d, canvas height: %d',
|
|
canvasWidth,
|
|
canvasHeight,
|
|
);
|
|
|
|
return {
|
|
rsv1,
|
|
icc,
|
|
alpha,
|
|
exif,
|
|
xmp,
|
|
animation,
|
|
rsv2,
|
|
rsv3,
|
|
canvasWidth,
|
|
canvasHeight,
|
|
};
|
|
});
|
|
}
|
|
|
|
private readANIMBitstream() {
|
|
// 6 bytes (48 bits)
|
|
// 32 bits - Background Color (rgba, byte order)
|
|
// 16 bits - Loop Count
|
|
return this.readBytes(6)((e: Error, buf: Buffer): ANIMBitstream => {
|
|
if (e) {
|
|
throw e;
|
|
}
|
|
|
|
const r = buf.readUInt8(0);
|
|
const g = buf.readUInt8(1);
|
|
const b = buf.readUInt8(2);
|
|
const a = buf.readUInt8(3);
|
|
|
|
const backgroundColor = { r, g, b, a };
|
|
const loopCount = buf.readUInt16LE(4);
|
|
|
|
return {
|
|
backgroundColor,
|
|
loopCount,
|
|
};
|
|
});
|
|
}
|
|
|
|
private readANMFBitstream() {
|
|
// 16 bytes (128 bits)
|
|
//
|
|
// 24 bits - Frame Offset X
|
|
// 24 bits - Frame Offset Y
|
|
// 24 bits - Frame Width minus one
|
|
// 24 bits - Frame Height minus one
|
|
// 24 bits - Frame Duration
|
|
// 6 bits - Reserved
|
|
// 1 bit - Blending method
|
|
// 1 bit - Disposal method
|
|
return this.readBytes(16)((e: Error, buf: Buffer): ANMFBitstream => {
|
|
if (e) {
|
|
throw e;
|
|
}
|
|
|
|
const frameX =
|
|
buf.readUInt8(0) | (buf.readUInt8(1) << 8) | (buf.readUInt8(2) << 16);
|
|
|
|
const frameY = buf.readUInt8(3) | buf.readUInt8(4) | buf.readUInt8(5);
|
|
|
|
const frameWidth =
|
|
(buf.readUInt8(6) | buf.readUInt8(7) | buf.readUInt8(8)) + 1;
|
|
|
|
const frameHeight =
|
|
(buf.readUInt8(9) | buf.readUInt8(10) | buf.readUInt8(11)) + 1;
|
|
|
|
const frameDuration =
|
|
buf.readUInt8(12) | buf.readUInt8(13) | buf.readUInt8(14);
|
|
|
|
const tailBits = buf.readUInt8(15);
|
|
|
|
const rsv1 = tailBits >> 2;
|
|
|
|
if (rsv1 !== 0) {
|
|
throw new Error('reserved bits (6 bits) should be zero');
|
|
}
|
|
|
|
const blending = (tailBits >> 6) & 0x01;
|
|
const disposal = (tailBits >> 7) & 0x01;
|
|
|
|
return {
|
|
offsetX: frameX,
|
|
offsetY: frameY,
|
|
width: frameWidth,
|
|
height: frameHeight,
|
|
duration: frameDuration,
|
|
rsv1,
|
|
blending,
|
|
disposal,
|
|
};
|
|
});
|
|
}
|
|
|
|
private readALPHBitstream() {
|
|
// 1 byte (8 bits)
|
|
//
|
|
// 2 bits - Reserved
|
|
// 2 bits - pre-processing method
|
|
// 2 bits - filtering method
|
|
// 2 bits - compression method
|
|
return this.readBytes(1)((e: Error, buf: Buffer): ALPHBitstream => {
|
|
if (e) {
|
|
throw e;
|
|
}
|
|
|
|
const bits = buf.readUInt8(0);
|
|
|
|
const rsv1 = bits >> 6;
|
|
const preprocessing = (bits >> 4) & 0x3;
|
|
const filtering = (bits >> 2) & 0x3;
|
|
const compression = bits & 0x3;
|
|
|
|
if (rsv1 !== 0) {
|
|
throw new Error('reserved bits (2 bits) should be zero');
|
|
}
|
|
|
|
return {
|
|
rsv1,
|
|
preprocessing,
|
|
filtering,
|
|
compression,
|
|
};
|
|
});
|
|
}
|
|
}
|