| OLD | NEW |
| (Empty) |
| 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 */ | |
| 9 | |
| 10 var NeuQuant = require('./TypedNeuQuant.js'); | |
| 11 var LZWEncoder = require('./LZWEncoder.js'); | |
| 12 | |
| 13 function ByteArray() { | |
| 14 this.page = -1; | |
| 15 this.pages = []; | |
| 16 this.newPage(); | |
| 17 } | |
| 18 | |
| 19 ByteArray.pageSize = 4096; | |
| 20 ByteArray.charMap = {}; | |
| 21 | |
| 22 for (var i = 0; i < 256; i++) | |
| 23 ByteArray.charMap[i] = String.fromCharCode(i); | |
| 24 | |
| 25 ByteArray.prototype.newPage = function() { | |
| 26 this.pages[++this.page] = new Uint8Array(ByteArray.pageSize); | |
| 27 this.cursor = 0; | |
| 28 }; | |
| 29 | |
| 30 ByteArray.prototype.getData = function() { | |
| 31 var rv = ''; | |
| 32 for (var p = 0; p < this.pages.length; p++) { | |
| 33 for (var i = 0; i < ByteArray.pageSize; i++) { | |
| 34 rv += ByteArray.charMap[this.pages[p][i]]; | |
| 35 } | |
| 36 } | |
| 37 return rv; | |
| 38 }; | |
| 39 | |
| 40 ByteArray.prototype.writeByte = function(val) { | |
| 41 if (this.cursor >= ByteArray.pageSize) this.newPage(); | |
| 42 this.pages[this.page][this.cursor++] = val; | |
| 43 }; | |
| 44 | |
| 45 ByteArray.prototype.writeUTFBytes = function(string) { | |
| 46 for (var l = string.length, i = 0; i < l; i++) | |
| 47 this.writeByte(string.charCodeAt(i)); | |
| 48 }; | |
| 49 | |
| 50 ByteArray.prototype.writeBytes = function(array, offset, length) { | |
| 51 for (var l = length || array.length, i = offset || 0; i < l; i++) | |
| 52 this.writeByte(array[i]); | |
| 53 }; | |
| 54 | |
| 55 function GIFEncoder(width, height) { | |
| 56 // image size | |
| 57 this.width = ~~width; | |
| 58 this.height = ~~height; | |
| 59 | |
| 60 // transparent color if given | |
| 61 this.transparent = null; | |
| 62 | |
| 63 // transparent index in color table | |
| 64 this.transIndex = 0; | |
| 65 | |
| 66 // -1 = no repeat, 0 = forever. anything else is repeat count | |
| 67 this.repeat = -1; | |
| 68 | |
| 69 // frame delay (hundredths) | |
| 70 this.delay = 0; | |
| 71 | |
| 72 this.image = null; // current frame | |
| 73 this.pixels = null; // BGR byte array from frame | |
| 74 this.indexedPixels = null; // converted frame indexed to palette | |
| 75 this.colorDepth = null; // number of bit planes | |
| 76 this.colorTab = null; // RGB palette | |
| 77 this.usedEntry = new Array(); // active palette entries | |
| 78 this.palSize = 7; // color table size (bits-1) | |
| 79 this.dispose = -1; // disposal code (-1 = use default) | |
| 80 this.firstFrame = true; | |
| 81 this.sample = 10; // default sample interval for quantizer | |
| 82 | |
| 83 this.out = new ByteArray(); | |
| 84 } | |
| 85 | |
| 86 /* | |
| 87 Sets the delay time between each frame, or changes it for subsequent frames | |
| 88 (applies to last frame added) | |
| 89 */ | |
| 90 GIFEncoder.prototype.setDelay = function(milliseconds) { | |
| 91 this.delay = Math.round(milliseconds / 10); | |
| 92 }; | |
| 93 | |
| 94 /* | |
| 95 Sets frame rate in frames per second. | |
| 96 */ | |
| 97 GIFEncoder.prototype.setFrameRate = function(fps) { | |
| 98 this.delay = Math.round(100 / fps); | |
| 99 }; | |
| 100 | |
| 101 /* | |
| 102 Sets the GIF frame disposal code for the last added frame and any | |
| 103 subsequent frames. | |
| 104 | |
| 105 Default is 0 if no transparent color has been set, otherwise 2. | |
| 106 */ | |
| 107 GIFEncoder.prototype.setDispose = function(disposalCode) { | |
| 108 if (disposalCode >= 0) this.dispose = disposalCode; | |
| 109 }; | |
| 110 | |
| 111 /* | |
| 112 Sets the number of times the set of GIF frames should be played. | |
| 113 | |
| 114 -1 = play once | |
| 115 0 = repeat indefinitely | |
| 116 | |
| 117 Default is -1 | |
| 118 | |
| 119 Must be invoked before the first image is added | |
| 120 */ | |
| 121 | |
| 122 GIFEncoder.prototype.setRepeat = function(repeat) { | |
| 123 this.repeat = repeat; | |
| 124 }; | |
| 125 | |
| 126 /* | |
| 127 Sets the transparent color for the last added frame and any subsequent | |
| 128 frames. Since all colors are subject to modification in the quantization | |
| 129 process, the color in the final palette for each frame closest to the given | |
| 130 color becomes the transparent color for that frame. May be set to null to | |
| 131 indicate no transparent color. | |
| 132 */ | |
| 133 GIFEncoder.prototype.setTransparent = function(color) { | |
| 134 this.transparent = color; | |
| 135 }; | |
| 136 | |
| 137 /* | |
| 138 Adds next GIF frame. The frame is not written immediately, but is | |
| 139 actually deferred until the next frame is received so that timing | |
| 140 data can be inserted. Invoking finish() flushes all frames. | |
| 141 */ | |
| 142 GIFEncoder.prototype.addFrame = function(imageData) { | |
| 143 this.image = imageData; | |
| 144 | |
| 145 this.getImagePixels(); // convert to correct format if necessary | |
| 146 this.analyzePixels(); // build color table & map pixels | |
| 147 | |
| 148 if (this.firstFrame) { | |
| 149 this.writeLSD(); // logical screen descriptior | |
| 150 this.writePalette(); // global color table | |
| 151 if (this.repeat >= 0) { | |
| 152 // use NS app extension to indicate reps | |
| 153 this.writeNetscapeExt(); | |
| 154 } | |
| 155 } | |
| 156 | |
| 157 this.writeGraphicCtrlExt(); // write graphic control extension | |
| 158 this.writeImageDesc(); // image descriptor | |
| 159 if (!this.firstFrame) this.writePalette(); // local color table | |
| 160 this.writePixels(); // encode and write pixel data | |
| 161 | |
| 162 this.firstFrame = false; | |
| 163 }; | |
| 164 | |
| 165 /* | |
| 166 Adds final trailer to the GIF stream, if you don't call the finish method | |
| 167 the GIF stream will not be valid. | |
| 168 */ | |
| 169 GIFEncoder.prototype.finish = function() { | |
| 170 this.out.writeByte(0x3b); // gif trailer | |
| 171 }; | |
| 172 | |
| 173 /* | |
| 174 Sets quality of color quantization (conversion of images to the maximum 256 | |
| 175 colors allowed by the GIF specification). Lower values (minimum = 1) | |
| 176 produce better colors, but slow processing significantly. 10 is the | |
| 177 default, and produces good color mapping at reasonable speeds. Values | |
| 178 greater than 20 do not yield significant improvements in speed. | |
| 179 */ | |
| 180 GIFEncoder.prototype.setQuality = function(quality) { | |
| 181 if (quality < 1) quality = 1; | |
| 182 this.sample = quality; | |
| 183 }; | |
| 184 | |
| 185 /* | |
| 186 Writes GIF file header | |
| 187 */ | |
| 188 GIFEncoder.prototype.writeHeader = function() { | |
| 189 this.out.writeUTFBytes("GIF89a"); | |
| 190 }; | |
| 191 | |
| 192 /* | |
| 193 Analyzes current frame colors and creates color map. | |
| 194 */ | |
| 195 GIFEncoder.prototype.analyzePixels = function() { | |
| 196 var len = this.pixels.length; | |
| 197 var nPix = len / 3; | |
| 198 | |
| 199 this.indexedPixels = new Uint8Array(nPix); | |
| 200 | |
| 201 var imgq = new NeuQuant(this.pixels, this.sample); | |
| 202 imgq.buildColormap(); // create reduced palette | |
| 203 this.colorTab = imgq.getColormap(); | |
| 204 | |
| 205 // map image pixels to new palette | |
| 206 var k = 0; | |
| 207 for (var j = 0; j < nPix; j++) { | |
| 208 var index = imgq.lookupRGB( | |
| 209 this.pixels[k++] & 0xff, | |
| 210 this.pixels[k++] & 0xff, | |
| 211 this.pixels[k++] & 0xff | |
| 212 ); | |
| 213 this.usedEntry[index] = true; | |
| 214 this.indexedPixels[j] = index; | |
| 215 } | |
| 216 | |
| 217 this.pixels = null; | |
| 218 this.colorDepth = 8; | |
| 219 this.palSize = 7; | |
| 220 | |
| 221 // get closest match to transparent color if specified | |
| 222 if (this.transparent !== null) { | |
| 223 this.transIndex = this.findClosest(this.transparent); | |
| 224 } | |
| 225 }; | |
| 226 | |
| 227 /* | |
| 228 Returns index of palette color closest to c | |
| 229 */ | |
| 230 GIFEncoder.prototype.findClosest = function(c) { | |
| 231 if (this.colorTab === null) return -1; | |
| 232 | |
| 233 var r = (c & 0xFF0000) >> 16; | |
| 234 var g = (c & 0x00FF00) >> 8; | |
| 235 var b = (c & 0x0000FF); | |
| 236 var minpos = 0; | |
| 237 var dmin = 256 * 256 * 256; | |
| 238 var len = this.colorTab.length; | |
| 239 | |
| 240 for (var i = 0; i < len;) { | |
| 241 var dr = r - (this.colorTab[i++] & 0xff); | |
| 242 var dg = g - (this.colorTab[i++] & 0xff); | |
| 243 var db = b - (this.colorTab[i] & 0xff); | |
| 244 var d = dr * dr + dg * dg + db * db; | |
| 245 var index = parseInt(i / 3); | |
| 246 if (this.usedEntry[index] && (d < dmin)) { | |
| 247 dmin = d; | |
| 248 minpos = index; | |
| 249 } | |
| 250 i++; | |
| 251 } | |
| 252 | |
| 253 return minpos; | |
| 254 }; | |
| 255 | |
| 256 /* | |
| 257 Extracts image pixels into byte array pixels | |
| 258 (removes alphachannel from canvas imagedata) | |
| 259 */ | |
| 260 GIFEncoder.prototype.getImagePixels = function() { | |
| 261 var w = this.width; | |
| 262 var h = this.height; | |
| 263 this.pixels = new Uint8Array(w * h * 3); | |
| 264 | |
| 265 var data = this.image; | |
| 266 var count = 0; | |
| 267 | |
| 268 for (var i = 0; i < h; i++) { | |
| 269 for (var j = 0; j < w; j++) { | |
| 270 var b = (i * w * 4) + j * 4; | |
| 271 this.pixels[count++] = data[b]; | |
| 272 this.pixels[count++] = data[b+1]; | |
| 273 this.pixels[count++] = data[b+2]; | |
| 274 } | |
| 275 } | |
| 276 }; | |
| 277 | |
| 278 /* | |
| 279 Writes Graphic Control Extension | |
| 280 */ | |
| 281 GIFEncoder.prototype.writeGraphicCtrlExt = function() { | |
| 282 this.out.writeByte(0x21); // extension introducer | |
| 283 this.out.writeByte(0xf9); // GCE label | |
| 284 this.out.writeByte(4); // data block size | |
| 285 | |
| 286 var transp, disp; | |
| 287 if (this.transparent === null) { | |
| 288 transp = 0; | |
| 289 disp = 0; // dispose = no action | |
| 290 } else { | |
| 291 transp = 1; | |
| 292 disp = 2; // force clear if using transparent color | |
| 293 } | |
| 294 | |
| 295 if (this.dispose >= 0) { | |
| 296 disp = dispose & 7; // user override | |
| 297 } | |
| 298 disp <<= 2; | |
| 299 | |
| 300 // packed fields | |
| 301 this.out.writeByte( | |
| 302 0 | // 1:3 reserved | |
| 303 disp | // 4:6 disposal | |
| 304 0 | // 7 user input - 0 = none | |
| 305 transp // 8 transparency flag | |
| 306 ); | |
| 307 | |
| 308 this.writeShort(this.delay); // delay x 1/100 sec | |
| 309 this.out.writeByte(this.transIndex); // transparent color index | |
| 310 this.out.writeByte(0); // block terminator | |
| 311 }; | |
| 312 | |
| 313 /* | |
| 314 Writes Image Descriptor | |
| 315 */ | |
| 316 GIFEncoder.prototype.writeImageDesc = function() { | |
| 317 this.out.writeByte(0x2c); // image separator | |
| 318 this.writeShort(0); // image position x,y = 0,0 | |
| 319 this.writeShort(0); | |
| 320 this.writeShort(this.width); // image size | |
| 321 this.writeShort(this.height); | |
| 322 | |
| 323 // packed fields | |
| 324 if (this.firstFrame) { | |
| 325 // no LCT - GCT is used for first (or only) frame | |
| 326 this.out.writeByte(0); | |
| 327 } else { | |
| 328 // specify normal LCT | |
| 329 this.out.writeByte( | |
| 330 0x80 | // 1 local color table 1=yes | |
| 331 0 | // 2 interlace - 0=no | |
| 332 0 | // 3 sorted - 0=no | |
| 333 0 | // 4-5 reserved | |
| 334 this.palSize // 6-8 size of color table | |
| 335 ); | |
| 336 } | |
| 337 }; | |
| 338 | |
| 339 /* | |
| 340 Writes Logical Screen Descriptor | |
| 341 */ | |
| 342 GIFEncoder.prototype.writeLSD = function() { | |
| 343 // logical screen size | |
| 344 this.writeShort(this.width); | |
| 345 this.writeShort(this.height); | |
| 346 | |
| 347 // packed fields | |
| 348 this.out.writeByte( | |
| 349 0x80 | // 1 : global color table flag = 1 (gct used) | |
| 350 0x70 | // 2-4 : color resolution = 7 | |
| 351 0x00 | // 5 : gct sort flag = 0 | |
| 352 this.palSize // 6-8 : gct size | |
| 353 ); | |
| 354 | |
| 355 this.out.writeByte(0); // background color index | |
| 356 this.out.writeByte(0); // pixel aspect ratio - assume 1:1 | |
| 357 }; | |
| 358 | |
| 359 /* | |
| 360 Writes Netscape application extension to define repeat count. | |
| 361 */ | |
| 362 GIFEncoder.prototype.writeNetscapeExt = function() { | |
| 363 this.out.writeByte(0x21); // extension introducer | |
| 364 this.out.writeByte(0xff); // app extension label | |
| 365 this.out.writeByte(11); // block size | |
| 366 this.out.writeUTFBytes('NETSCAPE2.0'); // app id + auth code | |
| 367 this.out.writeByte(3); // sub-block size | |
| 368 this.out.writeByte(1); // loop sub-block id | |
| 369 this.writeShort(this.repeat); // loop count (extra iterations, 0=repeat foreve
r) | |
| 370 this.out.writeByte(0); // block terminator | |
| 371 }; | |
| 372 | |
| 373 /* | |
| 374 Writes color table | |
| 375 */ | |
| 376 GIFEncoder.prototype.writePalette = function() { | |
| 377 this.out.writeBytes(this.colorTab); | |
| 378 var n = (3 * 256) - this.colorTab.length; | |
| 379 for (var i = 0; i < n; i++) | |
| 380 this.out.writeByte(0); | |
| 381 }; | |
| 382 | |
| 383 GIFEncoder.prototype.writeShort = function(pValue) { | |
| 384 this.out.writeByte(pValue & 0xFF); | |
| 385 this.out.writeByte((pValue >> 8) & 0xFF); | |
| 386 }; | |
| 387 | |
| 388 /* | |
| 389 Encodes and writes pixel data | |
| 390 */ | |
| 391 GIFEncoder.prototype.writePixels = function() { | |
| 392 var enc = new LZWEncoder(this.width, this.height, this.indexedPixels, this.col
orDepth); | |
| 393 enc.encode(this.out); | |
| 394 }; | |
| 395 | |
| 396 /* | |
| 397 Retrieves the GIF stream | |
| 398 */ | |
| 399 GIFEncoder.prototype.stream = function() { | |
| 400 return this.out; | |
| 401 }; | |
| 402 | |
| 403 module.exports = GIFEncoder; | |
| OLD | NEW |