UNPKG

gify-parse

Version:

An API for parsing information on (animated) GIF files.

267 lines (264 loc) 11.7 kB
/* * The MIT License (MIT) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ var jDataView = require('jdataview'); /* global console, jDataView, ArrayBuffer */ var gifyParse = (function () { 'use strict'; var defaultDelay = 100; function getPaletteSize(palette) { return (3 * Math.pow(2, 1 + bitToInt(palette.slice(5, 8)))); } function getBitArray(num) { var bits = []; for (var i = 7; i >= 0; i--) { bits.push(!!(num & (1 << i)) ? 1 : 0); } return bits; } function getDuration(duration) { return ((duration / 100) * 1000); } function bitToInt(bitArray) { return bitArray.reduce(function (s, n) { return s * 2 + n; }, 0); } function readSubBlock(view, pos, read) { var subBlock = { data: '', size: 0 }; while (true) { var size = view.getUint8(pos + subBlock.size, true); if (size === 0) { subBlock.size++; break; } if (read) { subBlock.data += view.getString(size, pos + subBlock.size + 1); } subBlock.size += size + 1; } return subBlock; } function getNewImage() { return { identifier: '0', localPalette: false, localPaletteSize: 0, interlace: false, comments: [], text: '', left: 0, top: 0, width: 0, height: 0, delay: 0, disposal: 0 }; } function getInfo(sourceArrayBuffer, quickPass) { var pos = 0, index = 0; var info = { valid: false, globalPalette: false, globalPaletteSize: 0, globalPaletteColorsRGB: [], loopCount: 0, height: 0, width: 0, animated: false, images: [], isBrowserDuration: false, duration: 0, durationIE: 0, durationSafari: 0, durationFirefox: 0, durationChrome: 0, durationOpera: 0 }; var view = new jDataView(sourceArrayBuffer); // needs to be at least 10 bytes long if (sourceArrayBuffer.byteLength < 10) { return info; } // GIF8 if ((view.getUint16(0) != 0x4749) || (view.getUint16(2) != 0x4638)) { return info; } //get width / height info.width = view.getUint16(6, true); info.height = view.getUint16(8, true); // not that safe to assume, but good enough by this point info.valid = true; // parse global palette var unpackedField = getBitArray(view.getUint8(10, true)); if (unpackedField[0]) { var globalPaletteSize = getPaletteSize(unpackedField); info.globalPalette = true; info.globalPaletteSize = (globalPaletteSize / 3); pos += globalPaletteSize; for (var i = 0; i < info.globalPaletteSize; i++) { var palettePos = 13 + i * 3; var r = view.getUint8(palettePos, true); //red var g = view.getUint8(palettePos + 1, true); //green var b = view.getUint8(palettePos + 2, true); //blue info.globalPaletteColorsRGB.push({r: r, g: g, b: b}); } } pos += 13; var image = getNewImage(); while (true) { try { var block = view.getUint8(pos, true); switch (block) { case 0x21: // EXTENSION BLOCK var type = view.getUint8(pos + 1, true); if (type === 0xF9) { //GRAPHICS CONTROL EXTENSION var length = view.getUint8(pos + 2); if (length === 4) { var delay = getDuration(view.getUint16(pos + 4, true)); if (delay < 60 && !info.isBrowserDuration) { info.isBrowserDuration = true; } // http://nullsleep.tumblr.com/post/16524517190/animated-gif-minimum-frame-delay-browser-compatibility (out of date) image.delay = delay; info.duration += delay; info.durationIE += (delay < 60) ? defaultDelay : delay; info.durationSafari += (delay < 20) ? defaultDelay : delay; info.durationChrome += (delay < 20) ? defaultDelay : delay; info.durationFirefox += (delay < 20) ? defaultDelay : delay; info.durationOpera += (delay < 20) ? defaultDelay : delay; // set disposal method unpackedField = getBitArray(view.getUint8(pos + 3)); var disposal = unpackedField.slice(3, 6).join(''); image.disposal = parseInt(disposal, 2); pos += 8; } else { pos++; } } else { pos += 2; var subBlock = readSubBlock(view, pos, true); switch (type) { case 0xFF: //APPLICATION EXTENSION /* since multiple application extension blocks can occur, we need to make sure we're only setting the loop count when the identifer is NETSCAPE */ var identifier = view.getString(8, pos + 1); if (identifier === 'NETSCAPE') { info.loopCount = view.getUint8(pos + 14, true); } break; case 0xCE: //NAME /* the only reference to this extension I could find was in gifsicle. I'm not sure if this is something gifsicle just made up or if this actually exists outside of this app */ image.identifier = subBlock.data; break; case 0xFE: //COMMENT EXTENSION image.comments.push(subBlock.data); break; case 0x01: //PLAIN TEXT EXTENSION image.text = subBlock.data; break; } pos += subBlock.size; } break; case 0x2C: // IMAGE DESCRIPTOR image.left = view.getUint16(pos + 1, true); image.top = view.getUint16(pos + 3, true); image.width = view.getUint16(pos + 5, true); image.height = view.getUint16(pos + 7, true); unpackedField = getBitArray(view.getUint8(pos + 9, true)); if (unpackedField[0]) { // local palette? var localPaletteSize = getPaletteSize(unpackedField); image.localPalette = true; image.localPaletteSize = (localPaletteSize / 3); pos += localPaletteSize; } if (unpackedField[1]) { // interlaced? image.interlace = true; } // add image & reset object info.images.push(image); index++; //create new image image = getNewImage(); image.identifier = index.toString(); // set animated flag if (info.images.length > 1 && !info.animated) { info.animated = true; // quickly bail if the gif has more than one image if (quickPass) { return info; } } pos += 11; subBlock = readSubBlock(view, pos, false); pos += subBlock.size; break; case 0x3B: // TRAILER BLOCK (THE END) return info; default: // UNKNOWN BLOCK (bad) pos++; break; } } catch (e) { info.valid = false; return info; } // this shouldn't happen, but if the trailer block is missing, we should bail at EOF if ((pos) >= sourceArrayBuffer.byteLength) { return info; } } } return { /** * Parses the GIF information from the given ArrayBuffer and returns true if the GIF is animated. * * @param sourceArrayBuffer * @returns {boolean} */ isAnimated: function (sourceArrayBuffer) { var info = getInfo(sourceArrayBuffer, true); return info.animated; }, /** * Parses the GIF information from the given ArrayBuffer and creates an information object. * * @param sourceArrayBuffer * @returns {{valid: boolean, globalPalette: boolean, globalPaletteSize: number, loopCount: number, height: number, width: number, animated: boolean, images: Array, isBrowserDuration: boolean, duration: number, durationIE: number, durationSafari: number, durationFirefox: number, durationChrome: number, durationOpera: number}} */ getInfo: function (sourceArrayBuffer) { return getInfo(sourceArrayBuffer, false); } }; })(); module.exports = gifyParse;