Created
November 15, 2017 00:03
-
-
Save simonlindholm/3eb0be32312cc2be4bff003aee8c176b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Async, for use in WebExtensions. CC0. | |
function* gifDecoder($) { | |
var size, r, len; | |
// Header | |
if ($.avail < 6) yield $.Ensure(6); | |
var header = $.read(6); | |
if (header[0] != 0x47 || header[1] != 0x49 || header[2] != 0x46) | |
return $.Error("not a gif"); | |
if (header[3] != 0x38 || header[5] != 0x61 || (header[4] != 0x37 && header[4] != 0x39)) | |
return $.Error("unrecognized version"); | |
// Metadata | |
if ($.avail < 7) yield $.Ensure(7); | |
$.skip(4); // size | |
var bits = $.read1(); // has gct, color resolution, sorted, gct size | |
$.skip(2); // background color, aspect ratio | |
// GCT | |
if (bits & 0x80) { | |
size = 3 << ((bits & 0x7) + 1); | |
r = $.longSkip(size); | |
if (r) yield r; | |
} | |
var atLeastOneImage = false; | |
for (;;) { | |
if ($.avail < 1) yield $.Ensure(1); | |
var type = $.read1(); | |
// Extension block | |
if (type == 0x21) { | |
if ($.avail < 2) yield $.Ensure(2); | |
type = $.read1(); | |
len = $.read1(); | |
if (len == 0) { | |
// do nothing, per fx impl | |
} else if (type == 0xf9) { // gce | |
// again per fx impl, read max(len, 4) bytes, and process the first 4 | |
len = Math.max(len, 4); | |
if ($.avail < len+1) yield $.Ensure(len+1); | |
$.skip(1); // reversed, disposal method, user input, transparency | |
var dur = $.readU16(); | |
$.skip(len - 3); // transparency index, dummy padding | |
if (dur) | |
return $.FoundAnimation(); | |
len = $.read1(); | |
} else if (type == 0xff && len == 11) { | |
if ($.avail < 12) yield $.Ensure(12); | |
var ext = $.read(11), exts = ""; | |
for (var i = 0; i < 11; i++) | |
exts += String.fromCharCode(ext[i]); | |
len = $.read1(); | |
if (exts == "NETSCAPE2.0" || exts == "ANIMEXTS1.0") { | |
while (len) { | |
len = Math.max(len, 3); | |
if ($.avail < len+1) yield $.Ensure(len+1); | |
var subid = $.read1() & 0x7; | |
if (subid === 1) { | |
$.readU16(); // iteration count, 0 = inf | |
} else if (subid !== 2) { | |
return $.Error("invalid NETSCAPE subblock id"); | |
} | |
if (len > 3) $.skip(len - 3); // padding | |
len = $.read1(); | |
} | |
} | |
} | |
// Skip over trailing data and extensions that did not match anything | |
// above, per fx impl. | |
while (len) { | |
if ($.avail < len+1) yield $.Ensure(len+1); | |
$.skip(len); | |
len = $.read1(); | |
} | |
} | |
// Image | |
else if (type === 0x2c) { | |
if (atLeastOneImage) // per fx impl | |
return $.FoundAnimation(); | |
atLeastOneImage = true; | |
// Metadata | |
if ($.avail < 7) yield $.Ensure(7); | |
$.skip(4); // size | |
bits = $.read1(); // has lct, interlaced, (unused bits), lct size | |
$.skip(2); // background color, aspect ratio | |
// LCT | |
if (bits & 0x80) { | |
size = 3 << ((bits & 0x7) + 1); | |
r = $.longSkip(size); | |
if (r) yield r; | |
} | |
if ($.avail < 2) yield $.Ensure(2); | |
$.skip(1); // lzw data size | |
// Actual LZW-coded image, split into blocks. Skip it. | |
len = $.read1(); | |
while (len) { | |
r = $.longSkip(len); | |
if (r) yield r; | |
if ($.avail < 1) yield $.Ensure(1); | |
len = $.read1(); | |
} | |
} | |
// Trailer | |
else if (type == 0x3b) { | |
break; | |
} | |
// Unrecognized segment type | |
else { | |
// per fx impl, allow this error if we have found an image | |
if (atLeastOneImage) | |
break; | |
return $.Error("unrecognized segment type"); | |
} | |
} | |
// Trailing junk is OK according to imagelib. We're done here. | |
return $.Done(); | |
} | |
function setFilterHandlers(filter, url, decoder) { | |
var $ = { | |
avail: 0, | |
index: 0, | |
skipping: 0, | |
leftovers: null, | |
bytearray: null, | |
Ensure(n) { | |
return {type: 0, n, skip: false}; | |
}, | |
Error(msg) { | |
return {type: 1, msg}; | |
}, | |
FoundAnimation() { | |
return {type: 2}; | |
}, | |
Done() { | |
return {type: 3}; | |
}, | |
feed(arr) { | |
if (!arr.length) | |
return; | |
if (this.skipping) { | |
if (this.skipping >= arr.length) { | |
this.skipping -= arr.length; | |
return; | |
} | |
arr = arr.subarray(this.skipping); | |
this.skipping = 0; | |
} | |
if (this.bytearray != null) { | |
if (this.leftovers == null) { | |
if (this.avail != 0) | |
this.leftovers = this.bytearray.subarray(this.index); | |
} else { | |
var buffer = new Uint8Array(new ArrayBuffer(this.avail)); | |
buffer.set(this.leftovers); | |
buffer.set(this.bytearray.subarray(this.index), this.leftovers.length); | |
this.leftovers = buffer; | |
} | |
} | |
this.bytearray = arr; | |
this.index = 0; | |
this.avail += arr.length; | |
}, | |
longSkip(n) { | |
if (this.avail < n) { | |
n -= this.avail; | |
this.avail = this.index = 0; | |
this.leftovers = null; | |
this.bytearray = null; | |
this.skipping = n; | |
return this.Ensure(n); | |
} | |
$.skip(n); | |
return null; | |
}, | |
read(n) { | |
var ret; | |
this.avail -= n; | |
if (this.avail < 0) { | |
throw new Error("must ensure space before reading!"); | |
} | |
if (this.leftovers != null) { | |
if (n <= this.leftovers.length) { | |
ret = this.leftovers.subarray(0, n); | |
this.leftovers = (n === this.leftovers.length ? null : this.leftovers.subarray(n)); | |
} else { | |
this.index = n - this.leftovers.length; | |
ret = new Uint8Array(new ArrayBuffer(n)); | |
ret.set(this.leftovers); | |
ret.set(this.bytearray.subarray(0, this.index), this.leftovers.length); | |
this.leftovers = null; | |
} | |
} else { | |
this.index += n; | |
ret = this.bytearray.subarray(this.index - n, this.index); | |
} | |
return ret; | |
}, | |
skip(n) { | |
this.avail -= n; | |
if (this.avail < 0) { | |
throw new Error("must ensure space before reading!"); | |
} | |
if (this.leftovers != null) { | |
if (n < this.leftovers.length) { | |
this.leftovers = this.leftovers.subarray(n); | |
} else { | |
this.index = n - this.leftovers.length; | |
this.leftovers = null; | |
} | |
} else { | |
this.index += n; | |
} | |
}, | |
read1() { | |
var ret; | |
this.avail -= 1; | |
if (this.avail < 0) { | |
throw new Error("must ensure space before reading!"); | |
} | |
if (this.leftovers != null) { | |
ret = this.leftovers[0]; | |
this.leftovers = (1 === this.leftovers.length ? null : this.leftovers.subarray(1)); | |
} else { | |
ret = this.bytearray[this.index]; | |
this.index += 1; | |
} | |
return ret; | |
}, | |
readU16() { | |
var a = this.read1(); | |
var b = this.read1(); | |
return a | (b << 8); | |
}, | |
}; | |
var dec = decoder($); | |
var waitingFor = 0; | |
function done() { | |
filter.disconnect(); | |
$.leftovers = null; | |
$.bytearray = null; | |
} | |
filter.ondata = function handleData(event) { | |
// Gets fed with ~10 kB of data at a time. | |
let arr = event.data; | |
filter.write(arr); | |
if (!arr.length) return; | |
$.feed(arr); | |
while ($.avail >= waitingFor) { | |
var r = dec.next(); | |
var s = r.value; | |
if (s.type === 0) { | |
waitingFor = s.n; | |
if ($.avail >= waitingFor) | |
throw new Error("yielded despite being able to continue"); | |
} else if (s.type === 1) { | |
console.log("error in image decode: " + s.msg); | |
return done(); | |
} else if (s.type === 2) { | |
console.log("found animated image " + url); | |
return done(); | |
} else if (s.type === 3) { | |
// successful parse | |
return done(); | |
} else { | |
throw new Error("unknown event type " + s.type); | |
} | |
} | |
}; | |
// We *could* set filter.onstop here, and do some processing for cases | |
// with truncated GIFs. But I think imagelib accepts those, so we don't. | |
// Well, maybe the resulting image is an error one, but at least the | |
// parsing seems to succeed from what I can tell from the source. | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
(Slightly buggy, see https://github.com/simonlindholm/toggle-gifs/blob/web-ext/background.js for a more up-to-date version.)