├── .gitignore ├── docs ├── img.gif └── getting-started.js ├── test ├── test-files │ ├── medium-size.png │ ├── checkerboard-pixels.json │ ├── inverse-checkerboard-pixels.json │ └── generate.html ├── expected-files │ ├── checkerboard.gif │ ├── moving-dot.gif │ ├── checkerboard-indexed.gif │ └── alternating-checkerboard.gif ├── utils │ └── image.js ├── gif-encoder_benchmark.js └── gif-encoder_test.js ├── .travis.yml ├── Gruntfile.js ├── CHANGELOG.md ├── UNLICENSE ├── package.json ├── lib ├── LZWEncoder.js ├── NeuQuant.js ├── TypedNeuQuant.js └── GIFEncoder.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | test/actual-files/ 3 | -------------------------------------------------------------------------------- /docs/img.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twolfson/gif-encoder/HEAD/docs/img.gif -------------------------------------------------------------------------------- /test/test-files/medium-size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twolfson/gif-encoder/HEAD/test/test-files/medium-size.png -------------------------------------------------------------------------------- /test/expected-files/checkerboard.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twolfson/gif-encoder/HEAD/test/expected-files/checkerboard.gif -------------------------------------------------------------------------------- /test/expected-files/moving-dot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twolfson/gif-encoder/HEAD/test/expected-files/moving-dot.gif -------------------------------------------------------------------------------- /test/expected-files/checkerboard-indexed.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twolfson/gif-encoder/HEAD/test/expected-files/checkerboard-indexed.gif -------------------------------------------------------------------------------- /test/expected-files/alternating-checkerboard.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twolfson/gif-encoder/HEAD/test/expected-files/alternating-checkerboard.gif -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | - "8" 5 | - "6" 6 | 7 | notifications: 8 | email: 9 | recipients: 10 | - todd@twolfson.com 11 | on_success: change 12 | on_failure: change 13 | -------------------------------------------------------------------------------- /docs/getting-started.js: -------------------------------------------------------------------------------- 1 | // Create a 10 x 10 gif 2 | var GifEncoder = require('../'); // #DEVONLY 3 | // var GifEncoder = require('gif-encoder'); // #DEVONLY 4 | var gif = new GifEncoder(10, 10); 5 | 6 | // using an rgba array of pixels [r, g, b, a, ... continues on for every pixel] 7 | // This can be collected from a via context.getImageData(0, 0, width, height).data 8 | // var pixels = [0, 0, 0, 255/*, ...*/]; // #DEVONLY 9 | var pixels = require('../test/test-files/checkerboard-pixels.json'); 10 | 11 | // Collect output 12 | var file = require('fs').createWriteStream(__dirname + '/img.gif'); // #DEVONLY 13 | // var file = require('fs').createWriteStream('img.gif'); // #DEVONLY 14 | gif.pipe(file); 15 | 16 | // Write out the image into memory 17 | gif.writeHeader(); 18 | gif.addFrame(pixels); 19 | // gif.addFrame(pixels); // Write subsequent rgba arrays for more frames 20 | gif.finish(); -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | // Project configuration. 4 | grunt.initConfig({ 5 | jshint: { 6 | files: ['Gruntfile.js', 'lib/**/*.js', 'test/**/*.js'], 7 | options: { 8 | curly: true, 9 | eqeqeq: true, 10 | immed: true, 11 | latedef: true, 12 | newcap: true, 13 | noarg: true, 14 | sub: true, 15 | undef: true, 16 | boss: true, 17 | eqnull: true, 18 | node: true, 19 | 20 | strict: false, 21 | globals: { 22 | exports: true, 23 | describe: true, 24 | before: true, 25 | it: true 26 | } 27 | } 28 | }, 29 | watch: { 30 | 'default': { 31 | files: '<%= jshint.files %>', 32 | tasks: ['default'] 33 | } 34 | } 35 | }); 36 | 37 | // Load in grunt tasks 38 | grunt.loadNpmTasks('grunt-contrib-jshint'); 39 | grunt.loadNpmTasks('grunt-contrib-watch'); 40 | 41 | // Default task. 42 | grunt.registerTask('default', ['jshint']); 43 | 44 | }; 45 | -------------------------------------------------------------------------------- /test/utils/image.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var getPixels = require('get-pixels'); 3 | 4 | exports.load = function (filename) { 5 | before(function loadImage (done) { 6 | var that = this; 7 | getPixels(__dirname + '/../test-files/' + filename, function (err, pixels) { 8 | if (err) { 9 | return done(err); 10 | } 11 | that.pixels = pixels; 12 | done(); 13 | }); 14 | }); 15 | }; 16 | 17 | exports.debug = function (filename) { 18 | if (process.env.DEBUG_TEST) { 19 | before(function saveDebugImage () { 20 | try { fs.mkdirSync(__dirname + '/../actual-files/'); } catch (e) {} 21 | fs.writeFileSync(__dirname + '/../actual-files/' + filename, this.gifData, 'binary'); 22 | }); 23 | } 24 | 25 | if (false && process.env.TRAVIS) { 26 | before(function outputDebugImage () { 27 | console.log(encodeURIComponent(this.gifData)); 28 | // Counter to it: 29 | // var fs = require('fs'); 30 | // var data = "<%= data %>"; 31 | // fs.writeFileSync('tmp.gif', decodeURIComponent(data), 'binary'); 32 | }); 33 | } 34 | }; -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # gif-encoder changelog 2 | 0.7.2 - Removed accidental length requirement documentation. Fixes #18 3 | 4 | 0.7.1 - Fixed supported Node.js versions and Travis CI 5 | 6 | 0.7.0 - Added support for palette and indexed pixels via @nurpax in #15 7 | 8 | 0.6.1 - Replaced Gratipay with support me page 9 | 10 | 0.6.0 - Repaired transparency support via @scinos in #7 11 | 12 | 0.5.0 - Dropped Node.js@0.8.0 support and updated Travis CI versions 13 | 14 | 0.4.3 - Removed global variable leak via @mattbierner in #6 15 | 16 | 0.4.2 - Added foundry for releases 17 | 18 | 0.4.1 - Introducing performance comparison between byte by byte buffers vs frame based buffers. 19 | 20 | 0.4.0 - Broke down `getImagePixels` into `removeAlphaChannel` and `setImagePixels` 21 | 22 | 0.3.0 - Moved to readable-stream for streams1/streams2 functionality 23 | 24 | 0.2.0 - Moved to larger chunked data events 25 | 26 | 0.1.0 - Moved to stream-like API (copied from gifsockets-server#f385006) 27 | 28 | 0.0.3 - Added basic info to README 29 | 30 | 0.0.2 - Added test for original GIFEncoder functionality 31 | 32 | 0.0.1 - Initial fork from gif.js#faee238 33 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /test/test-files/checkerboard-pixels.json: -------------------------------------------------------------------------------- 1 | [255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255] 2 | -------------------------------------------------------------------------------- /test/test-files/inverse-checkerboard-pixels.json: -------------------------------------------------------------------------------- 1 | [0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255,0,0,0,255] 2 | -------------------------------------------------------------------------------- /test/gif-encoder_benchmark.js: -------------------------------------------------------------------------------- 1 | var imageUtils = require('./utils/image'); 2 | var GifEncoder = require('../'); 3 | 4 | describe('A GifEncoder', function () { 5 | // DEV: With byte-by-byte buffers, we run into a process.nextTick overflow with all the events 6 | // Thus, frame based buffers win by default 7 | describe('encoding a bunch of frames to `data` events', function () { 8 | before(function createGifEncoder () { 9 | this.gif = new GifEncoder(200, 200); 10 | this.gif.writeHeader(); 11 | 12 | // Pipe output to nowhere 13 | // DEV: We should test .read() but we already have frame based buffers as our winner 14 | this.gif.on('data', function () {}); 15 | }); 16 | imageUtils.load('medium-size.png'); 17 | before(function encodeABunchOfFrames () { 18 | var startTime = Date.now(); 19 | var i = 500; 20 | var gif = this.gif; 21 | var pixels = this.pixels; 22 | while (i--) { 23 | gif.addFrame(pixels); 24 | } 25 | var endTime = Date.now(); 26 | this.totalTime = endTime - startTime; 27 | }); 28 | 29 | it('can do so efficiently', function () { 30 | // DEV: We should move to ops/second for other benchmarks but this test has a win-by-default 31 | // Medium size x 500 frames 13088 ms for frame based buffers 32 | // Medium size x 500 frames 14245 ms for byte by byte 33 | console.log('Medium size x 500 frames', this.totalTime + ' ms'); 34 | }); 35 | }); 36 | }); -------------------------------------------------------------------------------- /test/test-files/generate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GIF input data generator 5 | 10 | 11 | 12 | 13 | 47 | 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gif-encoder", 3 | "description": "Streaming GIF encoder", 4 | "version": "0.7.2", 5 | "homepage": "https://github.com/twolfson/gif-encoder", 6 | "author": { 7 | "name": "Todd Wolfson", 8 | "email": "todd@twolfson.com", 9 | "url": "http://twolfson.com/" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/twolfson/gif-encoder.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/twolfson/gif-encoder/issues" 17 | }, 18 | "licenses": [ 19 | { 20 | "type": "MIT", 21 | "url": "https://github.com/twolfson/gif-encoder/blob/master/LICENSE-MIT" 22 | } 23 | ], 24 | "main": "lib/GIFEncoder", 25 | "engines": { 26 | "node": ">= 6.0.0" 27 | }, 28 | "scripts": { 29 | "test": "npm run test-interface && npm run test-performance", 30 | "test-interface": "mocha test/gif-encoder_test.js", 31 | "test-performance": "mocha test/gif-encoder_benchmark.js" 32 | }, 33 | "dependencies": { 34 | "readable-stream": "~1.1.9" 35 | }, 36 | "devDependencies": { 37 | "async": "~0.2.9", 38 | "foundry": "~4.3.2", 39 | "foundry-release-git": "~2.0.2", 40 | "foundry-release-npm": "~2.0.2", 41 | "get-pixels": "~1.0.1", 42 | "grunt": "~0.4.1", 43 | "grunt-contrib-jshint": "~0.6.0", 44 | "grunt-contrib-watch": "~0.4.0", 45 | "mocha": "~1.11.0", 46 | "ndarray-fill": "~1.0.1", 47 | "zeros": "~1.0.0" 48 | }, 49 | "keywords": [ 50 | "gif", 51 | "encode", 52 | "encoder" 53 | ], 54 | "foundry": { 55 | "releaseCommands": [ 56 | "foundry-release-git", 57 | "foundry-release-npm" 58 | ] 59 | } 60 | } -------------------------------------------------------------------------------- /lib/LZWEncoder.js: -------------------------------------------------------------------------------- 1 | /* 2 | LZWEncoder.js 3 | 4 | Authors 5 | Kevin Weiner (original Java version - kweiner@fmsware.com) 6 | Thibault Imbert (AS3 version - bytearray.org) 7 | Johan Nordberg (JS version - code@johan-nordberg.com) 8 | 9 | Acknowledgements 10 | GIFCOMPR.C - GIF Image compression routines 11 | Lempel-Ziv compression based on 'compress'. GIF modifications by 12 | David Rowley (mgardi@watdcsu.waterloo.edu) 13 | GIF Image compression - modified 'compress' 14 | Based on: compress.c - File compression ala IEEE Computer, June 1984. 15 | By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas) 16 | Jim McKie (decvax!mcvax!jim) 17 | Steve Davies (decvax!vax135!petsd!peora!srd) 18 | Ken Turkowski (decvax!decwrl!turtlevax!ken) 19 | James A. Woods (decvax!ihnp4!ames!jaw) 20 | Joe Orost (decvax!vax135!petsd!joe) 21 | */ 22 | 23 | var EOF = -1; 24 | var BITS = 12; 25 | var HSIZE = 5003; // 80% occupancy 26 | var masks = [0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 27 | 0x003F, 0x007F, 0x00FF, 0x01FF, 0x03FF, 0x07FF, 28 | 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF]; 29 | 30 | function LZWEncoder(width, height, pixels, colorDepth) { 31 | var initCodeSize = Math.max(2, colorDepth); 32 | 33 | var accum = new Uint8Array(256); 34 | var htab = new Int32Array(HSIZE); 35 | var codetab = new Int32Array(HSIZE); 36 | 37 | var cur_accum, cur_bits = 0; 38 | var a_count; 39 | var free_ent = 0; // first unused entry 40 | var maxcode; 41 | var remaining; 42 | var curPixel; 43 | var n_bits; 44 | 45 | // block compression parameters -- after all codes are used up, 46 | // and compression rate changes, start over. 47 | var clear_flg = false; 48 | 49 | // Algorithm: use open addressing double hashing (no chaining) on the 50 | // prefix code / next character combination. We do a variant of Knuth's 51 | // algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime 52 | // secondary probe. Here, the modular division first probe is gives way 53 | // to a faster exclusive-or manipulation. Also do block compression with 54 | // an adaptive reset, whereby the code table is cleared when the compression 55 | // ratio decreases, but after the table fills. The variable-length output 56 | // codes are re-sized at this point, and a special CLEAR code is generated 57 | // for the decompressor. Late addition: construct the table according to 58 | // file size for noticeable speed improvement on small files. Please direct 59 | // questions about this implementation to ames!jaw. 60 | var g_init_bits, ClearCode, EOFCode; 61 | 62 | // Add a character to the end of the current packet, and if it is 254 63 | // characters, flush the packet to disk. 64 | function char_out(c, outs) { 65 | accum[a_count++] = c; 66 | if (a_count >= 254) flush_char(outs); 67 | } 68 | 69 | // Clear out the hash table 70 | // table clear for block compress 71 | function cl_block(outs) { 72 | cl_hash(HSIZE); 73 | free_ent = ClearCode + 2; 74 | clear_flg = true; 75 | output(ClearCode, outs); 76 | } 77 | 78 | // Reset code table 79 | function cl_hash(hsize) { 80 | for (var i = 0; i < hsize; ++i) htab[i] = -1; 81 | } 82 | 83 | function compress(init_bits, outs) { 84 | var fcode, c, i, ent, disp, hsize_reg, hshift; 85 | 86 | // Set up the globals: g_init_bits - initial number of bits 87 | g_init_bits = init_bits; 88 | 89 | // Set up the necessary values 90 | clear_flg = false; 91 | n_bits = g_init_bits; 92 | maxcode = MAXCODE(n_bits); 93 | 94 | ClearCode = 1 << (init_bits - 1); 95 | EOFCode = ClearCode + 1; 96 | free_ent = ClearCode + 2; 97 | 98 | a_count = 0; // clear packet 99 | 100 | ent = nextPixel(); 101 | 102 | hshift = 0; 103 | for (fcode = HSIZE; fcode < 65536; fcode *= 2) ++hshift; 104 | hshift = 8 - hshift; // set hash code range bound 105 | hsize_reg = HSIZE; 106 | cl_hash(hsize_reg); // clear hash table 107 | 108 | output(ClearCode, outs); 109 | 110 | outer_loop: while ((c = nextPixel()) != EOF) { 111 | fcode = (c << BITS) + ent; 112 | i = (c << hshift) ^ ent; // xor hashing 113 | if (htab[i] === fcode) { 114 | ent = codetab[i]; 115 | continue; 116 | } else if (htab[i] >= 0) { // non-empty slot 117 | disp = hsize_reg - i; // secondary hash (after G. Knott) 118 | if (i === 0) disp = 1; 119 | do { 120 | if ((i -= disp) < 0) i += hsize_reg; 121 | if (htab[i] === fcode) { 122 | ent = codetab[i]; 123 | continue outer_loop; 124 | } 125 | } while (htab[i] >= 0); 126 | } 127 | output(ent, outs); 128 | ent = c; 129 | if (free_ent < 1 << BITS) { 130 | codetab[i] = free_ent++; // code -> hashtable 131 | htab[i] = fcode; 132 | } else { 133 | cl_block(outs); 134 | } 135 | } 136 | 137 | // Put out the final code. 138 | output(ent, outs); 139 | output(EOFCode, outs); 140 | } 141 | 142 | function encode(outs) { 143 | outs.writeByte(initCodeSize); // write "initial code size" byte 144 | remaining = width * height; // reset navigation variables 145 | curPixel = 0; 146 | compress(initCodeSize + 1, outs); // compress and write the pixel data 147 | outs.writeByte(0); // write block terminator 148 | } 149 | 150 | // Flush the packet to disk, and reset the accumulator 151 | function flush_char(outs) { 152 | if (a_count > 0) { 153 | outs.writeByte(a_count); 154 | outs.writeBytes(accum, 0, a_count); 155 | a_count = 0; 156 | } 157 | } 158 | 159 | function MAXCODE(n_bits) { 160 | return (1 << n_bits) - 1; 161 | } 162 | 163 | // Return the next pixel from the image 164 | function nextPixel() { 165 | if (remaining === 0) return EOF; 166 | --remaining; 167 | var pix = pixels[curPixel++]; 168 | return pix & 0xff; 169 | } 170 | 171 | function output(code, outs) { 172 | cur_accum &= masks[cur_bits]; 173 | 174 | if (cur_bits > 0) cur_accum |= (code << cur_bits); 175 | else cur_accum = code; 176 | 177 | cur_bits += n_bits; 178 | 179 | while (cur_bits >= 8) { 180 | char_out((cur_accum & 0xff), outs); 181 | cur_accum >>= 8; 182 | cur_bits -= 8; 183 | } 184 | 185 | // If the next entry is going to be too big for the code size, 186 | // then increase it, if possible. 187 | if (free_ent > maxcode || clear_flg) { 188 | if (clear_flg) { 189 | maxcode = MAXCODE(n_bits = g_init_bits); 190 | clear_flg = false; 191 | } else { 192 | ++n_bits; 193 | if (n_bits == BITS) maxcode = 1 << BITS; 194 | else maxcode = MAXCODE(n_bits); 195 | } 196 | } 197 | 198 | if (code == EOFCode) { 199 | // At EOF, write the rest of the buffer. 200 | while (cur_bits > 0) { 201 | char_out((cur_accum & 0xff), outs); 202 | cur_accum >>= 8; 203 | cur_bits -= 8; 204 | } 205 | flush_char(outs); 206 | } 207 | } 208 | 209 | this.encode = encode; 210 | } 211 | 212 | module.exports = LZWEncoder; 213 | -------------------------------------------------------------------------------- /test/gif-encoder_test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var fs = require('fs'); 3 | 4 | var async = require('async'); 5 | var ndarrayFill = require('ndarray-fill'); 6 | var zeros = require('zeros'); 7 | 8 | var GifEncoder = require('../lib/GIFEncoder.js'); 9 | var checkerboardPixels = require('./test-files/checkerboard-pixels.json'); 10 | 11 | function createGif(height, width, options) { 12 | before(function () { 13 | this.gif = new GifEncoder(height, width, options); 14 | }); 15 | } 16 | 17 | describe('GifEncoder encoding a checkerboard', function () { 18 | createGif(10, 10); 19 | before(function () { 20 | this.gif.writeHeader(); 21 | this.gif.addFrame(checkerboardPixels); 22 | this.gif.finish(); 23 | }); 24 | before(function (done) { 25 | this.gif.once('readable', done); 26 | }); 27 | 28 | it('generates the expected bytes', function () { 29 | // TODO: Output canvas to a file, perceptual diff GIF to canvas output =3 30 | // Grab the expected and actual content 31 | var expectedBytes = fs.readFileSync(__dirname + '/expected-files/checkerboard.gif'); 32 | var actualBytes = this.gif.read(); 33 | 34 | // DEV: Write out actual file to expected file 35 | if (process.env.DEBUG_TEST) { 36 | try { fs.mkdirSync(__dirname + '/actual-files'); } catch (e) {} 37 | fs.writeFileSync(__dirname + '/actual-files/checkerboard.gif', actualBytes); 38 | } 39 | 40 | // Assert the expected matches the actual content 41 | assert.deepEqual(expectedBytes, actualBytes); 42 | }); 43 | }); 44 | 45 | describe('GifEncoder encoding a multi-framed checkerboard', function () { 46 | createGif(10, 10); 47 | before(function () { 48 | this.gif.writeHeader(); 49 | this.gif.setDelay(500); 50 | this.gif.setRepeat(0); 51 | this.gif.addFrame(checkerboardPixels); 52 | this.gif.addFrame(require('./test-files/inverse-checkerboard-pixels.json')); 53 | this.gif.finish(); 54 | }); 55 | before(function (done) { 56 | this.gif.once('readable', done); 57 | }); 58 | 59 | it('generates the expected bytes', function () { 60 | var expectedBytes = fs.readFileSync(__dirname + '/expected-files/alternating-checkerboard.gif'); 61 | var actualBytes = this.gif.read(); 62 | if (process.env.DEBUG_TEST) { 63 | try { fs.mkdirSync(__dirname + '/actual-files'); } catch (e) {} 64 | fs.writeFileSync(__dirname + '/actual-files/alternating-checkerboard.gif', actualBytes); 65 | } 66 | assert.deepEqual(expectedBytes, actualBytes); 67 | }); 68 | }); 69 | 70 | describe('GifEncoder encoding an overly large, underly read checkerboard', function () { 71 | createGif(10, 10); 72 | before(function (done) { 73 | var that = this; 74 | this.gif.writeHeader(); 75 | this.gif.on('error', function saveError (err) { 76 | that.error = err; 77 | }); 78 | 79 | // Write out a new frame until we encounter an error 80 | // DEV: This is async so mocha can time us out 81 | async.until(function errorHasOccurred () { 82 | return that.error; 83 | }, function addNewFrame(cb) { 84 | process.nextTick(function () { 85 | that.gif.addFrame(checkerboardPixels); 86 | cb(); 87 | }); 88 | }, done); 89 | }); 90 | 91 | it('emits an error', function () { 92 | assert.notEqual(this.error, undefined); 93 | }); 94 | }); 95 | 96 | describe('GifEncoder encoding a multi-framed image with a transparent background', function () { 97 | createGif(15, 15); 98 | before(function () { 99 | this.gif.writeHeader(); 100 | this.gif.setDelay(500); 101 | this.gif.setTransparent(0xFF00FF); 102 | this.gif.setRepeat(0); 103 | var i = 0; 104 | for (; i < 3; i++) { 105 | // DEV: We are drawing a diagonally moving dot 106 | // +------+ +------+ +------+ 107 | // |xx | | | | | 108 | // |xx | | | | | 109 | // | | -> | xx | -> | | 110 | // | | -> | xx | -> | | 111 | // | | | | | xx| 112 | // | | | | | xx| 113 | // +------+ +------+ +------+ 114 | 115 | // Generate our frame 116 | var frameNdarray = zeros([15, 15, 4]); 117 | ndarrayFill(frameNdarray, function fillFrame (x, y, rgbaIndex) { 118 | // If this is the alpha channel, always return it as full 119 | if (rgbaIndex === 3) { 120 | return 0xFF; 121 | } 122 | 123 | // Otherwise, if we are on our black dot, then draw it 124 | if (i * 5 <= x && x < (i + 1) * 5 && 125 | i * 5 <= y && y < (i + 1) * 5) { 126 | // Generate black dot (00 00 00) 127 | return 0x00; 128 | // Otherwise, draw our transparent color 129 | } else { 130 | return rgbaIndex === 1 ? 0x00 : 0xFF; 131 | } 132 | }); 133 | 134 | // Draw our frame 135 | this.gif.addFrame(frameNdarray.data); 136 | } 137 | this.gif.finish(); 138 | }); 139 | before(function (done) { 140 | this.gif.once('readable', done); 141 | }); 142 | 143 | it('generates the expected bytes', function () { 144 | var expectedBytes = fs.readFileSync(__dirname + '/expected-files/moving-dot.gif'); 145 | var actualBytes = this.gif.read(); 146 | if (process.env.DEBUG_TEST) { 147 | try { fs.mkdirSync(__dirname + '/actual-files'); } catch (e) {} 148 | fs.writeFileSync(__dirname + '/actual-files/moving-dot.gif', actualBytes); 149 | } 150 | assert.deepEqual(expectedBytes, actualBytes); 151 | }); 152 | }); 153 | 154 | describe('GifEncoder encoding a checkerboard from indexed pixels', function () { 155 | createGif(10, 10); 156 | before(function () { 157 | this.gif.writeHeader(); 158 | this.gif.addFrame([ 159 | 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 160 | 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 161 | 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 162 | 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 163 | 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 164 | 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 165 | 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 166 | 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 167 | 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 168 | 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 169 | ], { 170 | palette: [0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], 171 | indexedPixels: true 172 | }); 173 | this.gif.finish(); 174 | }); 175 | before(function (done) { 176 | this.gif.once('readable', done); 177 | }); 178 | 179 | it('generates the expected bytes', function () { 180 | // TODO: Output canvas to a file, perceptual diff GIF to canvas output =3 181 | // Grab the expected and actual content 182 | var expectedBytes = fs.readFileSync(__dirname + '/expected-files/checkerboard-indexed.gif'); 183 | var actualBytes = this.gif.read(); 184 | 185 | // DEV: Write out actual file to expected file 186 | if (process.env.DEBUG_TEST) { 187 | try { fs.mkdirSync(__dirname + '/actual-files'); } catch (e) {} 188 | fs.writeFileSync(__dirname + '/actual-files/checkerboard-indexed.gif', actualBytes); 189 | } 190 | 191 | // Assert the expected matches the actual content 192 | assert.deepEqual(expectedBytes, actualBytes); 193 | }); 194 | }); 195 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gif-encoder [![Build status](https://travis-ci.org/twolfson/gif-encoder.png?branch=master)](https://travis-ci.org/twolfson/gif-encoder) 2 | 3 | Streaming [GIF][] encoder 4 | 5 | [GIF]: http://en.wikipedia.org/wiki/Graphics_Interchange_Format 6 | 7 | This is built as part of the [gifsockets][] project. It is forked from [gif.js][] to allow for a streaming API and performance optimization. 8 | 9 | [gifsockets]: https://github.com/twolfson/gifsockets-server 10 | 11 | ## Getting Started 12 | Install the module with: `npm install gif-encoder` 13 | 14 | ```js 15 | // Create a 10 x 10 gif 16 | var GifEncoder = require('gif-encoder'); 17 | var gif = new GifEncoder(10, 10); 18 | 19 | // using an rgba array of pixels [r, g, b, a, ... continues on for every pixel] 20 | // This can be collected from a via context.getImageData(0, 0, width, height).data 21 | var pixels = [0, 0, 0, 255/*, ...*/]; 22 | 23 | // Collect output 24 | var file = require('fs').createWriteStream('img.gif'); 25 | gif.pipe(file); 26 | 27 | // Write out the image into memory 28 | gif.writeHeader(); 29 | gif.addFrame(pixels); 30 | // gif.addFrame(pixels); // Write subsequent rgba arrays for more frames 31 | gif.finish(); 32 | ``` 33 | 34 | ## Documentation 35 | `gif-encoder` exports `GifEncoder`, a constructor function which extends `readable-stream@~1.1.9`. This means you can use any `streams1`/`streams2` functionality. I will re-iterate what this means below. 36 | 37 | ```js 38 | // streams1 39 | var gif = new GifEncoder(10, 10); 40 | gif.on('data', console.log); 41 | gif.on('end', process.exit); 42 | 43 | // streams2 44 | var gif = new GifEncoder(10, 10); 45 | gif.on('readable', function () { 46 | console.log(gif.read()); 47 | }); 48 | ``` 49 | 50 | ### `new GifEncoder(width, height, [options])` 51 | Constructor for a new `GifEncoder` 52 | 53 | - width `Number` - Width, in pixels, of the `GIF` to output 54 | - height `Number` - Height, in pixels, of the `GIF` to output 55 | - options `Object` - Optional container for any options 56 | - highWaterMark `Number` - Number, in bytes, to store in internal buffer. Defaults to 64kB. 57 | 58 | **NEVER CALL `.removeAllListeners()`. NO DATA EVENTS WILL BE ABLE TO EMIT.** 59 | 60 | > We implement the GIF89a specification which can be found at 61 | > 62 | > http://www.w3.org/Graphics/GIF/spec-gif89a.txt 63 | 64 | ### Events 65 | #### Event: `data` 66 | `function (buffer) {}` 67 | 68 | Emits a [`Buffer`][] containing either header bytes, frame bytes, or footer bytes. 69 | 70 | [`Buffer`]: http://nodejs.org/api/buffer.html 71 | 72 | #### Event: `end` 73 | `function () {}` 74 | 75 | Signifies end of the encoding has been reached. This will be emitted once `.finish()` is called. 76 | 77 | #### Event: `error` 78 | `function (error) {}` 79 | 80 | Emits an `Error` when internal buffer is exceeded. This occurs when you do not `read` (either via `.on('data')` or `.read()`) and we cannot flush prepared data. 81 | 82 | > If you have a very large GIF, you can update [`options.highWaterMark`][Constructor] via the [Constructor][]. 83 | 84 | [Constructor]: #constructor 85 | 86 | #### Event: `readable` 87 | `function () {}` 88 | 89 | Emits when the stream is ready to be `.read()` from. 90 | 91 | #### Event: `writeHeader#start/stop` 92 | `function () {}` 93 | 94 | Emits when at the start and end of `.writeHeader()`. 95 | 96 | #### Event: `frame#start/stop` 97 | `function () {}` 98 | 99 | Emits when at the start and end of `.addFrame()` 100 | 101 | #### Event: `finish#start/stop` 102 | `function () {}` 103 | 104 | Emits when at the start and end of `.finish()` 105 | 106 | ### Settings 107 | #### `gif.setDelay(ms)` 108 | Set milliseconds to wait between frames 109 | 110 | - ms `Number` - Amount of milliseconds to delay between frames 111 | 112 | #### `setFrameRate(framesPerSecond)` 113 | Set delay based on amount of frames per second. Cannot be used with `gif.setDelay`. 114 | 115 | - framesPerSecond `Number` - Amount of frames per second 116 | 117 | #### `setDispose(disposalCode)` 118 | Set the disposal code 119 | 120 | - disposalCode `Number` - Alters behavior of how to render between frames 121 | - If no transparent color has been set, defaults to 0. 122 | - Otherwise, defaults to 2. 123 | 124 | ``` 125 | Values : 0 - No disposal specified. The decoder is 126 | not required to take any action. 127 | 1 - Do not dispose. The graphic is to be left 128 | in place. 129 | 2 - Restore to background color. The area used by the 130 | graphic must be restored to the background color. 131 | 3 - Restore to previous. The decoder is required to 132 | restore the area overwritten by the graphic with 133 | what was there prior to rendering the graphic. 134 | 4-7 - To be defined. 135 | ``` 136 | 137 | Taken from http://www.w3.org/Graphics/GIF/spec-gif89a.txt 138 | 139 | #### `setRepeat(n)` 140 | Sets amount of times to repeat `GIF` 141 | 142 | - n `Number` 143 | - If `n` is -1, play once. 144 | - If `n` is 0, loop indefinitely. 145 | - If `n` is a positive number, loop `n` times. 146 | 147 | #### `setTransparent(color)` 148 | Define the color which represents transparency in the `GIF`. 149 | 150 | - color `Hexadecimal Number` - Color to represent transparent background 151 | - Example: `0x00FF00` 152 | 153 | #### `setQuality(quality)` 154 | Set the quality (computational/performance trade-off). 155 | 156 | - quality `Positive number` 157 | - 1 is best colors, worst performance. 158 | - 20 is suggested maximum but there is no limit. 159 | - 10 is the default, provided an even trade-off. 160 | 161 | ### Input/output 162 | #### `read([size])` 163 | Read out `size` bytes or until the end of the buffer. This is implemented by `readable-stream`. 164 | 165 | - size `Number` - Optional number of bytes to read out 166 | 167 | #### `writeHeader()` 168 | Write out header bytes. We are following `GIF89a` specification. 169 | 170 | #### `addFrame(imageData, options)` 171 | Write out a new frame to the GIF. 172 | 173 | - imageData `Array` - Array of pixels for the new frame. It should follow the sequence of `r, g, b, a` and be `4 * height * width` in length. 174 | - If used with the options `palette` and `indexedPixels`, then this becomes the index in the palette (e.g. `0` for `color #0`) 175 | - options `Object` - Optional container for options 176 | - palette `Array` - Array of pixels to use as palette for the frame. It should follow the sequence of `r, g, b, a` 177 | - At the moment, this must be used with `options.indexedPixels` 178 | - indexedPixels `Boolean` - Indicator to treat `imageData` as RGBA values (`false`) or indicies in `palette` (`true`) 179 | 180 | #### `finish()` 181 | Write out footer bytes. 182 | 183 | ### Low-level 184 | For performance in [gifsockets][], we needed to open up some lower level methods for fancy tricks. 185 | 186 | **Don't use these unless you know what you are doing.** 187 | 188 | #### `flushData()` 189 | We have a secondary internal buffer that collects each byte from `writeByte`. This is to prevent create a new `Buffer` and `data` event for *every byte of data*. 190 | 191 | This method empties the internal buffer and pushes it out to the `stream` buffer for reading. 192 | 193 | #### `pixels` 194 | Internal store for `imageData` passed in by `addFrame`. 195 | 196 | #### `analyzeImage(imageData, options)` 197 | First part of `addFrame`; runs `setImagePixels(removeAlphaChannel(imageData))` and runs `analyzePixels()`. 198 | 199 | - imageData `Array` - Same as that in [`addFrame`][] 200 | - options `Object` - Optional container for options 201 | - indexedPixels `Boolean` - Indicator to treat `imageData` as RGBA values (`false`) or indicies in `palette` (`true`) 202 | 203 | [`addFrame`]: #addframeimagedata 204 | 205 | #### `removeAlphaChannel(imageData)` 206 | Reduces `imageData` into a `Uint8Array` of length `3 * width * height` containing sequences of `r, g, b`; removing the alpha channel. 207 | 208 | - imageData `Array` - Same as that in [`addFrame`][]; array containing `r, g, b, a` sequences. 209 | 210 | #### `setImagePixels(pixels)` 211 | Save `pixels` as `this.pixels` for image analysis. 212 | 213 | - pixels `Array` - Same as `imageData` from [`addFrame`][] 214 | - **`GifEncoder` will mutate the original data.** 215 | 216 | #### `setImagePalette(palette)` 217 | Save `palette` as `this.userPalette` for frame writing. 218 | 219 | - palette `Array` - Same as `options.palette` from [`addFrame`][] 220 | 221 | #### `writeImageInfo()` 222 | Second part of `addFrame`; behavior varies on if it is the first frame or following frame. 223 | 224 | In either case, it writes out a bunch of bytes about the image (e.g. palette, color tables). 225 | 226 | #### `outputImage()` 227 | Third part of `addFrame`; encodes the analyzed/indexed pixels for the GIF format. 228 | 229 | ## Donating 230 | Support this project and [others by twolfson][twolfson-projects] via [donations][twolfson-support-me]. 231 | 232 | 233 | 234 | [twolfson-projects]: http://twolfson.com/projects 235 | [twolfson-support-me]: http://twolfson.com/support-me 236 | 237 | ## Contributing 238 | In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint via [grunt](https://github.com/gruntjs/grunt) and test via `npm test`. 239 | 240 | ## UNLICENSE 241 | As of Nov 11 2013, Todd Wolfson has released all code differences since initial fork from [gif.js][] to the public domain. 242 | 243 | These differences have been released under the [UNLICENSE][]. 244 | 245 | [UNLICENSE]: UNLICENSE 246 | 247 | At the [gif.js][] time of forking, [gif.js][] was using the [MIT license][]. 248 | 249 | [gif.js]: https://github.com/jnordberg/gif.js/tree/faee238491302de05a1ed05e4fbe562738a76310 250 | 251 | [MIT license]: https://github.com/jnordberg/gif.js/tree/faee238491302de05a1ed05e4fbe562738a76310#license 252 | -------------------------------------------------------------------------------- /lib/NeuQuant.js: -------------------------------------------------------------------------------- 1 | /* NeuQuant Neural-Net Quantization Algorithm 2 | * ------------------------------------------ 3 | * 4 | * Copyright (c) 1994 Anthony Dekker 5 | * 6 | * NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. 7 | * See "Kohonen neural networks for optimal colour quantization" 8 | * in "Network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367. 9 | * for a discussion of the algorithm. 10 | * See also http://members.ozemail.com.au/~dekker/NEUQUANT.HTML 11 | * 12 | * Any party obtaining a copy of these files from the author, directly or 13 | * indirectly, is granted, free of charge, a full and unrestricted irrevocable, 14 | * world-wide, paid up, royalty-free, nonexclusive right and license to deal 15 | * in this software and documentation files (the "Software"), including without 16 | * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 17 | * and/or sell copies of the Software, and to permit persons who receive 18 | * copies from any such party to do so, with the only requirement being 19 | * that this copyright notice remain intact. 20 | * 21 | * (JavaScript port 2012 by Johan Nordberg) 22 | */ 23 | 24 | function toInt(v) { 25 | return ~~v; 26 | } 27 | 28 | var ncycles = 100; // number of learning cycles 29 | var netsize = 256; // number of colors used 30 | var maxnetpos = netsize - 1; 31 | 32 | // defs for freq and bias 33 | var netbiasshift = 4; // bias for colour values 34 | var intbiasshift = 16; // bias for fractions 35 | var intbias = (1 << intbiasshift); 36 | var gammashift = 10; 37 | var gamma = (1 << gammashift); 38 | var betashift = 10; 39 | var beta = (intbias >> betashift); /* beta = 1/1024 */ 40 | var betagamma = (intbias << (gammashift - betashift)); 41 | 42 | // defs for decreasing radius factor 43 | var initrad = (netsize >> 3); // for 256 cols, radius starts 44 | var radiusbiasshift = 6; // at 32.0 biased by 6 bits 45 | var radiusbias = (1 << radiusbiasshift); 46 | var initradius = (initrad * radiusbias); //and decreases by a 47 | var radiusdec = 30; // factor of 1/30 each cycle 48 | 49 | // defs for decreasing alpha factor 50 | var alphabiasshift = 10; // alpha starts at 1.0 51 | var initalpha = (1 << alphabiasshift); 52 | var alphadec; // biased by 10 bits 53 | 54 | /* radbias and alpharadbias used for radpower calculation */ 55 | var radbiasshift = 8; 56 | var radbias = (1 << radbiasshift); 57 | var alpharadbshift = (alphabiasshift + radbiasshift); 58 | var alpharadbias = (1 << alpharadbshift); 59 | 60 | // four primes near 500 - assume no image has a length so large that it is 61 | // divisible by all four primes 62 | var prime1 = 499; 63 | var prime2 = 491; 64 | var prime3 = 487; 65 | var prime4 = 503; 66 | var minpicturebytes = (3 * prime4); 67 | 68 | /* 69 | Constructor: NeuQuant 70 | 71 | Arguments: 72 | 73 | pixels - array of pixels in RGB format 74 | samplefac - sampling factor 1 to 30 where lower is better quality 75 | 76 | > 77 | > pixels = [r, g, b, r, g, b, r, g, b, ..] 78 | > 79 | */ 80 | function NeuQuant(pixels, samplefac) { 81 | var network; // int[netsize][4] 82 | var netindex; // for network lookup - really 256 83 | 84 | // bias and freq arrays for learning 85 | var bias; 86 | var freq; 87 | var radpower; 88 | 89 | /* 90 | Private Method: init 91 | 92 | sets up arrays 93 | */ 94 | function init() { 95 | network = []; 96 | netindex = []; 97 | bias = []; 98 | freq = []; 99 | radpower = []; 100 | 101 | var i, v; 102 | for (i = 0; i < netsize; i++) { 103 | v = (i << (netbiasshift + 8)) / netsize; 104 | network[i] = [v, v, v]; 105 | freq[i] = intbias / netsize; 106 | bias[i] = 0; 107 | } 108 | } 109 | 110 | /* 111 | Private Method: unbiasnet 112 | 113 | unbiases network to give byte values 0..255 and record position i to prepare for sort 114 | */ 115 | function unbiasnet() { 116 | for (var i = 0; i < netsize; i++) { 117 | network[i][0] >>= netbiasshift; 118 | network[i][1] >>= netbiasshift; 119 | network[i][2] >>= netbiasshift; 120 | network[i][3] = i; // record color number 121 | } 122 | } 123 | 124 | /* 125 | Private Method: altersingle 126 | 127 | moves neuron *i* towards biased (b,g,r) by factor *alpha* 128 | */ 129 | function altersingle(alpha, i, b, g, r) { 130 | network[i][0] -= (alpha * (network[i][0] - b)) / initalpha; 131 | network[i][1] -= (alpha * (network[i][1] - g)) / initalpha; 132 | network[i][2] -= (alpha * (network[i][2] - r)) / initalpha; 133 | } 134 | 135 | /* 136 | Private Method: alterneigh 137 | 138 | moves neurons in *radius* around index *i* towards biased (b,g,r) by factor *alpha* 139 | */ 140 | function alterneigh(radius, i, b, g, r) { 141 | var lo = Math.abs(i - radius); 142 | var hi = Math.min(i + radius, netsize); 143 | 144 | var j = i + 1; 145 | var k = i - 1; 146 | var m = 1; 147 | 148 | var p, a; 149 | while ((j < hi) || (k > lo)) { 150 | a = radpower[m++]; 151 | 152 | if (j < hi) { 153 | p = network[j++]; 154 | p[0] -= (a * (p[0] - b)) / alpharadbias; 155 | p[1] -= (a * (p[1] - g)) / alpharadbias; 156 | p[2] -= (a * (p[2] - r)) / alpharadbias; 157 | } 158 | 159 | if (k > lo) { 160 | p = network[k--]; 161 | p[0] -= (a * (p[0] - b)) / alpharadbias; 162 | p[1] -= (a * (p[1] - g)) / alpharadbias; 163 | p[2] -= (a * (p[2] - r)) / alpharadbias; 164 | } 165 | } 166 | } 167 | 168 | /* 169 | Private Method: contest 170 | 171 | searches for biased BGR values 172 | */ 173 | function contest(b, g, r) { 174 | /* 175 | finds closest neuron (min dist) and updates freq 176 | finds best neuron (min dist-bias) and returns position 177 | for frequently chosen neurons, freq[i] is high and bias[i] is negative 178 | bias[i] = gamma * ((1 / netsize) - freq[i]) 179 | */ 180 | 181 | var bestd = ~(1 << 31); 182 | var bestbiasd = bestd; 183 | var bestpos = -1; 184 | var bestbiaspos = bestpos; 185 | 186 | var i, n, dist, biasdist, betafreq; 187 | for (i = 0; i < netsize; i++) { 188 | n = network[i]; 189 | 190 | dist = Math.abs(n[0] - b) + Math.abs(n[1] - g) + Math.abs(n[2] - r); 191 | if (dist < bestd) { 192 | bestd = dist; 193 | bestpos = i; 194 | } 195 | 196 | biasdist = dist - ((bias[i]) >> (intbiasshift - netbiasshift)); 197 | if (biasdist < bestbiasd) { 198 | bestbiasd = biasdist; 199 | bestbiaspos = i; 200 | } 201 | 202 | betafreq = (freq[i] >> betashift); 203 | freq[i] -= betafreq; 204 | bias[i] += (betafreq << gammashift); 205 | } 206 | 207 | freq[bestpos] += beta; 208 | bias[bestpos] -= betagamma; 209 | 210 | return bestbiaspos; 211 | } 212 | 213 | /* 214 | Private Method: inxbuild 215 | 216 | sorts network and builds netindex[0..255] 217 | */ 218 | function inxbuild() { 219 | var i, j, p, q, smallpos, smallval, previouscol = 0, startpos = 0; 220 | for (i = 0; i < netsize; i++) { 221 | p = network[i]; 222 | smallpos = i; 223 | smallval = p[1]; // index on g 224 | // find smallest in i..netsize-1 225 | for (j = i + 1; j < netsize; j++) { 226 | q = network[j]; 227 | if (q[1] < smallval) { // index on g 228 | smallpos = j; 229 | smallval = q[1]; // index on g 230 | } 231 | } 232 | q = network[smallpos]; 233 | // swap p (i) and q (smallpos) entries 234 | if (i != smallpos) { 235 | j = q[0]; q[0] = p[0]; p[0] = j; 236 | j = q[1]; q[1] = p[1]; p[1] = j; 237 | j = q[2]; q[2] = p[2]; p[2] = j; 238 | j = q[3]; q[3] = p[3]; p[3] = j; 239 | } 240 | // smallval entry is now in position i 241 | 242 | if (smallval != previouscol) { 243 | netindex[previouscol] = (startpos + i) >> 1; 244 | for (j = previouscol + 1; j < smallval; j++) 245 | netindex[j] = i; 246 | previouscol = smallval; 247 | startpos = i; 248 | } 249 | } 250 | netindex[previouscol] = (startpos + maxnetpos) >> 1; 251 | for (j = previouscol + 1; j < 256; j++) 252 | netindex[j] = maxnetpos; // really 256 253 | } 254 | 255 | /* 256 | Private Method: inxsearch 257 | 258 | searches for BGR values 0..255 and returns a color index 259 | */ 260 | function inxsearch(b, g, r) { 261 | var a, p, dist; 262 | 263 | var bestd = 1000; // biggest possible dist is 256*3 264 | var best = -1; 265 | 266 | var i = netindex[g]; // index on g 267 | var j = i - 1; // start at netindex[g] and work outwards 268 | 269 | while ((i < netsize) || (j >= 0)) { 270 | if (i < netsize) { 271 | p = network[i]; 272 | dist = p[1] - g; // inx key 273 | if (dist >= bestd) i = netsize; // stop iter 274 | else { 275 | i++; 276 | if (dist < 0) dist = -dist; 277 | a = p[0] - b; if (a < 0) a = -a; 278 | dist += a; 279 | if (dist < bestd) { 280 | a = p[2] - r; if (a < 0) a = -a; 281 | dist += a; 282 | if (dist < bestd) { 283 | bestd = dist; 284 | best = p[3]; 285 | } 286 | } 287 | } 288 | } 289 | if (j >= 0) { 290 | p = network[j]; 291 | dist = g - p[1]; // inx key - reverse dif 292 | if (dist >= bestd) j = -1; // stop iter 293 | else { 294 | j--; 295 | if (dist < 0) dist = -dist; 296 | a = p[0] - b; if (a < 0) a = -a; 297 | dist += a; 298 | if (dist < bestd) { 299 | a = p[2] - r; if (a < 0) a = -a; 300 | dist += a; 301 | if (dist < bestd) { 302 | bestd = dist; 303 | best = p[3]; 304 | } 305 | } 306 | } 307 | } 308 | } 309 | 310 | return best; 311 | } 312 | 313 | /* 314 | Private Method: learn 315 | 316 | "Main Learning Loop" 317 | */ 318 | function learn() { 319 | var i; 320 | 321 | var lengthcount = pixels.length; 322 | var alphadec = toInt(30 + ((samplefac - 1) / 3)); 323 | var samplepixels = toInt(lengthcount / (3 * samplefac)); 324 | var delta = toInt(samplepixels / ncycles); 325 | var alpha = initalpha; 326 | var radius = initradius; 327 | 328 | var rad = radius >> radiusbiasshift; 329 | 330 | if (rad <= 1) rad = 0; 331 | for (i = 0; i < rad; i++) 332 | radpower[i] = toInt(alpha * (((rad * rad - i * i) * radbias) / (rad * rad))); 333 | 334 | var step; 335 | if (lengthcount < minpicturebytes) { 336 | samplefac = 1; 337 | step = 3; 338 | } else if ((lengthcount % prime1) !== 0) { 339 | step = 3 * prime1; 340 | } else if ((lengthcount % prime2) !== 0) { 341 | step = 3 * prime2; 342 | } else if ((lengthcount % prime3) !== 0) { 343 | step = 3 * prime3; 344 | } else { 345 | step = 3 * prime4; 346 | } 347 | 348 | var b, g, r, j; 349 | var pix = 0; // current pixel 350 | 351 | i = 0; 352 | while (i < samplepixels) { 353 | b = (pixels[pix] & 0xff) << netbiasshift; 354 | g = (pixels[pix + 1] & 0xff) << netbiasshift; 355 | r = (pixels[pix + 2] & 0xff) << netbiasshift; 356 | 357 | j = contest(b, g, r); 358 | 359 | altersingle(alpha, j, b, g, r); 360 | if (rad !== 0) alterneigh(rad, j, b, g, r); // alter neighbours 361 | 362 | pix += step; 363 | if (pix >= lengthcount) pix -= lengthcount; 364 | 365 | i++; 366 | 367 | if (delta === 0) delta = 1; 368 | if (i % delta === 0) { 369 | alpha -= alpha / alphadec; 370 | radius -= radius / radiusdec; 371 | rad = radius >> radiusbiasshift; 372 | 373 | if (rad <= 1) rad = 0; 374 | for (j = 0; j < rad; j++) 375 | radpower[j] = toInt(alpha * (((rad * rad - j * j) * radbias) / (rad * rad))); 376 | } 377 | } 378 | } 379 | 380 | /* 381 | Method: buildColormap 382 | 383 | 1. initializes network 384 | 2. trains it 385 | 3. removes misconceptions 386 | 4. builds colorindex 387 | */ 388 | function buildColormap() { 389 | init(); 390 | learn(); 391 | unbiasnet(); 392 | inxbuild(); 393 | } 394 | this.buildColormap = buildColormap; 395 | 396 | /* 397 | Method: getColormap 398 | 399 | builds colormap from the index 400 | 401 | returns array in the format: 402 | 403 | > 404 | > [r, g, b, r, g, b, r, g, b, ..] 405 | > 406 | */ 407 | function getColormap() { 408 | var map = []; 409 | var index = []; 410 | 411 | for (var i = 0; i < netsize; i++) 412 | index[network[i][3]] = i; 413 | 414 | var k = 0; 415 | for (var l = 0; l < netsize; l++) { 416 | var j = index[l]; 417 | map[k++] = (network[j][0]); 418 | map[k++] = (network[j][1]); 419 | map[k++] = (network[j][2]); 420 | } 421 | return map; 422 | } 423 | this.getColormap = getColormap; 424 | 425 | /* 426 | Method: lookupRGB 427 | 428 | looks for the closest *r*, *g*, *b* color in the map and 429 | returns its index 430 | */ 431 | this.lookupRGB = inxsearch; 432 | } 433 | 434 | module.exports = NeuQuant; 435 | -------------------------------------------------------------------------------- /lib/TypedNeuQuant.js: -------------------------------------------------------------------------------- 1 | /* NeuQuant Neural-Net Quantization Algorithm 2 | * ------------------------------------------ 3 | * 4 | * Copyright (c) 1994 Anthony Dekker 5 | * 6 | * NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. 7 | * See "Kohonen neural networks for optimal colour quantization" 8 | * in "Network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367. 9 | * for a discussion of the algorithm. 10 | * See also http://members.ozemail.com.au/~dekker/NEUQUANT.HTML 11 | * 12 | * Any party obtaining a copy of these files from the author, directly or 13 | * indirectly, is granted, free of charge, a full and unrestricted irrevocable, 14 | * world-wide, paid up, royalty-free, nonexclusive right and license to deal 15 | * in this software and documentation files (the "Software"), including without 16 | * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 17 | * and/or sell copies of the Software, and to permit persons who receive 18 | * copies from any such party to do so, with the only requirement being 19 | * that this copyright notice remain intact. 20 | * 21 | * (JavaScript port 2012 by Johan Nordberg) 22 | */ 23 | 24 | var ncycles = 100; // number of learning cycles 25 | var netsize = 256; // number of colors used 26 | var maxnetpos = netsize - 1; 27 | 28 | // defs for freq and bias 29 | var netbiasshift = 4; // bias for colour values 30 | var intbiasshift = 16; // bias for fractions 31 | var intbias = (1 << intbiasshift); 32 | var gammashift = 10; 33 | var gamma = (1 << gammashift); 34 | var betashift = 10; 35 | var beta = (intbias >> betashift); /* beta = 1/1024 */ 36 | var betagamma = (intbias << (gammashift - betashift)); 37 | 38 | // defs for decreasing radius factor 39 | var initrad = (netsize >> 3); // for 256 cols, radius starts 40 | var radiusbiasshift = 6; // at 32.0 biased by 6 bits 41 | var radiusbias = (1 << radiusbiasshift); 42 | var initradius = (initrad * radiusbias); //and decreases by a 43 | var radiusdec = 30; // factor of 1/30 each cycle 44 | 45 | // defs for decreasing alpha factor 46 | var alphabiasshift = 10; // alpha starts at 1.0 47 | var initalpha = (1 << alphabiasshift); 48 | var alphadec; // biased by 10 bits 49 | 50 | /* radbias and alpharadbias used for radpower calculation */ 51 | var radbiasshift = 8; 52 | var radbias = (1 << radbiasshift); 53 | var alpharadbshift = (alphabiasshift + radbiasshift); 54 | var alpharadbias = (1 << alpharadbshift); 55 | 56 | // four primes near 500 - assume no image has a length so large that it is 57 | // divisible by all four primes 58 | var prime1 = 499; 59 | var prime2 = 491; 60 | var prime3 = 487; 61 | var prime4 = 503; 62 | var minpicturebytes = (3 * prime4); 63 | 64 | /* 65 | Constructor: NeuQuant 66 | 67 | Arguments: 68 | 69 | pixels - array of pixels in RGB format 70 | samplefac - sampling factor 1 to 30 where lower is better quality 71 | 72 | > 73 | > pixels = [r, g, b, r, g, b, r, g, b, ..] 74 | > 75 | */ 76 | function NeuQuant(pixels, samplefac) { 77 | var network; // int[netsize][4] 78 | var netindex; // for network lookup - really 256 79 | 80 | // bias and freq arrays for learning 81 | var bias; 82 | var freq; 83 | var radpower; 84 | 85 | /* 86 | Private Method: init 87 | 88 | sets up arrays 89 | */ 90 | function init() { 91 | network = []; 92 | netindex = new Int32Array(256); 93 | bias = new Int32Array(netsize); 94 | freq = new Int32Array(netsize); 95 | radpower = new Int32Array(netsize >> 3); 96 | 97 | var i, v; 98 | for (i = 0; i < netsize; i++) { 99 | v = (i << (netbiasshift + 8)) / netsize; 100 | network[i] = new Float64Array([v, v, v, 0]); 101 | //network[i] = [v, v, v, 0] 102 | freq[i] = intbias / netsize; 103 | bias[i] = 0; 104 | } 105 | } 106 | 107 | /* 108 | Private Method: unbiasnet 109 | 110 | unbiases network to give byte values 0..255 and record position i to prepare for sort 111 | */ 112 | function unbiasnet() { 113 | for (var i = 0; i < netsize; i++) { 114 | network[i][0] >>= netbiasshift; 115 | network[i][1] >>= netbiasshift; 116 | network[i][2] >>= netbiasshift; 117 | network[i][3] = i; // record color number 118 | } 119 | } 120 | 121 | /* 122 | Private Method: altersingle 123 | 124 | moves neuron *i* towards biased (b,g,r) by factor *alpha* 125 | */ 126 | function altersingle(alpha, i, b, g, r) { 127 | network[i][0] -= (alpha * (network[i][0] - b)) / initalpha; 128 | network[i][1] -= (alpha * (network[i][1] - g)) / initalpha; 129 | network[i][2] -= (alpha * (network[i][2] - r)) / initalpha; 130 | } 131 | 132 | /* 133 | Private Method: alterneigh 134 | 135 | moves neurons in *radius* around index *i* towards biased (b,g,r) by factor *alpha* 136 | */ 137 | function alterneigh(radius, i, b, g, r) { 138 | var lo = Math.abs(i - radius); 139 | var hi = Math.min(i + radius, netsize); 140 | 141 | var j = i + 1; 142 | var k = i - 1; 143 | var m = 1; 144 | 145 | var p, a; 146 | while ((j < hi) || (k > lo)) { 147 | a = radpower[m++]; 148 | 149 | if (j < hi) { 150 | p = network[j++]; 151 | p[0] -= (a * (p[0] - b)) / alpharadbias; 152 | p[1] -= (a * (p[1] - g)) / alpharadbias; 153 | p[2] -= (a * (p[2] - r)) / alpharadbias; 154 | } 155 | 156 | if (k > lo) { 157 | p = network[k--]; 158 | p[0] -= (a * (p[0] - b)) / alpharadbias; 159 | p[1] -= (a * (p[1] - g)) / alpharadbias; 160 | p[2] -= (a * (p[2] - r)) / alpharadbias; 161 | } 162 | } 163 | } 164 | 165 | /* 166 | Private Method: contest 167 | 168 | searches for biased BGR values 169 | */ 170 | function contest(b, g, r) { 171 | /* 172 | finds closest neuron (min dist) and updates freq 173 | finds best neuron (min dist-bias) and returns position 174 | for frequently chosen neurons, freq[i] is high and bias[i] is negative 175 | bias[i] = gamma * ((1 / netsize) - freq[i]) 176 | */ 177 | 178 | var bestd = ~(1 << 31); 179 | var bestbiasd = bestd; 180 | var bestpos = -1; 181 | var bestbiaspos = bestpos; 182 | 183 | var i, n, dist, biasdist, betafreq; 184 | for (i = 0; i < netsize; i++) { 185 | n = network[i]; 186 | 187 | dist = Math.abs(n[0] - b) + Math.abs(n[1] - g) + Math.abs(n[2] - r); 188 | if (dist < bestd) { 189 | bestd = dist; 190 | bestpos = i; 191 | } 192 | 193 | biasdist = dist - ((bias[i]) >> (intbiasshift - netbiasshift)); 194 | if (biasdist < bestbiasd) { 195 | bestbiasd = biasdist; 196 | bestbiaspos = i; 197 | } 198 | 199 | betafreq = (freq[i] >> betashift); 200 | freq[i] -= betafreq; 201 | bias[i] += (betafreq << gammashift); 202 | } 203 | 204 | freq[bestpos] += beta; 205 | bias[bestpos] -= betagamma; 206 | 207 | return bestbiaspos; 208 | } 209 | 210 | /* 211 | Private Method: inxbuild 212 | 213 | sorts network and builds netindex[0..255] 214 | */ 215 | function inxbuild() { 216 | var i, j, p, q, smallpos, smallval, previouscol = 0, startpos = 0; 217 | for (i = 0; i < netsize; i++) { 218 | p = network[i]; 219 | smallpos = i; 220 | smallval = p[1]; // index on g 221 | // find smallest in i..netsize-1 222 | for (j = i + 1; j < netsize; j++) { 223 | q = network[j]; 224 | if (q[1] < smallval) { // index on g 225 | smallpos = j; 226 | smallval = q[1]; // index on g 227 | } 228 | } 229 | q = network[smallpos]; 230 | // swap p (i) and q (smallpos) entries 231 | if (i != smallpos) { 232 | j = q[0]; q[0] = p[0]; p[0] = j; 233 | j = q[1]; q[1] = p[1]; p[1] = j; 234 | j = q[2]; q[2] = p[2]; p[2] = j; 235 | j = q[3]; q[3] = p[3]; p[3] = j; 236 | } 237 | // smallval entry is now in position i 238 | 239 | if (smallval != previouscol) { 240 | netindex[previouscol] = (startpos + i) >> 1; 241 | for (j = previouscol + 1; j < smallval; j++) 242 | netindex[j] = i; 243 | previouscol = smallval; 244 | startpos = i; 245 | } 246 | } 247 | netindex[previouscol] = (startpos + maxnetpos) >> 1; 248 | for (j = previouscol + 1; j < 256; j++) 249 | netindex[j] = maxnetpos; // really 256 250 | } 251 | 252 | /* 253 | Private Method: inxsearch 254 | 255 | searches for BGR values 0..255 and returns a color index 256 | */ 257 | function inxsearch(b, g, r) { 258 | var a, p, dist; 259 | 260 | var bestd = 1000; // biggest possible dist is 256*3 261 | var best = -1; 262 | 263 | var i = netindex[g]; // index on g 264 | var j = i - 1; // start at netindex[g] and work outwards 265 | 266 | while ((i < netsize) || (j >= 0)) { 267 | if (i < netsize) { 268 | p = network[i]; 269 | dist = p[1] - g; // inx key 270 | if (dist >= bestd) i = netsize; // stop iter 271 | else { 272 | i++; 273 | if (dist < 0) dist = -dist; 274 | a = p[0] - b; if (a < 0) a = -a; 275 | dist += a; 276 | if (dist < bestd) { 277 | a = p[2] - r; if (a < 0) a = -a; 278 | dist += a; 279 | if (dist < bestd) { 280 | bestd = dist; 281 | best = p[3]; 282 | } 283 | } 284 | } 285 | } 286 | if (j >= 0) { 287 | p = network[j]; 288 | dist = g - p[1]; // inx key - reverse dif 289 | if (dist >= bestd) j = -1; // stop iter 290 | else { 291 | j--; 292 | if (dist < 0) dist = -dist; 293 | a = p[0] - b; if (a < 0) a = -a; 294 | dist += a; 295 | if (dist < bestd) { 296 | a = p[2] - r; if (a < 0) a = -a; 297 | dist += a; 298 | if (dist < bestd) { 299 | bestd = dist; 300 | best = p[3]; 301 | } 302 | } 303 | } 304 | } 305 | } 306 | 307 | return best; 308 | } 309 | 310 | /* 311 | Private Method: learn 312 | 313 | "Main Learning Loop" 314 | */ 315 | function learn() { 316 | var i; 317 | 318 | var lengthcount = pixels.length; 319 | var alphadec = 30 + ((samplefac - 1) / 3); 320 | var samplepixels = lengthcount / (3 * samplefac); 321 | var delta = ~~(samplepixels / ncycles); 322 | var alpha = initalpha; 323 | var radius = initradius; 324 | 325 | var rad = radius >> radiusbiasshift; 326 | 327 | if (rad <= 1) rad = 0; 328 | for (i = 0; i < rad; i++) 329 | radpower[i] = alpha * (((rad * rad - i * i) * radbias) / (rad * rad)); 330 | 331 | var step; 332 | if (lengthcount < minpicturebytes) { 333 | samplefac = 1; 334 | step = 3; 335 | } else if ((lengthcount % prime1) !== 0) { 336 | step = 3 * prime1; 337 | } else if ((lengthcount % prime2) !== 0) { 338 | step = 3 * prime2; 339 | } else if ((lengthcount % prime3) !== 0) { 340 | step = 3 * prime3; 341 | } else { 342 | step = 3 * prime4; 343 | } 344 | 345 | var b, g, r, j; 346 | var pix = 0; // current pixel 347 | 348 | i = 0; 349 | while (i < samplepixels) { 350 | b = (pixels[pix] & 0xff) << netbiasshift; 351 | g = (pixels[pix + 1] & 0xff) << netbiasshift; 352 | r = (pixels[pix + 2] & 0xff) << netbiasshift; 353 | 354 | j = contest(b, g, r); 355 | 356 | altersingle(alpha, j, b, g, r); 357 | if (rad !== 0) alterneigh(rad, j, b, g, r); // alter neighbours 358 | 359 | pix += step; 360 | if (pix >= lengthcount) pix -= lengthcount; 361 | 362 | i++; 363 | 364 | if (delta === 0) delta = 1; 365 | if (i % delta === 0) { 366 | alpha -= alpha / alphadec; 367 | radius -= radius / radiusdec; 368 | rad = radius >> radiusbiasshift; 369 | 370 | if (rad <= 1) rad = 0; 371 | for (j = 0; j < rad; j++) 372 | radpower[j] = alpha * (((rad * rad - j * j) * radbias) / (rad * rad)); 373 | } 374 | } 375 | } 376 | 377 | /* 378 | Method: buildColormap 379 | 380 | 1. initializes network 381 | 2. trains it 382 | 3. removes misconceptions 383 | 4. builds colorindex 384 | */ 385 | function buildColormap() { 386 | init(); 387 | learn(); 388 | unbiasnet(); 389 | inxbuild(); 390 | } 391 | this.buildColormap = buildColormap; 392 | 393 | /* 394 | Method: getColormap 395 | 396 | builds colormap from the index 397 | 398 | returns array in the format: 399 | 400 | > 401 | > [r, g, b, r, g, b, r, g, b, ..] 402 | > 403 | */ 404 | function getColormap() { 405 | var map = []; 406 | var index = []; 407 | 408 | for (var i = 0; i < netsize; i++) 409 | index[network[i][3]] = i; 410 | 411 | var k = 0; 412 | for (var l = 0; l < netsize; l++) { 413 | var j = index[l]; 414 | map[k++] = (network[j][0]); 415 | map[k++] = (network[j][1]); 416 | map[k++] = (network[j][2]); 417 | } 418 | return map; 419 | } 420 | this.getColormap = getColormap; 421 | 422 | /* 423 | Method: lookupRGB 424 | 425 | looks for the closest *r*, *g*, *b* color in the map and 426 | returns its index 427 | */ 428 | this.lookupRGB = inxsearch; 429 | } 430 | 431 | module.exports = NeuQuant; 432 | -------------------------------------------------------------------------------- /lib/GIFEncoder.js: -------------------------------------------------------------------------------- 1 | /* 2 | GIFEncoder.js 3 | 4 | Authors 5 | Kevin Weiner (original Java version - kweiner@fmsware.com) 6 | Thibault Imbert (AS3 version - bytearray.org) 7 | Johan Nordberg (JS version - code@johan-nordberg.com) 8 | Todd Wolfson (Implemented streams - todd@twolfson.com) 9 | */ 10 | 11 | var assert = require('assert'); 12 | var EventEmitter = require('events').EventEmitter; 13 | var ReadableStream = require('readable-stream'); 14 | var util = require('util'); 15 | 16 | var NeuQuant = require('./TypedNeuQuant.js'); 17 | var LZWEncoder = require('./LZWEncoder.js'); 18 | 19 | // DEV: By using a capacitor, we prevent creating a data event for every byte written 20 | function ByteCapacitor(options) { 21 | // Inherit from ReadableStream 22 | ReadableStream.call(this, options); 23 | 24 | // Start with an empty buffer and allow writes 25 | this.okayToPush = true; 26 | this.resetData(); 27 | } 28 | util.inherits(ByteCapacitor, ReadableStream); 29 | 30 | ByteCapacitor.prototype._read = function () { 31 | // The output is controlled by the input provided by methods. 32 | // If we exceed the highwater mark, we will raise an error. 33 | this.okayToPush = true; 34 | }; 35 | 36 | ByteCapacitor.prototype.resetData = function () { 37 | this.data = []; 38 | }; 39 | 40 | ByteCapacitor.prototype.flushData = function () { 41 | // If we are not okay to push, emit an error 42 | if (!this.okayToPush) { 43 | var err = new Error('GIF memory limit exceeded. Please `read` from GIF before writing additional frames/information.'); 44 | return this.emit('error', err); 45 | } 46 | 47 | // Otherwise, push out the new buffer 48 | var buff = new Buffer(this.data); 49 | this.resetData(); 50 | this.okayToPush = this.push(buff); 51 | }; 52 | 53 | ByteCapacitor.prototype.writeByte = function (val) { 54 | this.data.push(val); 55 | }; 56 | 57 | ByteCapacitor.prototype.writeUTFBytes = function (string) { 58 | for (var l = string.length, i = 0; i < l; i++) { 59 | this.writeByte(string.charCodeAt(i)); 60 | } 61 | }; 62 | 63 | ByteCapacitor.prototype.writeBytes = function (array, offset, length) { 64 | for (var l = length || array.length, i = offset || 0; i < l; i++) { 65 | this.writeByte(array[i]); 66 | } 67 | }; 68 | 69 | function GIFEncoder(width, height, options) { 70 | // Fallback options 71 | options = options || {}; 72 | 73 | // Inherit from ByteCapacitor immediately 74 | // https://github.com/isaacs/readable-stream/blob/v1.1.9/lib/_stream_readable.js#L60-L63 75 | var hwm = options.highWaterMark; 76 | ByteCapacitor.call(this, { 77 | // Allow for up to 64kB of GIFfy-goodness 78 | highWaterMark: (hwm || hwm === 0) ? hwm : 64 * 1024 79 | }); 80 | 81 | // image size 82 | this.width = ~~width; 83 | this.height = ~~height; 84 | 85 | // transparent color if given 86 | this.transparent = null; 87 | 88 | // transparent index in color table 89 | this.transIndex = 0; 90 | 91 | // -1 = no repeat, 0 = forever. anything else is repeat count 92 | this.repeat = -1; 93 | 94 | // frame delay (hundredths) 95 | this.delay = 0; 96 | 97 | this.pixels = null; // BGR byte array from frame 98 | this.indexedPixels = null; // converted frame indexed to palette 99 | this.colorDepth = null; // number of bit planes 100 | this.colorTab = null; // RGB palette 101 | this.userPalette = null; // User-input palette 102 | this.usedEntry = []; // active palette entries 103 | this.palSize = 7; // color table size (bits-1) 104 | this.dispose = -1; // disposal code (-1 = use default) 105 | this.firstFrame = true; 106 | this.sample = 10; // default sample interval for quantizer 107 | 108 | // When we encounter a header, new frame, or stop, emit data 109 | var that = this; 110 | function flushData() { 111 | that.flushData(); 112 | } 113 | this.on('writeHeader#stop', flushData); 114 | this.on('frame#stop', flushData); 115 | this.on('finish#stop', function finishGif () { 116 | // Flush the data 117 | flushData(); 118 | 119 | // Close the gif 120 | that.push(null); 121 | }); 122 | } 123 | util.inherits(GIFEncoder, ByteCapacitor); 124 | 125 | /* 126 | Sets the delay time between each frame, or changes it for subsequent frames 127 | (applies to last frame added) 128 | */ 129 | GIFEncoder.prototype.setDelay = function(milliseconds) { 130 | this.delay = Math.round(milliseconds / 10); 131 | }; 132 | 133 | /* 134 | Sets frame rate in frames per second. 135 | */ 136 | GIFEncoder.prototype.setFrameRate = function(fps) { 137 | this.delay = Math.round(100 / fps); 138 | }; 139 | 140 | /* 141 | Sets the GIF frame disposal code for the last added frame and any 142 | subsequent frames. 143 | 144 | Default is 0 if no transparent color has been set, otherwise 2. 145 | */ 146 | GIFEncoder.prototype.setDispose = function(disposalCode) { 147 | if (disposalCode >= 0) this.dispose = disposalCode; 148 | }; 149 | 150 | /* 151 | Sets the number of times the set of GIF frames should be played. 152 | 153 | -1 = play once 154 | 0 = repeat indefinitely 155 | 156 | Default is -1 157 | 158 | Must be invoked before the first image is added 159 | */ 160 | 161 | GIFEncoder.prototype.setRepeat = function(repeat) { 162 | this.repeat = repeat; 163 | }; 164 | 165 | /* 166 | Sets the transparent color for the last added frame and any subsequent 167 | frames. Since all colors are subject to modification in the quantization 168 | process, the color in the final palette for each frame closest to the given 169 | color becomes the transparent color for that frame. May be set to null to 170 | indicate no transparent color. 171 | */ 172 | GIFEncoder.prototype.setTransparent = function(color) { 173 | this.transparent = color; 174 | }; 175 | 176 | // Custom methods for performance hacks around streaming GIF data pieces without re-analyzing/loading 177 | GIFEncoder.prototype.analyzeImage = function (imageData, options) { 178 | // convert to correct format if necessary 179 | if (options && options.palette) { 180 | this.setImagePalette(this.removeAlphaChannel(options.palette)); 181 | } 182 | if (options && options.indexedPixels === true) { 183 | assert(options.palette, '`options.indexedPixels` requires `options.palette` to load from. Please include one`'); 184 | this.setImagePixels(imageData); 185 | } else { 186 | this.setImagePixels(this.removeAlphaChannel(imageData)); 187 | } 188 | this.analyzePixels(options); // build color table & map pixels 189 | if (options && options.palette) { 190 | this.userPalette = null; 191 | } 192 | }; 193 | 194 | GIFEncoder.prototype.writeImageInfo = function () { 195 | if (this.firstFrame) { 196 | this.writeLSD(); // logical screen descriptior 197 | this.writePalette(); // global color table 198 | if (this.repeat >= 0) { 199 | // use NS app extension to indicate reps 200 | this.writeNetscapeExt(); 201 | } 202 | } 203 | 204 | this.writeGraphicCtrlExt(); // write graphic control extension 205 | this.writeImageDesc(); // image descriptor 206 | if (!this.firstFrame) this.writePalette(); // local color table 207 | 208 | // DEV: This was originally after outputImage but it does not affect order it seems 209 | this.firstFrame = false; 210 | }; 211 | 212 | GIFEncoder.prototype.outputImage = function () { 213 | this.writePixels(); // encode and write pixel data 214 | }; 215 | 216 | /* 217 | Adds next GIF frame. The frame is not written immediately, but is 218 | actually deferred until the next frame is received so that timing 219 | data can be inserted. Invoking finish() flushes all frames. 220 | */ 221 | GIFEncoder.prototype.addFrame = function(imageData, options) { 222 | this.emit('frame#start'); 223 | 224 | this.analyzeImage(imageData, options); 225 | this.writeImageInfo(); 226 | this.outputImage(); 227 | 228 | this.emit('frame#stop'); 229 | }; 230 | 231 | /* 232 | Adds final trailer to the GIF stream, if you don't call the finish method 233 | the GIF stream will not be valid. 234 | */ 235 | GIFEncoder.prototype.finish = function() { 236 | this.emit('finish#start'); 237 | this.writeByte(0x3b); // gif trailer 238 | this.emit('finish#stop'); 239 | }; 240 | 241 | /* 242 | Sets quality of color quantization (conversion of images to the maximum 256 243 | colors allowed by the GIF specification). Lower values (minimum = 1) 244 | produce better colors, but slow processing significantly. 10 is the 245 | default, and produces good color mapping at reasonable speeds. Values 246 | greater than 20 do not yield significant improvements in speed. 247 | */ 248 | GIFEncoder.prototype.setQuality = function(quality) { 249 | if (quality < 1) quality = 1; 250 | this.sample = quality; 251 | }; 252 | 253 | /* 254 | Writes GIF file header 255 | */ 256 | GIFEncoder.prototype.writeHeader = function() { 257 | this.emit('writeHeader#start'); 258 | this.writeUTFBytes("GIF89a"); 259 | this.emit('writeHeader#stop'); 260 | }; 261 | 262 | /* 263 | Analyzes current frame colors and creates color map. 264 | */ 265 | GIFEncoder.prototype.analyzePixels = function(options) { 266 | // If we're being called with a user defined palette, skip NeuQuant and 267 | // color remapping. 268 | var nPix, index, imgq; 269 | if (this.userPalette !== null) { 270 | assert(options && options.indexedPixels === true, 271 | '`palette` can only be used with `options.indexedPixels` at the moment. ' + 272 | 'Please add `{indexedPixels: true}` to `addFrame()`'); 273 | nPix = this.pixels.length; 274 | this.indexedPixels = new Uint8Array(nPix); 275 | 276 | this.colorTab = this.userPalette; 277 | 278 | for (var i = 0; i < nPix; i++) { 279 | index = this.pixels[i]; 280 | this.indexedPixels[i] = index; 281 | this.usedEntry[index] = true; 282 | } 283 | } else { 284 | var len = this.pixels.length; 285 | nPix = len / 3; 286 | this.indexedPixels = new Uint8Array(nPix); 287 | 288 | imgq = new NeuQuant(this.pixels, this.sample); 289 | imgq.buildColormap(); // create reduced palette 290 | this.colorTab = imgq.getColormap(); 291 | 292 | // map image pixels to new palette 293 | var k = 0; 294 | for (var j = 0; j < nPix; j++) { 295 | index = imgq.lookupRGB( 296 | this.pixels[k++] & 0xff, 297 | this.pixels[k++] & 0xff, 298 | this.pixels[k++] & 0xff 299 | ); 300 | this.usedEntry[index] = true; 301 | this.indexedPixels[j] = index; 302 | } 303 | } 304 | 305 | this.pixels = null; 306 | this.colorDepth = 8; 307 | this.palSize = 7; 308 | 309 | // find index for transparent color 310 | if (this.transparent !== null) { 311 | this.transIndex = imgq.lookupRGB( 312 | (this.transparent & 0xFF0000) >> 16, 313 | (this.transparent & 0x00FF00) >> 8, 314 | (this.transparent & 0x0000FF) 315 | ); 316 | } else { 317 | this.transIndex = 0; 318 | } 319 | }; 320 | 321 | 322 | /* 323 | Extracts image pixels into byte array pixels 324 | (removes alphachannel from canvas imagedata) 325 | */ 326 | GIFEncoder.prototype.removeAlphaChannel = function (data) { 327 | var w = this.width; 328 | var h = this.height; 329 | var pixels = new Uint8Array(w * h * 3); 330 | 331 | var count = 0; 332 | 333 | for (var i = 0; i < h; i++) { 334 | for (var j = 0; j < w; j++) { 335 | var b = (i * w * 4) + j * 4; 336 | pixels[count++] = data[b]; 337 | pixels[count++] = data[b+1]; 338 | pixels[count++] = data[b+2]; 339 | } 340 | } 341 | 342 | return pixels; 343 | }; 344 | 345 | GIFEncoder.prototype.setImagePixels = function(pixels) { 346 | this.pixels = pixels; 347 | }; 348 | GIFEncoder.prototype.setImagePalette = function(userPalette) { 349 | this.userPalette = userPalette; 350 | }; 351 | 352 | /* 353 | Writes Graphic Control Extension 354 | */ 355 | GIFEncoder.prototype.writeGraphicCtrlExt = function() { 356 | this.writeByte(0x21); // extension introducer 357 | this.writeByte(0xf9); // GCE label 358 | this.writeByte(4); // data block size 359 | 360 | var transp, disp; 361 | if (this.transparent === null) { 362 | transp = 0; 363 | disp = 0; // dispose = no action 364 | } else { 365 | transp = 1; 366 | disp = 2; // force clear if using transparent color 367 | } 368 | 369 | if (this.dispose >= 0) { 370 | disp = this.dispose & 7; // user override 371 | } 372 | disp <<= 2; 373 | 374 | // packed fields 375 | this.writeByte( 376 | 0 | // 1:3 reserved 377 | disp | // 4:6 disposal 378 | 0 | // 7 user input - 0 = none 379 | transp // 8 transparency flag 380 | ); 381 | 382 | this.writeShort(this.delay); // delay x 1/100 sec 383 | this.writeByte(this.transIndex); // transparent color index 384 | this.writeByte(0); // block terminator 385 | }; 386 | 387 | /* 388 | Writes Image Descriptor 389 | */ 390 | GIFEncoder.prototype.writeImageDesc = function() { 391 | this.writeByte(0x2c); // image separator 392 | this.writeShort(0); // image position x,y = 0,0 393 | this.writeShort(0); 394 | this.writeShort(this.width); // image size 395 | this.writeShort(this.height); 396 | 397 | // packed fields 398 | if (this.firstFrame) { 399 | // no LCT - GCT is used for first (or only) frame 400 | this.writeByte(0); 401 | } else { 402 | // specify normal LCT 403 | this.writeByte( 404 | 0x80 | // 1 local color table 1=yes 405 | 0 | // 2 interlace - 0=no 406 | 0 | // 3 sorted - 0=no 407 | 0 | // 4-5 reserved 408 | this.palSize // 6-8 size of color table 409 | ); 410 | } 411 | }; 412 | 413 | /* 414 | Writes Logical Screen Descriptor 415 | */ 416 | GIFEncoder.prototype.writeLSD = function() { 417 | // logical screen size 418 | this.writeShort(this.width); 419 | this.writeShort(this.height); 420 | 421 | // packed fields 422 | this.writeByte( 423 | 0x80 | // 1 : global color table flag = 1 (gct used) 424 | 0x70 | // 2-4 : color resolution = 7 425 | 0x00 | // 5 : gct sort flag = 0 426 | this.palSize // 6-8 : gct size 427 | ); 428 | 429 | this.writeByte(0); // background color index 430 | this.writeByte(0); // pixel aspect ratio - assume 1:1 431 | }; 432 | 433 | /* 434 | Writes Netscape application extension to define repeat count. 435 | */ 436 | GIFEncoder.prototype.writeNetscapeExt = function() { 437 | this.writeByte(0x21); // extension introducer 438 | this.writeByte(0xff); // app extension label 439 | this.writeByte(11); // block size 440 | this.writeUTFBytes('NETSCAPE2.0'); // app id + auth code 441 | this.writeByte(3); // sub-block size 442 | this.writeByte(1); // loop sub-block id 443 | this.writeShort(this.repeat); // loop count (extra iterations, 0=repeat forever) 444 | this.writeByte(0); // block terminator 445 | }; 446 | 447 | /* 448 | Writes color table 449 | */ 450 | GIFEncoder.prototype.writePalette = function() { 451 | this.writeBytes(this.colorTab); 452 | var n = (3 * 256) - this.colorTab.length; 453 | for (var i = 0; i < n; i++) 454 | this.writeByte(0); 455 | }; 456 | 457 | GIFEncoder.prototype.writeShort = function(pValue) { 458 | this.writeByte(pValue & 0xFF); 459 | this.writeByte((pValue >> 8) & 0xFF); 460 | }; 461 | 462 | /* 463 | Encodes and writes pixel data 464 | */ 465 | GIFEncoder.prototype.writePixels = function() { 466 | var enc = new LZWEncoder(this.width, this.height, this.indexedPixels, this.colorDepth); 467 | enc.encode(this); 468 | }; 469 | 470 | /* 471 | Retrieves the GIF stream 472 | */ 473 | GIFEncoder.prototype.stream = function() { 474 | return this; 475 | }; 476 | 477 | GIFEncoder.ByteCapacitor = ByteCapacitor; 478 | 479 | module.exports = GIFEncoder; 480 | --------------------------------------------------------------------------------