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

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