Skip to content

Instantly share code, notes, and snippets.

@lahmatiy
Last active May 17, 2021 17:07
Show Gist options
  • Save lahmatiy/1460bd0a1b8812072cfea1bb55d94142 to your computer and use it in GitHub Desktop.
Save lahmatiy/1460bd0a1b8812072cfea1bb55d94142 to your computer and use it in GitHub Desktop.
const EXTENSION_TYPE = {
0x01: 'PlainText',
0xF9: 'GraphicControl',
0xFE: 'Comment',
0xFF: 'Application'
};
/**
* Returns total length of data blocks sequence
*
* @param {Buffer} buffer
* @param {number} offset
* @returns {number}
*/
function getBlockLength(buffer, offset) {
let length = 0;
while (buffer[offset + length] !== 0) {
length += buffer[offset + length] + 1;
}
return length + 1;
}
/**
* Checks if buffer contains a GIF image
*
* @param {Buffer} buffer
* @returns {boolean}
*/
function isGIF(buffer) {
const header = buffer.slice(0, 6).toString('ascii');
return header === 'GIF87a' || header === 'GIF89a';
}
/**
* Convert an animated GIF into a static
*
* @param {Buffer} input A GIF image
* @returns {Buffer} Converted GIF
*/
module.exports = function makeGifStatic(input) {
/* eslint-disable require-jsdoc, no-use-before-define */
function write(bytes) {
input.copy(output, outputOffset, offset, offset + bytes);
offset += bytes;
outputOffset += bytes;
}
// Check if this is this image has valid GIF header.
// If not return false. Chrome, FF and IE doesn't handle GIFs with invalid version.
if (!isGIF(input)) {
return input.slice();
}
const output = Buffer.alloc(input.length);
let outputOffset = 0;
let offset = 0;
let hasColorTable;
let colorTableSize;
let imagesCount = 0;
// header, logical screen descriptor and global color table
write(6 + 7); // 6 bytes header + 7 bytes screen
hasColorTable = input[6 + 4] & 0x80; // 0b10000000
colorTableSize = input[6 + 4] & 0x07; // 0b00000111
if (hasColorTable) {
write(3 * Math.pow(2, colorTableSize + 1));
}
while (offset < input.length) {
switch (input[offset]) {
// Extension block introducer
case 0x21: {
const label = input[offset + 1];
const type = EXTENSION_TYPE[label] || 'Unknown';
const dataLength = 2 + getBlockLength(input, offset + 2);
if (type === 'GraphicControl' || type === 'Application') {
write(dataLength);
} else {
offset += dataLength;
}
break;
}
// Image descriptor block. According to specification there could be any
// number of these blocks (even zero). When there is more than one image
// descriptor browsers will display animation (they shouldn't when there
// is no delays defined, but they do it anyway).
case 0x2C: {
let dataLength = 11;
hasColorTable = input[offset + 9] & 0x80; // 0b10000000
colorTableSize = input[offset + 9] & 0x07; // 0b00000111
if (hasColorTable) {
dataLength += 3 * Math.pow(2, colorTableSize + 1);
}
dataLength += getBlockLength(input, offset + dataLength);
if (imagesCount === 0) {
write(dataLength);
} else {
offset += dataLength;
}
imagesCount++;
break;
}
// Stop processing on trailer block,
// all data after this point will is ignored by decoders
case 0x3B:
output[outputOffset++] = 0x3B;
offset = input.length;
break;
// Oops! This GIF seems to be invalid
default:
offset = input.length; // fast forward to end of input
break;
}
}
return output.slice(0, outputOffset);
};
var fs = require('fs');
var animated = fs.readFileSync('./test.gif');
var static = makeGifStatic(animated);
fs.writeFileSync('./test-static.gif', static);
@PitBeast
Copy link

PitBeast commented Sep 6, 2018

3 * Math.pow(2, colorTableSize + 1) is equal to 3 * 2 << colorTableSize and 6 << colorTableSize for non-negative integer colorTableSize

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment