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 |