Skip to content

Instantly share code, notes, and snippets.

@yurydelendik
Created November 12, 2014 17:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yurydelendik/02c61763bf9b0631b820 to your computer and use it in GitHub Desktop.
Save yurydelendik/02c61763bf9b0631b820 to your computer and use it in GitHub Desktop.
APNG builder from canvas
var APNGBuilder = (function () {
function convertImageToByteArray(canvas) {
var url = canvas.toDataURL('image/png');
var i = url.indexOf('base64,');
if (i < 0) {
throw new Error('invalid image url');
}
var data = atob(url.substring(i + 7));
var arr = new Uint8Array(data.length);
for (var i = 0; i < data.length; i++) {
arr[i] = data.charCodeAt(i) & 0xFF;
}
return arr;
}
function convertImageDataToDataURL(chunks) {
var s = '';
chunks.forEach(function (item) {
for (var i = 0; i < item.length; i++) {
s += String.fromCharCode(item[i]);
}
});
return 'data:image/png;base64,' + btoa(s);
}
function indexPngData(data) {
if (data[0] != 137 || data[1] != 80 || data[2] != 78 || data[3] != 71 ||
data[4] != 13 || data[5] != 10 || data[6] != 26 || data[7] != 10) {
throw new Error('invalid png header');
}
var pos = 8;
var chunks = [];
while (pos < data.length) {
var chunk = {};
chunk.offset = pos;
chunk.dataLength = ((data[pos] << 24) | (data[pos + 1] << 16) |
(data[pos + 2] << 8) | data[pos + 3]) >>> 0;
pos += 4;
chunk.type = String.fromCharCode(data[pos], data[pos + 1],
data[pos + 2], data[pos + 3]);
pos += 4;
chunk.dataOffset = pos;
chunk.data = data.subarray(pos, chunk.dataLength + pos);
pos += chunk.dataLength;
chunk.crc = (data[pos] << 24) | (data[pos + 1] << 16) |
(data[pos + 2] << 8) | data[pos + 3];
pos += 4;
chunk.length = pos - chunk.offset;
chunk.fragment = data.subarray(chunk.offset, pos);
chunks.push(chunk);
if (chunk.type === 'IEND') {
break;
}
}
return chunks;
}
var crcTable = new Int32Array(256);
for (var i = 0; i < 256; i++) {
var c = i;
for (var h = 0; h < 8; h++) {
if (c & 1) {
c = 0xedB88320 ^ ((c >> 1) & 0x7fffffff);
} else {
c = (c >> 1) & 0x7fffffff;
}
}
crcTable[i] = c;
}
function crc32(data, start, end) {
var crc = -1;
for (var i = start; i < end; i++) {
var a = (crc ^ data[i]) & 0xff;
var b = crcTable[a];
crc = (crc >>> 8) ^ b;
}
return crc ^ -1;
}
function createInt32(n) {
return [(n >> 24) & 0xFF, (n >> 16) & 0xFF, (n >> 8) & 0xFF, n & 0xFF];
}
function createChunk(tag, data) {
var dataLength = 0;
for (var i = 1; i < arguments.length; i++) {
dataLength += arguments[i].length;
}
var bytes = new Uint8Array(dataLength + 12);
bytes[0] = (dataLength >> 24) & 0xFF;
bytes[1] = (dataLength >> 16) & 0xFF;
bytes[2] = (dataLength >> 8) & 0xFF;
bytes[3] = dataLength & 0xFF;
bytes[4] = tag.charCodeAt(0) & 0xFF;
bytes[5] = tag.charCodeAt(1) & 0xFF;
bytes[6] = tag.charCodeAt(2) & 0xFF;
bytes[7] = tag.charCodeAt(3) & 0xFF;
var pos = 8;
for (var i = 1; i < arguments.length; i++) {
for (var j = 0; j < arguments[i].length; j++) {
bytes[pos++] = arguments[i][j];
}
}
var crc = crc32(bytes, 4, pos);
bytes[pos] = (crc >> 24) & 0xFF;
bytes[pos + 1] = (crc >> 16) & 0xFF;
bytes[pos + 2] = (crc >> 8) & 0xFF;
bytes[pos + 3] = crc & 0xFF;
return bytes;
}
var MIN_LOOP_DELAY = 3000;
function APNGBuilder() {
this.frames = [];
}
APNGBuilder.prototype = {
addFrame: function (offset, canvas) {
var frame = {};
frame.data = convertImageToByteArray(canvas);
frame.index = indexPngData(frame.data);
frame.idats = frame.index.filter(function (item) {
return item.type === 'IDAT';
});
frame.offset = offset;
frame.width = canvas.width;
frame.height = canvas.height;
this.frames.push(frame);
},
toDataURL: function () {
if (this.frames.length === 0) {
throw new Error('no frames');
}
if (this.frames.length === 1) {
return convertImageDataToDataURL([this.frames.data]);
}
var result = [];
var firstFrame = this.frames[0];
var firstIDAT = firstFrame.idats[0];
if (!firstIDAT) {
throw new Error('cannot find IDAT');
}
result.push(firstFrame.data.subarray(0, firstIDAT.offset));
result.push(createChunk('acTL',
createInt32(this.frames.length),
createInt32(0)));
var delay = this.frames[1].offset - firstFrame.offset;
var seq = 0;
result.push(createChunk('fcTL',
createInt32(seq++),
createInt32(firstFrame.width),
createInt32(firstFrame.height),
createInt32(0),
createInt32(0),
[(delay >> 8) & 0xFF, delay & 0xFF, 0x03, 0xE8],
[1, 0]));
firstFrame.idats.forEach(function (idat) {
result.push(idat.fragment);
});
for (var i = 1; i < this.frames.length; i++) {
var frame = this.frames[i];
if (i === this.frames.length - 1) {
delay = Math.max(MIN_LOOP_DELAY, firstFrame.offset);
} else {
delay = this.frames[i + 1].offset - frame.offset;
}
result.push(createChunk('fcTL',
createInt32(seq++),
createInt32(frame.width),
createInt32(frame.height),
createInt32(0),
createInt32(0),
[(delay >> 8) & 0xFF, delay & 0xFF, 0x03, 0xE8],
[1, 0]));
frame.idats.forEach(function (idat) {
result.push(createChunk('fdAT',
createInt32(seq++),
idat.data));
});
}
result.push(createChunk('IEND'));
return convertImageDataToDataURL(result);
}
};
return APNGBuilder;
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment