//https://github.com/shachaf/jsgif // Generic functions var bitsToNum = function (ba) { return ba.reduce(function (s, n) { return s * 2 + n; }, 0); }; var byteToBitArr = function (bite) { var a = []; for (var i = 7; i >= 0; i--) { a.push(!!(bite & (1 << i))); } return a; }; // Stream /** * @constructor */ // Make compiler happy. var Stream = function (data) { this.data = data; this.len = this.data.length; this.pos = 0; this.readByte = function () { if (this.pos >= this.data.length) { throw new Error('Attempted to read past end of stream.'); } return data.charCodeAt(this.pos++) & 0xFF; }; this.readBytes = function (n) { var bytes = []; for (var i = 0; i < n; i++) { bytes.push(this.readByte()); } return bytes; }; this.read = function (n) { var s = ''; for (var i = 0; i < n; i++) { s += String.fromCharCode(this.readByte()); } return s; }; this.readUnsigned = function () { // Little-endian. var a = this.readBytes(2); return (a[1] << 8) + a[0]; }; }; var lzwDecode = function (minCodeSize, data) { // TODO: Now that the GIF parser is a bit different, maybe this should get an array of bytes instead of a String? var pos = 0; // Maybe this streaming thing should be merged with the Stream? var readCode = function (size) { var code = 0; for (var i = 0; i < size; i++) { if (data.charCodeAt(pos >> 3) & (1 << (pos & 7))) { code |= 1 << i; } pos++; } return code; }; var output = []; var clearCode = 1 << minCodeSize; var eoiCode = clearCode + 1; var codeSize = minCodeSize + 1; var dict = []; var clear = function () { dict = []; codeSize = minCodeSize + 1; for (var i = 0; i < clearCode; i++) { dict[i] = [i]; } dict[clearCode] = []; dict[eoiCode] = null; }; var code; var last; while (true) { 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; }; // The actual parsing; returns an object with properties. var parseGIF = function (st, handler) { handler || (handler = {}); // LZW (GIF-specific) var parseCT = function (entries) { // Each entry is 3 bytes, for RGB. var ct = []; for (var i = 0; i < entries; i++) { ct.push(st.readBytes(3)); } return ct; }; var readSubBlocks = function () { var size, data; data = ''; do { size = st.readByte(); data += st.read(size); } while (size !== 0); return data; }; var parseHeader = function () { var hdr = {}; hdr.sig = st.read(3); hdr.ver = st.read(3); if (hdr.sig !== 'GIF') throw new Error('Not a GIF file.'); // XXX: This should probably be handled more nicely. hdr.width = st.readUnsigned(); hdr.height = st.readUnsigned(); var 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(1 << (hdr.gctSize + 1)); } handler.hdr && handler.hdr(hdr); }; var parseExt = function (block) { var parseGCExt = function (block) { var blockSize = st.readByte(); // Always 4 var 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 && handler.gce(block); }; var parseComExt = function (block) { block.comment = readSubBlocks(); handler.com && handler.com(block); }; var parsePTExt = function (block) { // No one *ever* uses this. If you use it, deal with parsing it yourself. var blockSize = st.readByte(); // Always 12 block.ptHeader = st.readBytes(12); block.ptData = readSubBlocks(); handler.pte && handler.pte(block); }; var parseAppExt = function (block) { var parseNetscapeExt = function (block) { var blockSize = st.readByte(); // 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); }; var parseUnknownAppExt = function (block) { block.appData = readSubBlocks(); // 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); }; var blockSize = st.readByte(); // Always 11 block.identifier = st.read(8); block.authCode = st.read(3); switch (block.identifier) { case 'NETSCAPE': parseNetscapeExt(block); break; default: parseUnknownAppExt(block); break; } }; var parseUnknownExt = function (block) { block.data = readSubBlocks(); 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; } }; var parseImg = function (img) { var deinterlace = function (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... var newPixels = new Array(pixels.length); var rows = pixels.length / width; var cpRow = function (toRow, fromRow) { var fromPixels = pixels.slice(fromRow * width, (fromRow + 1) * width); newPixels.splice.apply(newPixels, [toRow * width, width].concat(fromPixels)); }; // See appendix E. var offsets = [0, 4, 2, 1]; var steps = [8, 8, 4, 2]; var fromRow = 0; for (var pass = 0; pass < 4; pass++) { for (var 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(); var 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(1 << (img.lctSize + 1)); } img.lzwMinCodeSize = st.readByte(); var lzwData = readSubBlocks(); img.pixels = lzwDecode(img.lzwMinCodeSize, lzwData); if (img.interlaced) { // Move img.pixels = deinterlace(img.pixels, img.width); } handler.img && handler.img(img); }; var parseBlock = function () { var block = {}; block.sentinel = st.readByte(); switch (String.fromCharCode(block.sentinel)) { // For ease of matching case '!': block.type = 'ext'; parseExt(block); break; case ',': block.type = 'img'; parseImg(block); break; case ';': block.type = 'eof'; handler.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,0); if (block.type !== 'eof') parseBlock(); }; var parse = function () { parseHeader(); parseBlock(); // setTimeout(parseBlock, 0); }; parse(); }; // BEGIN_NON_BOOKMARKLET_CODE exports.Stream = Stream; exports.parseGIF = parseGIF; exports.decode = function (buffer) { var gifData = { images: [] }; var gct; var GCE; var log = console.log; // var showBool = function (b) { // return b ? 'yes' : 'no'; // }; // var showColor = function (rgb) { // // FIXME When I have an Internet connection. // var showHex = function (n) { // Two-digit code. // var hexChars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']; // return hexChars[(n >>> 4) & 0xF] + hexChars[n & 0xF]; // }; // return '#' + showHex(rgb[0]) + showHex(rgb[1]) + showHex(rgb[2]); // }; // var showDisposalMethod = function (dm) { // var map = { // 0: 'None', // 1: 'Do not dispose', // 2: 'Restore to background', // 3: 'Restore to previous' // }; // return map[dm] || 'Unknown'; // } //尺寸 背景色 调色板 等 var doHdr = function (hdr) { // log('Header:'); // log(' Version: %s', hdr.ver); // log(' Size: %dx%d', hdr.width, hdr.height); // log(' GCT? %s%s', showBool(hdr.gctFlag), hdr.gctFlag ? ' (' + hdr.gct.length + ' entries)' : ''); // log(' Color resolution: %d', hdr.colorRes); // log(' Sorted? %s', showBool(hdr.sorted)); // log(' Background color: %s (%d)', hdr.gctFlag ? showColor(hdr.bgColor) : 'no GCT', hdr.bgColor); // log(' Pixel aspect ratio: %d FIXME', hdr.pixelAspectRatio); gct = hdr.gct; gifData.width = hdr.width; gifData.height = hdr.height; }; //当前帧的非颜色信息 (透明色,延时 等)) var doGCE = function (gce) { // log('GCE:'); // log(' Disposal method: %d (%s)', gce.disposalMethod, showDisposalMethod(gce.disposalMethod)); // log(' User input expected? %s', showBool(gce.userInput)); // log(' Transparency given? %s%s', showBool(gce.transparencyGiven), // gce.transparencyGiven ? ' (index: ' + gce.transparencyIndex + ')' : ''); // log(' Delay time: %d', gce.delayTime); GCE = gce; }; //当前帧的图片图像信息 var doImg = function (img) { // log('Image descriptor:'); // log(Object.assign({},img,{'pixels':Math.max.apply(Math,img.pixels)})); // log(' Geometry: %dx%d+%d+%d', img.width, img.height, img.leftPos, img.topPos); // log(' LCT? %s%s', showBool(img.lctFlag), img.lctFlag ? ' (' + img.lct.length + ' entries)' : ''); // log(' Interlaced? %s', showBool(img.interlaced)); // log(' %d pixels', img.pixels.length); var data = []; var imageData = { left: img.leftPos, top: img.topPos, width: img.width, height: img.height, data: data } img.pixels.forEach(function (v) { var color = gct[v]; data.push.apply(data, color.concat(GCE.transparencyIndex == v ? 0 : 255)); }) gifData.images.push(imageData); }; var doNetscape = function (block) { // log('Netscape application extension:'); // log(' Iterations: %d%s', block.iterations, block.iterations === 0 ? ' (infinite)' : ''); }; var doCom = function (com) { // log('Comment extension:'); // log(' Comment: %s', com.comment); }; var doEOF = function (eof) { // log('EOF:'); // log(eof); }; var doUnknownApp = function (block) { // log(block) }; var doUnknownExt = function (block) { // log(block) } var handler = { hdr: doHdr, img: doImg, gce: doGCE, com: doCom, app: { NETSCAPE: doNetscape, unknown: doUnknownApp }, eof: doEOF }; var st = new Stream(buffer.toString('binary')); parseGIF(st, handler); return gifData; }