/** * @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 = clearCode; let last; clear(); 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); }