319 lines
9.1 KiB
JavaScript
319 lines
9.1 KiB
JavaScript
|
/**
|
||
|
* @copyright Shachaf Ben-Kiki and the Converse.js contributors
|
||
|
* @description
|
||
|
* Started as a fork of Shachaf Ben-Kiki's jsgif library
|
||
|
* https://github.com/shachaf/jsgif
|
||
|
* @license MIT License
|
||
|
*/
|
||
|
|
||
|
function bitsToNum (ba) {
|
||
|
return ba.reduce(function (s, n) {
|
||
|
return s * 2 + n;
|
||
|
}, 0);
|
||
|
}
|
||
|
|
||
|
function byteToBitArr (bite) {
|
||
|
const a = [];
|
||
|
for (let i = 7; i >= 0; i--) {
|
||
|
a.push( !! (bite & (1 << i)));
|
||
|
}
|
||
|
return a;
|
||
|
}
|
||
|
|
||
|
function lzwDecode (minCodeSize, data) {
|
||
|
// TODO: Now that the GIF parser is a bit different, maybe this should get an array of bytes instead of a String?
|
||
|
let pos = 0; // Maybe this streaming thing should be merged with the Stream?
|
||
|
function readCode (size) {
|
||
|
let code = 0;
|
||
|
for (let i = 0; i < size; i++) {
|
||
|
if (data.charCodeAt(pos >> 3) & (1 << (pos & 7))) {
|
||
|
code |= 1 << i;
|
||
|
}
|
||
|
pos++;
|
||
|
}
|
||
|
return code;
|
||
|
}
|
||
|
|
||
|
const output = [];
|
||
|
const clearCode = 1 << minCodeSize;
|
||
|
const eoiCode = clearCode + 1;
|
||
|
|
||
|
let codeSize = minCodeSize + 1;
|
||
|
let dict = [];
|
||
|
|
||
|
const clear = function () {
|
||
|
dict = [];
|
||
|
codeSize = minCodeSize + 1;
|
||
|
for (let i = 0; i < clearCode; i++) {
|
||
|
dict[i] = [i];
|
||
|
}
|
||
|
dict[clearCode] = [];
|
||
|
dict[eoiCode] = null;
|
||
|
};
|
||
|
|
||
|
let code;
|
||
|
let last;
|
||
|
|
||
|
while (true) { // eslint-disable-line no-constant-condition
|
||
|
last = code;
|
||
|
code = readCode(codeSize);
|
||
|
|
||
|
if (code === clearCode) {
|
||
|
clear();
|
||
|
continue;
|
||
|
}
|
||
|
if (code === eoiCode) break;
|
||
|
|
||
|
if (code < dict.length) {
|
||
|
if (last !== clearCode) {
|
||
|
dict.push(dict[last].concat(dict[code][0]));
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
if (code !== dict.length) throw new Error('Invalid LZW code.');
|
||
|
dict.push(dict[last].concat(dict[last][0]));
|
||
|
}
|
||
|
output.push.apply(output, dict[code]);
|
||
|
|
||
|
if (dict.length === (1 << codeSize) && codeSize < 12) {
|
||
|
// If we're at the last code and codeSize is 12, the next code will be a clearCode, and it'll be 12 bits long.
|
||
|
codeSize++;
|
||
|
}
|
||
|
}
|
||
|
// I don't know if this is technically an error, but some GIFs do it.
|
||
|
//if (Math.ceil(pos / 8) !== data.length) throw new Error('Extraneous LZW bytes.');
|
||
|
return output;
|
||
|
}
|
||
|
|
||
|
|
||
|
function readSubBlocks (st) {
|
||
|
let size, data;
|
||
|
data = '';
|
||
|
do {
|
||
|
size = st.readByte();
|
||
|
data += st.read(size);
|
||
|
} while (size !== 0);
|
||
|
return data;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parses GIF image color table information
|
||
|
* @param { Stream } st
|
||
|
* @param { Number } entries
|
||
|
*/
|
||
|
function parseCT (st, entries) { // Each entry is 3 bytes, for RGB.
|
||
|
const ct = [];
|
||
|
for (let i = 0; i < entries; i++) {
|
||
|
ct.push(st.readBytes(3));
|
||
|
}
|
||
|
return ct;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parses GIF image information
|
||
|
* @param { Stream } st
|
||
|
* @param { ByteStream } img
|
||
|
* @param { Function } [callback]
|
||
|
*/
|
||
|
function parseImg (st, img, callback) {
|
||
|
function deinterlace (pixels, width) {
|
||
|
// Of course this defeats the purpose of interlacing. And it's *probably*
|
||
|
// the least efficient way it's ever been implemented. But nevertheless...
|
||
|
const newPixels = new Array(pixels.length);
|
||
|
const rows = pixels.length / width;
|
||
|
function cpRow (toRow, fromRow) {
|
||
|
const fromPixels = pixels.slice(fromRow * width, (fromRow + 1) * width);
|
||
|
newPixels.splice.apply(newPixels, [toRow * width, width].concat(fromPixels));
|
||
|
}
|
||
|
|
||
|
// See appendix E.
|
||
|
const offsets = [0, 4, 2, 1];
|
||
|
const steps = [8, 8, 4, 2];
|
||
|
let fromRow = 0;
|
||
|
for (let pass = 0; pass < 4; pass++) {
|
||
|
for (let toRow = offsets[pass]; toRow < rows; toRow += steps[pass]) {
|
||
|
cpRow(toRow, fromRow)
|
||
|
fromRow++;
|
||
|
}
|
||
|
}
|
||
|
return newPixels;
|
||
|
}
|
||
|
|
||
|
img.leftPos = st.readUnsigned();
|
||
|
img.topPos = st.readUnsigned();
|
||
|
img.width = st.readUnsigned();
|
||
|
img.height = st.readUnsigned();
|
||
|
|
||
|
const bits = byteToBitArr(st.readByte());
|
||
|
img.lctFlag = bits.shift();
|
||
|
img.interlaced = bits.shift();
|
||
|
img.sorted = bits.shift();
|
||
|
img.reserved = bits.splice(0, 2);
|
||
|
img.lctSize = bitsToNum(bits.splice(0, 3));
|
||
|
|
||
|
if (img.lctFlag) {
|
||
|
img.lct = parseCT(st, 1 << (img.lctSize + 1));
|
||
|
}
|
||
|
img.lzwMinCodeSize = st.readByte();
|
||
|
|
||
|
const lzwData = readSubBlocks(st);
|
||
|
img.pixels = lzwDecode(img.lzwMinCodeSize, lzwData);
|
||
|
|
||
|
if (img.interlaced) { // Move
|
||
|
img.pixels = deinterlace(img.pixels, img.width);
|
||
|
}
|
||
|
callback?.(img);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parses GIF header information
|
||
|
* @param { Stream } st
|
||
|
* @param { Function } [callback]
|
||
|
*/
|
||
|
function parseHeader (st, callback) {
|
||
|
const hdr = {};
|
||
|
hdr.sig = st.read(3);
|
||
|
hdr.ver = st.read(3);
|
||
|
if (hdr.sig !== 'GIF') {
|
||
|
throw new Error('Not a GIF file.');
|
||
|
}
|
||
|
hdr.width = st.readUnsigned();
|
||
|
hdr.height = st.readUnsigned();
|
||
|
|
||
|
const bits = byteToBitArr(st.readByte());
|
||
|
hdr.gctFlag = bits.shift();
|
||
|
hdr.colorRes = bitsToNum(bits.splice(0, 3));
|
||
|
hdr.sorted = bits.shift();
|
||
|
hdr.gctSize = bitsToNum(bits.splice(0, 3));
|
||
|
|
||
|
hdr.bgColor = st.readByte();
|
||
|
hdr.pixelAspectRatio = st.readByte(); // if not 0, aspectRatio = (pixelAspectRatio + 15) / 64
|
||
|
if (hdr.gctFlag) {
|
||
|
hdr.gct = parseCT(st, 1 << (hdr.gctSize + 1));
|
||
|
}
|
||
|
callback?.(hdr);
|
||
|
}
|
||
|
|
||
|
function parseExt (st, block, handler) {
|
||
|
|
||
|
function parseGCExt (block) {
|
||
|
st.readByte(); // blocksize, always 4
|
||
|
const bits = byteToBitArr(st.readByte());
|
||
|
block.reserved = bits.splice(0, 3); // Reserved; should be 000.
|
||
|
block.disposalMethod = bitsToNum(bits.splice(0, 3));
|
||
|
block.userInput = bits.shift();
|
||
|
block.transparencyGiven = bits.shift();
|
||
|
block.delayTime = st.readUnsigned();
|
||
|
block.transparencyIndex = st.readByte();
|
||
|
block.terminator = st.readByte();
|
||
|
handler?.gce(block);
|
||
|
}
|
||
|
|
||
|
function parseComExt (block) {
|
||
|
block.comment = readSubBlocks(st);
|
||
|
handler.com && handler.com(block);
|
||
|
}
|
||
|
|
||
|
function parsePTExt (block) {
|
||
|
// No one *ever* uses this. If you use it, deal with parsing it yourself.
|
||
|
st.readByte(); // blocksize, always 12
|
||
|
block.ptHeader = st.readBytes(12);
|
||
|
block.ptData = readSubBlocks(st);
|
||
|
handler.pte && handler.pte(block);
|
||
|
}
|
||
|
|
||
|
function parseAppExt (block) {
|
||
|
function parseNetscapeExt (block) {
|
||
|
st.readByte(); // blocksize, always 3
|
||
|
block.unknown = st.readByte(); // ??? Always 1? What is this?
|
||
|
block.iterations = st.readUnsigned();
|
||
|
block.terminator = st.readByte();
|
||
|
handler.app && handler.app.NETSCAPE && handler.app.NETSCAPE(block);
|
||
|
}
|
||
|
|
||
|
function parseUnknownAppExt (block) {
|
||
|
block.appData = readSubBlocks(st);
|
||
|
// FIXME: This won't work if a handler wants to match on any identifier.
|
||
|
handler.app && handler.app[block.identifier] && handler.app[block.identifier](block);
|
||
|
}
|
||
|
|
||
|
st.readByte(); // blocksize, always 11
|
||
|
block.identifier = st.read(8);
|
||
|
block.authCode = st.read(3);
|
||
|
switch (block.identifier) {
|
||
|
case 'NETSCAPE':
|
||
|
parseNetscapeExt(block);
|
||
|
break;
|
||
|
default:
|
||
|
parseUnknownAppExt(block);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function parseUnknownExt (block) {
|
||
|
block.data = readSubBlocks(st);
|
||
|
handler.unknown && handler.unknown(block);
|
||
|
}
|
||
|
|
||
|
block.label = st.readByte();
|
||
|
switch (block.label) {
|
||
|
case 0xF9:
|
||
|
block.extType = 'gce';
|
||
|
parseGCExt(block);
|
||
|
break;
|
||
|
case 0xFE:
|
||
|
block.extType = 'com';
|
||
|
parseComExt(block);
|
||
|
break;
|
||
|
case 0x01:
|
||
|
block.extType = 'pte';
|
||
|
parsePTExt(block);
|
||
|
break;
|
||
|
case 0xFF:
|
||
|
block.extType = 'app';
|
||
|
parseAppExt(block);
|
||
|
break;
|
||
|
default:
|
||
|
block.extType = 'unknown';
|
||
|
parseUnknownExt(block);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param { Stream } st
|
||
|
* @param { GIFParserHandlers } handler
|
||
|
*/
|
||
|
function parseBlock (st, handler) {
|
||
|
const block = {}
|
||
|
block.sentinel = st.readByte();
|
||
|
switch (String.fromCharCode(block.sentinel)) { // For ease of matching
|
||
|
case '!':
|
||
|
block.type = 'ext';
|
||
|
parseExt(st, block, handler);
|
||
|
break;
|
||
|
case ',':
|
||
|
block.type = 'img';
|
||
|
parseImg(st, block, handler?.img);
|
||
|
break;
|
||
|
case ';':
|
||
|
block.type = 'eof';
|
||
|
handler?.eof(block);
|
||
|
break;
|
||
|
default:
|
||
|
throw new Error('Unknown block: 0x' + block.sentinel.toString(16)); // TODO: Pad this with a 0.
|
||
|
}
|
||
|
if (block.type !== 'eof') setTimeout(() => parseBlock(st, handler), 0);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Takes a Stream and parses it for GIF data, calling the relevant handler
|
||
|
* methods on the passed in `handler` object.
|
||
|
* @param { Stream } st
|
||
|
* @param { GIFParserHandlers } handler
|
||
|
*/
|
||
|
export function parseGIF (st, handler={}) {
|
||
|
parseHeader(st, handler?.hdr);
|
||
|
setTimeout(() => parseBlock(st, handler), 0);
|
||
|
}
|