You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

467 lines
12 KiB
JavaScript

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

//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;
}