123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318 |
- /**
- * @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);
- }
|