├── test ├── test.gif ├── js │ └── index.html ├── build.hxml └── Test.hx ├── haxelib.json ├── README.md └── gif ├── LzwEncoder.hx ├── GifEncoder.hx └── NeuQuant.hx /test/test.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snowkit/gif/HEAD/test/test.gif -------------------------------------------------------------------------------- /test/js/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Here should be an animated GIF:

4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gif", 3 | "url": "https://github.com/snowkit/gif", 4 | "license": "MIT", 5 | "tags": [], 6 | "description": "A haxe port of the LZW and NeuQuant gif encoding algorithms", 7 | "version": "1.0.0", 8 | "releasenote": "initial commit", 9 | "contributors": [ "underscorediscovery", "keymaster" ], 10 | "dependencies": {} 11 | } 12 | -------------------------------------------------------------------------------- /test/build.hxml: -------------------------------------------------------------------------------- 1 | -main Test.hx 2 | -cpp cpp/ 3 | -cp ../ 4 | 5 | --next 6 | 7 | -main Test.hx 8 | -js js/Test.js 9 | -cp ../ 10 | 11 | # pick a target 12 | 13 | # -D mac 14 | #-D windows 15 | #-D linux 16 | 17 | # To build 32 or 64 bit 18 | #(if not using the default for your platform): 19 | 20 | #-D HXCPP_M64 21 | #-D HXCPP_M32 22 | 23 | # -cmd ./cpp/Test 24 | #-cmd ./cpp/Test.exe 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GIF 2 | --- 3 | 4 | **A GIF format encoder.** 5 | This only deals with the encoding (writing) and not reading of GIF files (see [format](https://github.com/haxefoundation/format)). 6 | 7 | Haxe port by [KeyMaster-](https://github.com/KeyMaster-) and [underscorediscovery](https://github.com/underscorediscovery) 8 | from [Chman/Moments](https://github.com/Chman/Moments) 9 | 10 | **LICENSE**: The individual files are licensed accordingly. 11 | **REQUIREMENTS**: Haxe 3.2+, no dependencies 12 | 13 | --- 14 | 15 | ### Install 16 | 17 | `haxelib git gif https://github.com/snowkit/gif.git` 18 | 19 | Then, add `gif` as a library dependency to your project. 20 | 21 | ### Simple usage 22 | 23 | See `test/Test.hx` for an example! 24 | 25 | ### Helpers 26 | 27 | See https://github.com/snowkit/gifcapture 28 | -------------------------------------------------------------------------------- /test/Test.hx: -------------------------------------------------------------------------------- 1 | 2 | import gif.GifEncoder; 3 | 4 | class Test { 5 | 6 | static var width = 32; 7 | static var height = 32; 8 | static var delay = 1; 9 | 10 | static function main() { 11 | 12 | trace("creating test.gif ..."); 13 | 14 | var output = new haxe.io.BytesOutput(); 15 | var encoder = new gif.GifEncoder(width, height, 1, GifRepeat.Infinite, GifQuality.High); 16 | 17 | encoder.start(output); 18 | 19 | //add 4 frames of random colors 20 | encoder.add(output, make_frame()); 21 | encoder.add(output, make_frame()); 22 | encoder.add(output, make_frame()); 23 | encoder.add(output, make_frame()); 24 | 25 | encoder.commit(output); 26 | 27 | var bytes = output.getBytes(); 28 | 29 | #if sys 30 | sys.io.File.saveBytes("test.gif", bytes); 31 | #elseif js 32 | var imageElement :js.html.ImageElement = cast js.Browser.document.createElement("img"); 33 | js.Browser.document.body.appendChild(imageElement); 34 | imageElement.src = 'data:image/gif;base64,' + haxe.crypto.Base64.encode(bytes); 35 | #else 36 | throw 'Unsupported platform!'; 37 | #end 38 | 39 | trace("done."); 40 | 41 | } //main 42 | 43 | static function make_frame() { 44 | 45 | var red = Std.random(255); 46 | var blue = Std.random(255); 47 | var green = Std.random(255); 48 | 49 | var pixels = new haxe.io.UInt8Array(width * height * 3); 50 | for(i in 0 ... width * height) { 51 | pixels[i * 3 + 0] = red; 52 | pixels[i * 3 + 1] = green; 53 | pixels[i * 3 + 2] = blue; 54 | } 55 | 56 | var frame: GifFrame = { 57 | delay: delay, 58 | flippedY: false, 59 | data: pixels 60 | } 61 | 62 | return frame; 63 | 64 | } //make_frame 65 | 66 | } //Test 67 | -------------------------------------------------------------------------------- /gif/LzwEncoder.hx: -------------------------------------------------------------------------------- 1 | package gif; 2 | 3 | /* 4 | * No copyright asserted on the source code of this class. May be used 5 | * for any purpose, however, refer to the Unisys LZW patent for restrictions 6 | * on use of the associated LZWEncoder class : 7 | * 8 | * The Unisys patent expired on 20 June 2003 in the USA, in Europe it expired 9 | * on 18 June 2004, in Japan the patent expired on 20 June 2004 and in Canada 10 | * it expired on 7 July 2004. The U.S. IBM patent expired 11 August 2006, The 11 | * Software Freedom Law Center says that after 1 October 2006, there will be 12 | * no significant patent claims interfering with employment of the GIF format. 13 | * 14 | * Original code by Kevin Weiner, FM Software. 15 | * Adapted from Jef Poskanzer's Java port by way of J. M. G. Elliott. 16 | * Ported to Haxe by Tilman Schmidt and Sven Bergström 17 | * 18 | */ 19 | 20 | import haxe.io.Int32Array; 21 | import haxe.io.UInt8Array; 22 | 23 | class LzwEncoder { 24 | static var EOF(default, never):Int = -1; 25 | 26 | var pixAry:UInt8Array; 27 | var initCodeSize:Int; 28 | var curPixel:Int; 29 | 30 | // GIFCOMPR.C - GIF Image compression routines 31 | // 32 | // Lempel-Ziv compression based on 'compress'. GIF modifications by 33 | // David Rowley (mgardi@watdcsu.waterloo.edu) 34 | 35 | // General DEFINEs 36 | 37 | static var BITS(default, never):Int = 12; 38 | 39 | static var HSIZE(default, never):Int = 5003; // 80% occupancy 40 | 41 | // GIF Image compression - modified 'compress' 42 | // 43 | // Based on: compress.c - File compression ala IEEE Computer, June 1984. 44 | // 45 | // By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas) 46 | // Jim McKie (decvax!mcvax!jim) 47 | // Steve Davies (decvax!vax135!petsd!peora!srd) 48 | // Ken Turkowski (decvax!decwrl!turtlevax!ken) 49 | // James A. Woods (decvax!ihnp4!ames!jaw) 50 | // Joe Orost (decvax!vax135!petsd!joe) 51 | 52 | var n_bits:Int; // number of bits/code 53 | var maxbits:Int = BITS; // user settable max # bits/code 54 | var maxcode:Int; // maximum code, given n_bits 55 | var maxmaxcode:Int = 1 << BITS; // should NEVER generate this code 56 | 57 | var htab:Int32Array; 58 | var codetab:Int32Array; 59 | 60 | var hsize:Int = HSIZE; // for dynamic table sizing 61 | 62 | var free_ent:Int = 0; // first unused entry 63 | 64 | // block compression parameters -- after all codes are used up, 65 | // and compression rate changes, start over. 66 | var clear_flg:Bool = false; 67 | 68 | // Algorithm: use open addressing double hashing (no chaining) on the 69 | // prefix code / next character combination. We do a variant of Knuth's 70 | // algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime 71 | // secondary probe. Here, the modular division first probe is gives way 72 | // to a faster exclusive-or manipulation. Also do block compression with 73 | // an adaptive reset, whereby the code table is cleared when the compression 74 | // ratio decreases, but after the table fills. The variable-length output 75 | // codes are re-sized at this point, and a special CLEAR code is generated 76 | // for the decompressor. Late addition: construct the table according to 77 | // file size for noticeable speed improvement on small files. Please direct 78 | // questions about this implementation to ames!jaw. 79 | 80 | var g_init_bits:Int; 81 | 82 | var ClearCode:Int; 83 | var EOFCode:Int; 84 | 85 | // output 86 | // 87 | // output the given code. 88 | // Inputs: 89 | // code: A n_bits-bit integer. If == -1, then EOF. This assumes 90 | // that n_bits =< wordsize - 1. 91 | // outputs: 92 | // outputs code to the file. 93 | // Assumptions: 94 | // Chars are 8 bits long. 95 | // Algorithm: 96 | // Maintain a BITS character long buffer (so that 8 codes will 97 | // fit in it exactly). Use the VAX insv instruction to insert each 98 | // code in turn. When the buffer fills up empty it and start over. 99 | 100 | var cur_accum:Int = 0; 101 | var cur_bits:Int = 0; 102 | 103 | var masks:Array = 104 | [ 105 | 0x0000, 106 | 0x0001, 107 | 0x0003, 108 | 0x0007, 109 | 0x000F, 110 | 0x001F, 111 | 0x003F, 112 | 0x007F, 113 | 0x00FF, 114 | 0x01FF, 115 | 0x03FF, 116 | 0x07FF, 117 | 0x0FFF, 118 | 0x1FFF, 119 | 0x3FFF, 120 | 0x7FFF, 121 | 0xFFFF ]; 122 | 123 | // Number of characters so far in this 'packet' 124 | var a_count:Int; 125 | 126 | // Define the storage for the packet accumulator 127 | var accum:UInt8Array; 128 | 129 | //---------------------------------------------------------------------------- 130 | public function new() 131 | { 132 | htab = new Int32Array(HSIZE); 133 | codetab = new Int32Array(HSIZE); 134 | accum = new UInt8Array(256); 135 | } 136 | 137 | //Reset the encoder to new pixel data and default values 138 | public function reset(pixels:UInt8Array, color_depth:Int) { //width and height used to be passed in though they were never used 139 | pixAry = pixels; 140 | initCodeSize = Std.int(Math.max(2, color_depth)); 141 | 142 | maxbits = BITS; 143 | maxmaxcode = 1 << BITS; 144 | hsize = HSIZE; 145 | free_ent = 0; 146 | clear_flg = false; 147 | cur_accum = 0; 148 | cur_bits = 0; 149 | } 150 | 151 | // add a character to the end of the current packet, and if it is 254 152 | // characters, flush the packet to disk. 153 | function add(c:UInt, out:haxe.io.Output):Void 154 | { 155 | accum[a_count++] = c; 156 | if (a_count >= 254) 157 | flush(out); 158 | } 159 | 160 | // Clear out the hash table 161 | 162 | // table clear for block compress 163 | function clearTable(out:haxe.io.Output):Void 164 | { 165 | resetCodeTable(hsize); 166 | free_ent = ClearCode + 2; 167 | clear_flg = true; 168 | 169 | output(ClearCode, out); 170 | } 171 | 172 | // reset code table 173 | function resetCodeTable(hsize:Int):Void 174 | { 175 | for (i in 0...hsize) 176 | htab[i] = -1; 177 | } 178 | 179 | function compress(init_bits:Int, out:haxe.io.Output):Void 180 | { 181 | var fcode:Int; 182 | var i:Int /* = 0 */; 183 | var c:Int; 184 | var ent:Int; 185 | var disp:Int; 186 | var hsize_reg:Int; 187 | var hshift:Int; 188 | 189 | // Set up the globals: g_init_bits - initial number of bits 190 | g_init_bits = init_bits; 191 | 192 | // Set up the necessary values 193 | clear_flg = false; 194 | n_bits = g_init_bits; 195 | maxcode = maxCode(n_bits); 196 | 197 | ClearCode = 1 << (init_bits - 1); 198 | EOFCode = ClearCode + 1; 199 | free_ent = ClearCode + 2; 200 | 201 | a_count = 0; // clear packet 202 | 203 | ent = nextPixel(); 204 | 205 | hshift = 0; 206 | fcode = hsize; 207 | while (fcode < 65536) { 208 | ++hshift; 209 | fcode *= 2; 210 | } 211 | 212 | hshift = 8 - hshift; // set hash code range bound 213 | 214 | hsize_reg = hsize; 215 | resetCodeTable(hsize_reg); // clear hash table 216 | 217 | output(ClearCode, out); 218 | 219 | while ((c = nextPixel()) != EOF) 220 | { 221 | fcode = (c << maxbits) + ent; 222 | i = (c << hshift) ^ ent; // xor hashing 223 | 224 | if (htab[i] == fcode) 225 | { 226 | ent = codetab[i]; 227 | continue; 228 | } 229 | else if (htab[i] >= 0) // non-empty slot 230 | { 231 | disp = hsize_reg - i; // secondary hash (after G. Knott) 232 | if (i == 0) 233 | disp = 1; 234 | do 235 | { 236 | if ((i -= disp) < 0) 237 | i += hsize_reg; 238 | 239 | if (htab[i] == fcode) 240 | { 241 | ent = codetab[i]; 242 | break; 243 | } 244 | } while (htab[i] >= 0); 245 | if (htab[i] == fcode) continue; 246 | } 247 | output(ent, out); 248 | ent = c; 249 | if (free_ent < maxmaxcode) 250 | { 251 | codetab[i] = free_ent++; // code -> hashtable 252 | htab[i] = fcode; 253 | } 254 | else 255 | clearTable(out); 256 | } 257 | // Put out the final code. 258 | output(ent, out); 259 | output(EOFCode, out); 260 | } 261 | 262 | //---------------------------------------------------------------------------- 263 | public function encode(os:haxe.io.Output):Void 264 | { 265 | os.writeByte( initCodeSize ); // write "initial code size" byte 266 | curPixel = 0; 267 | compress(initCodeSize + 1, os); // compress and write the pixel data 268 | os.writeByte(0); // write block terminator 269 | } 270 | 271 | // flush the packet to disk, and reset the accumulator 272 | function flush(out:haxe.io.Output):Void 273 | { 274 | if (a_count > 0) 275 | { 276 | out.writeByte(a_count); 277 | out.writeBytes(accum.view.buffer, 0, a_count); 278 | a_count = 0; 279 | } 280 | } 281 | 282 | inline function maxCode(n_bits:Int):Int 283 | { 284 | return (1 << n_bits) - 1; 285 | } 286 | 287 | //---------------------------------------------------------------------------- 288 | // Return the next pixel from the image 289 | //---------------------------------------------------------------------------- 290 | function nextPixel():Int 291 | { 292 | if (curPixel == pixAry.length) 293 | return EOF; 294 | 295 | curPixel++; 296 | return pixAry[curPixel - 1] & 0xff; 297 | } 298 | 299 | function output(code:Int, out:haxe.io.Output):Void 300 | { 301 | cur_accum &= masks[cur_bits]; 302 | 303 | if (cur_bits > 0) 304 | cur_accum |= (code << cur_bits); 305 | else 306 | cur_accum = code; 307 | 308 | cur_bits += n_bits; 309 | 310 | while (cur_bits >= 8) 311 | { 312 | add(cur_accum & 0xff, out); 313 | cur_accum >>= 8; 314 | cur_bits -= 8; 315 | } 316 | 317 | // If the next entry is going to be too big for the code size, 318 | // then increase it, if possible. 319 | if (free_ent > maxcode || clear_flg) 320 | { 321 | if (clear_flg) 322 | { 323 | maxcode = maxCode(n_bits = g_init_bits); 324 | clear_flg = false; 325 | } 326 | else 327 | { 328 | ++n_bits; 329 | if (n_bits == maxbits) 330 | maxcode = maxmaxcode; 331 | else 332 | maxcode = maxCode(n_bits); 333 | } 334 | } 335 | 336 | if (code == EOFCode) 337 | { 338 | // At EOF, write the rest of the buffer. 339 | while (cur_bits > 0) 340 | { 341 | add(cur_accum & 0xff, out); 342 | cur_accum >>= 8; 343 | cur_bits -= 8; 344 | } 345 | 346 | flush(out); 347 | } 348 | } 349 | 350 | } 351 | -------------------------------------------------------------------------------- /gif/GifEncoder.hx: -------------------------------------------------------------------------------- 1 | package gif; 2 | 3 | /* 4 | * No copyright asserted on the source code of this class. May be used 5 | * for any purpose. 6 | * 7 | * Original code by Kevin Weiner, FM Software. 8 | * Adapted by Thomas Hourdel (https://github.com/Chman/Moments) 9 | * Ported to Haxe by Tilman Schmidt and Sven Bergström 10 | */ 11 | 12 | import haxe.io.UInt8Array; 13 | import haxe.io.BytesOutput; 14 | 15 | @:enum abstract GifRepeat(Int) 16 | from Int to Int { 17 | var None = 0; 18 | var Infinite = -1; 19 | } 20 | 21 | @:enum abstract GifQuality(Int) 22 | from Int to Int { 23 | var Best = 1; 24 | var VeryHigh = 10; 25 | var QuiteHigh = 20; 26 | var High = 35; 27 | var Mid = 50; 28 | var Low = 65; 29 | var QuiteLow = 80; 30 | var VeryLow = 90; 31 | var Worst = 100; 32 | } 33 | 34 | class GifEncoder { 35 | 36 | var width: Int; 37 | var height: Int; 38 | var framerate: Float = 24; // used if frame.delay < 0 39 | var repeat: Int = -1; // -1: infinite, 0: none, >0: repeat count 40 | 41 | var colorDepth: Int = 8; // Number of bit planes 42 | var paletteSize: Int = 7; // Color table size (bits-1) 43 | var sampleInterval: Int = 10; // Default sample interval for quantizer 44 | 45 | //caches 46 | var pixels: UInt8Array; 47 | var indexedPixels: UInt8Array; // Converted frame indexed to palette 48 | var colorTab: UInt8Array; // RGB palette 49 | var usedEntry: Array; // Active palette entries 50 | // 51 | var nq: NeuQuant; 52 | var lzwEncoder: LzwEncoder; 53 | //internal 54 | var started: Bool = false; 55 | var first_frame: Bool = true; 56 | 57 | //:todo: error handling could be better - but throw inside of another thread on cpp is too quiet 58 | 59 | /** Allows a custom print handler for error messages. 60 | Defaults to Sys.println on sys targets, and trace otherwise. */ 61 | public var print: Dynamic->Void; 62 | 63 | // Public API 64 | 65 | /** Construct a gif encoder with options: 66 | 67 | frame width/height: 68 | Default is 0, required 69 | 70 | framerate: 71 | This is used if an added frame has a delay that is negative. 72 | 73 | repeat: 74 | Default is 0 (no repeat); -1 means play indefinitely. 75 | Use GifRepeat for clarity 76 | 77 | quality: 78 | Sets quality of color quantization (conversion of images to 79 | the maximum 256 colors allowed by the GIF specification). Lower values (minimum = 1) 80 | produce better colors, but slow processing significantly. Higher values will speed 81 | up the quantization pass at the cost of lower image quality (maximum = 100). */ 82 | public function new( 83 | _frame_width:Int, 84 | _frame_height:Int, 85 | _framerate:Float, 86 | _repeat:Int = GifRepeat.Infinite, 87 | _quality:Int = 10 88 | ) { 89 | 90 | #if sys 91 | print = Sys.println; 92 | #else 93 | print = function(v) { trace(v); } 94 | #end 95 | 96 | width = _frame_width; 97 | height = _frame_height; 98 | framerate = _framerate; 99 | repeat = _repeat; 100 | 101 | sampleInterval = Std.int(clamp(_quality, 1, 100)); 102 | usedEntry = [for (i in 0...256) false]; 103 | 104 | pixels = new UInt8Array(width * height * 3); 105 | indexedPixels = new UInt8Array(width * height); 106 | 107 | nq = new NeuQuant(); 108 | lzwEncoder = new LzwEncoder(); 109 | 110 | } //new 111 | 112 | public function start(output:BytesOutput) : Void { 113 | 114 | if(output == null) { 115 | print("gif: start() output must not be null."); 116 | return; 117 | } 118 | 119 | output.writeString("GIF89a"); 120 | 121 | write_LSD(output); 122 | 123 | started = true; 124 | 125 | } //start 126 | 127 | public function add(output:BytesOutput, frame:GifFrame) : Void { 128 | 129 | if(output == null) { 130 | print("gif: add() output must not be null."); 131 | return; 132 | } 133 | 134 | if(!started) { 135 | print("gif: add() requires start to be called before adding frames."); 136 | return; 137 | } 138 | 139 | var pixels = get_pixels(frame); 140 | analyze(pixels); 141 | 142 | if(first_frame) { 143 | 144 | write_palette(output); 145 | 146 | if(repeat != GifRepeat.None) { 147 | write_NetscapeExt(output); 148 | } 149 | 150 | first_frame = false; 151 | 152 | } //first_frame 153 | 154 | var delay = if(frame.delay < 0) { 155 | 1.0/framerate; 156 | } else { 157 | frame.delay; 158 | } 159 | 160 | write_GraphicControlExt(output, delay); 161 | write_image_desc(output, first_frame); 162 | 163 | if(!first_frame) { 164 | write_palette(output); 165 | } 166 | 167 | write_pixels(output); 168 | 169 | } //add 170 | 171 | public function commit(output:BytesOutput) : Void { 172 | 173 | if(output == null) { 174 | print("gif: commit() output must be not null."); 175 | return; 176 | } 177 | 178 | if(!started) { 179 | print("gif: commit() called without start() being called first."); 180 | return; 181 | } 182 | 183 | output.writeByte(0x3b); // Gif trailer 184 | output.flush(); 185 | output.close(); 186 | 187 | started = false; 188 | first_frame = true; 189 | 190 | } //commit 191 | 192 | //helpers 193 | 194 | function get_pixels(frame:GifFrame):UInt8Array { 195 | 196 | //if not flipped we can use the data as is 197 | if (!frame.flippedY) return frame.data; 198 | 199 | //otherwise flip it, and return the cached array 200 | var stride = width * 3; 201 | for(y in 0...height) { 202 | var begin = (height - 1 - y) * stride; 203 | pixels.view.buffer.blit(y * stride, frame.data.view.buffer, begin, stride); 204 | } 205 | 206 | return pixels; 207 | 208 | } //get_pixels 209 | 210 | function analyze(pixels:UInt8Array) { 211 | 212 | // Create reduced palette 213 | nq.reset(pixels, pixels.length, sampleInterval); 214 | colorTab = nq.process(); 215 | 216 | // Map image pixels to new palette 217 | var k:Int = 0; 218 | for (i in 0...(width * height)) { 219 | var r = pixels[k++] & 0xff; 220 | var g = pixels[k++] & 0xff; 221 | var b = pixels[k++] & 0xff; 222 | var index = nq.map(r, g,b); 223 | usedEntry[index] = true; 224 | indexedPixels[i] = index; 225 | } 226 | 227 | } //analyze 228 | 229 | //writers 230 | // 231 | 232 | /** Writes Logical Screen Descriptor. */ 233 | function write_LSD(output:BytesOutput) { 234 | // 235 | 236 | // Logical screen size 237 | output.writeInt16(width); 238 | output.writeInt16(height); 239 | 240 | // Packed fields 241 | output.writeByte(0x80 | // 1 : global color table flag = 1 (gct used) 242 | 0x70 | // 2-4 : color resolution = 7 243 | 0x00 | // 5 : gct sort flag = 0 244 | paletteSize); // 6-8 : gct size 245 | 246 | output.writeByte(0); // Background color index 247 | output.writeByte(0); // Pixel aspect ratio - assume 1:1 248 | 249 | } //write_LSD 250 | 251 | /** Writes Netscape application extension to define repeat count. */ 252 | function write_NetscapeExt(output:BytesOutput):Void { 253 | 254 | var repeats = repeat; 255 | if(repeats == GifRepeat.Infinite || repeats < 0) repeats = 0; 256 | if(repeats == GifRepeat.None) repeats = -1; 257 | 258 | output.writeByte(0x21); // Extension introducer 259 | output.writeByte(0xff); // App extension label 260 | output.writeByte(11); // Block size 261 | output.writeString("NETSCAPE" + "2.0"); // App id + auth code 262 | output.writeByte(3); // Sub-block size 263 | output.writeByte(1); // Loop sub-block id 264 | output.writeInt16(repeats); // Loop count (extra iterations, 0=repeat forever) 265 | output.writeByte(0); // Block terminator 266 | 267 | } //write_NetscapeExt 268 | 269 | /** Write color table. */ 270 | function write_palette(output:BytesOutput):Void { 271 | 272 | output.write(colorTab.view.buffer); 273 | 274 | var n:Int = (3 * 256) - colorTab.length; 275 | 276 | for (i in 0...n) { 277 | output.writeByte(0); 278 | } 279 | 280 | } //write_palette 281 | 282 | /** Encodes and writes pixel data. */ 283 | function write_pixels(output:BytesOutput):Void { 284 | 285 | lzwEncoder.reset(indexedPixels, colorDepth); 286 | lzwEncoder.encode(output); 287 | 288 | } //write_pixels 289 | 290 | /** Writes Image Descriptor. */ 291 | function write_image_desc(output:BytesOutput, first:Bool):Void { 292 | 293 | output.writeByte(0x2c); // Image separator 294 | output.writeInt16(0); // Image position x = 0 295 | output.writeInt16(0); // Image position y = 0 296 | output.writeInt16(width); // Image width 297 | output.writeInt16(height); // Image height 298 | 299 | //Write LCT, or GCT 300 | 301 | if(first) { 302 | 303 | output.writeByte(0); // No LCT - GCT is used for first (or only) frame 304 | 305 | } else { 306 | 307 | output.writeByte(0x80 | // 1 local color table 1=yes 308 | 0 | // 2 interlace - 0=no 309 | 0 | // 3 sorted - 0=no 310 | 0 | // 4-5 reserved 311 | paletteSize); // 6-8 size of color table 312 | 313 | } //else 314 | 315 | } //write_image_desc 316 | 317 | /** Writes Graphic Control Extension. Delay is in seconds, floored and converted to 1/100 of a second */ 318 | function write_GraphicControlExt(output:BytesOutput, delay:Float):Void { 319 | 320 | output.writeByte(0x21); // Extension introducer 321 | output.writeByte(0xf9); // GCE label 322 | output.writeByte(4); // data block size 323 | 324 | // Packed fields 325 | output.writeByte(0 | // 1:3 reserved 326 | 0 | // 4:6 disposal 327 | 0 | // 7 user input - 0 = none 328 | 0 ); // 8 transparency flag 329 | 330 | //convert to 1/100 sec 331 | var delay_val = Math.floor(delay * 100); 332 | 333 | output.writeInt16(delay_val); // Delay x 1/100 sec 334 | output.writeByte(0); // Transparent color index 335 | output.writeByte(0); // Block terminator 336 | 337 | } //write_GraphicControlExt 338 | 339 | /** Clamp a value between a and b and return the clamped version */ 340 | static inline public function clamp(value:Float, a:Float, b:Float):Float 341 | { 342 | return ( value < a ) ? a : ( ( value > b ) ? b : value ); 343 | } 344 | 345 | } //GifEncoder 346 | 347 | 348 | typedef GifFrame = { 349 | 350 | /** Delay of the frame in seconds. This value gets floored 351 | when encoded due to gif format requirements. If this value is negative, 352 | the default encoder frame rate will be used. */ 353 | var delay: Float; 354 | /** Whether or not this frame should be flipped on the Y axis */ 355 | var flippedY: Bool; 356 | /** Pixels data in unsigned bytes, rgb format */ 357 | var data: UInt8Array; 358 | 359 | } 360 | -------------------------------------------------------------------------------- /gif/NeuQuant.hx: -------------------------------------------------------------------------------- 1 | package gif; 2 | 3 | /* 4 | * Copyright (c) 1994 Anthony Dekker 5 | * Ported to Java by Kevin Weiner, FM Software 6 | * Ported to Haxe by Tilman Schmidt and Sven Bergström 7 | * 8 | * NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. 9 | * See "Kohonen neural networks for optimal colour quantization" 10 | * in "Network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367. 11 | * for a discussion of the algorithm. 12 | * 13 | * Any party obtaining a copy of these files from the author, directly or 14 | * indirectly, is granted, free of charge, a full and unrestricted irrevocable, 15 | * world-wide, paid up, royalty-free, nonexclusive right and license to deal 16 | * in this software and documentation files (the "Software"), including without 17 | * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 18 | * and/or sell copies of the Software, and to permit persons who receive 19 | * copies from any such party to do so, with the only requirement being 20 | * that this copyright notice remain intact. 21 | * 22 | */ 23 | 24 | import haxe.io.Int32Array; 25 | import haxe.io.UInt8Array; 26 | 27 | class NeuQuant { 28 | 29 | inline static var netsize : Int = 256; // Number of colours used 30 | 31 | // Four primes near 500 - assume no image has a length so large that it is divisible by all four primes 32 | inline static var prime1 : Int = 499; 33 | inline static var prime2 : Int = 491; 34 | inline static var prime3 : Int = 487; 35 | inline static var prime4 : Int = 503; 36 | 37 | inline static var minpicturebytes : Int = (3 * prime4); // Minimum size for input image 38 | 39 | // Network Definitions 40 | inline static var netbiasshift : Int = 4; // Bias for colour values 41 | inline static var ncycles : Int = 100; // No. of learning cycles 42 | 43 | // Defs for freq and bias 44 | inline static var intbiasshift : Int = 16; // Bias for fractions 45 | inline static var intbias : Int = (1 << intbiasshift); 46 | inline static var gammashift : Int = 10; // Gamma = 1024 47 | inline static var gamma : Int = (1 << gammashift); 48 | inline static var betashift : Int = 10; 49 | inline static var beta : Int = (intbias >> betashift); // Beta = 1/1024 50 | inline static var betagamma : Int = (intbias << (gammashift - betashift)); 51 | 52 | // Defs for decreasing radius factor 53 | inline static var initrad : Int = (netsize >> 3); // For 256 cols, radius starts 54 | inline static var radiusbiasshift : Int = 6; // At 32.0 biased by 6 bits 55 | inline static var radiusbias : Int = (1 << radiusbiasshift); 56 | inline static var initradius : Int = (initrad * radiusbias); // And decreases by a 57 | inline static var radiusdec : Int = 30; // Factor of 1/30 each cycle 58 | 59 | // Defs for decreasing alpha factor 60 | inline static var alphabiasshift : Int = 10; /* alpha starts at 1.0 */ 61 | inline static var initalpha : Int = (1 << alphabiasshift); 62 | 63 | // Radbias and alpharadbias used for radpower calculation 64 | inline static var radbiasshift : Int = 8; 65 | inline static var radbias : Int = (1 << radbiasshift); 66 | inline static var alpharadbshift : Int = (alphabiasshift + radbiasshift); 67 | inline static var alpharadbias : Int = (1 << alpharadbshift); 68 | 69 | var alphadec:Int; // Biased by 10 bits 70 | 71 | // Types and Global Variables 72 | 73 | var thepicture: UInt8Array; // The input image itself 74 | var lengthcount: Int; // Lengthcount = H*W*3 75 | var samplefac: Int; // Sampling factor 1..30 76 | var network: Int32Array; // The network itself - [netsize][4] 77 | var netindex: Int32Array; // For network lookup - really 256 78 | var bias: Int32Array; // Bias array for learning 79 | var freq: Int32Array; // Frequency array for learning 80 | var radpower: Int32Array; // Radpower for precomputation 81 | var colormap_map: UInt8Array; // Cached color map array 82 | var colormap_index: Int32Array; // Cached color map index 83 | 84 | public function new() 85 | { 86 | netindex = new Int32Array(256); 87 | bias = new Int32Array(netsize); 88 | freq = new Int32Array(netsize); 89 | radpower = new Int32Array(initrad); 90 | network = new Int32Array(netsize * 4); 91 | colormap_map = new UInt8Array(3 * netsize); 92 | colormap_index = new Int32Array(netsize); 93 | } 94 | 95 | // Reset network in range (0,0,0) to (255,255,255) and set parameters 96 | public function reset(thepic:UInt8Array, len:Int, sample:Int):Void { 97 | thepicture = thepic; 98 | lengthcount = len; 99 | samplefac = sample; 100 | 101 | for (i in 0...netsize) { 102 | network[i*4 + 0] = network[i*4 + 1] = network[i*4 + 2] = Std.int((i << (netbiasshift + 8)) / netsize); 103 | freq[i] = Std.int(intbias / netsize); // 1 / netsize 104 | bias[i] = 0; // allocated to zero? 105 | } 106 | } 107 | 108 | public function colormap():UInt8Array 109 | { 110 | for(i in 0...netsize) { 111 | colormap_index[network[i * 4 + 3]] = i; 112 | } 113 | 114 | var k:Int = 0; 115 | for (i in 0...netsize) 116 | { 117 | var j = colormap_index[i]; 118 | colormap_map[k++] = network[j * 4]; 119 | colormap_map[k++] = network[j * 4 + 1]; 120 | colormap_map[k++] = network[j * 4 + 2]; 121 | } 122 | 123 | return colormap_map; 124 | } 125 | 126 | // Insertion sort of network and building of netindex[0..255] (to do after unbias) 127 | public function inxbuild():Void 128 | { 129 | var i:Int; 130 | var j:Int; 131 | var smallpos:Int; 132 | var smallval:Int; 133 | var previouscol:Int; 134 | var startpos:Int; 135 | 136 | previouscol = 0; 137 | startpos = 0; 138 | 139 | for (i in 0...netsize) 140 | { 141 | smallpos = i; 142 | smallval = network[i*4 + 1]; // Index on g 143 | 144 | // Find smallest in i..netsize-1 145 | for (j in (i + 1)...netsize) 146 | { 147 | if (network[j*4 + 1] < smallval) 148 | { 149 | smallpos = j; 150 | smallval = network[j*4 + 1]; // Index on g 151 | } 152 | } 153 | 154 | 155 | // Swap p (i) and q (smallpos) entries 156 | if (i != smallpos) 157 | { 158 | j = network[smallpos*4 + 0]; 159 | network[smallpos*4 + 0] = network[i*4 + 0]; 160 | network[i*4 + 0] = j; 161 | j = network[smallpos*4 + 1]; 162 | network[smallpos*4 + 1] = network[i*4 + 1]; 163 | network[i*4 + 1] = j; 164 | j = network[smallpos*4 + 2]; 165 | network[smallpos*4 + 2] = network[i*4 + 2]; 166 | network[i*4 + 2] = j; 167 | j = network[smallpos*4 + 3]; 168 | network[smallpos*4 + 3] = network[i*4 + 3]; 169 | network[i*4 + 3] = j; 170 | } 171 | 172 | // Smallval entry is now in position i 173 | if (smallval != previouscol) 174 | { 175 | netindex[previouscol] = (startpos + i) >> 1; 176 | 177 | for (j in (previouscol + 1)...smallval) 178 | netindex[j] = i; 179 | 180 | previouscol = smallval; 181 | startpos = i; 182 | } 183 | } 184 | 185 | var maxnetpos = netsize - 1; 186 | 187 | netindex[previouscol] = (startpos + maxnetpos) >> 1; 188 | 189 | for (j in (previouscol + 1)...256) 190 | netindex[j] = maxnetpos; 191 | } 192 | 193 | // Main learning Loop 194 | public function learn():Void 195 | { 196 | var i:Int; 197 | var j:Int; 198 | var b:Int; 199 | var g:Int; 200 | var r:Int; 201 | var radius:Int; 202 | var rad:Int; 203 | var alpha:Int; 204 | var step:Int; 205 | var delta:Int; 206 | var samplepixels:Int; 207 | 208 | var p:UInt8Array; 209 | var pix:Int; 210 | var lim:Int; 211 | 212 | if (lengthcount < minpicturebytes) 213 | samplefac = 1; 214 | 215 | alphadec = 30 + Std.int((samplefac - 1) / 3); 216 | p = thepicture; 217 | pix = 0; 218 | lim = lengthcount; 219 | samplepixels = Std.int(lengthcount / (3 * samplefac)); 220 | delta = Std.int(samplepixels / ncycles); 221 | alpha = initalpha; 222 | radius = initradius; 223 | 224 | rad = radius >> radiusbiasshift; 225 | 226 | if (rad <= 1) 227 | rad = 0; 228 | 229 | for (i in 0...rad) 230 | radpower[i] = Std.int(alpha * (((rad * rad - i * i) * radbias) / (rad * rad))); 231 | 232 | if (lengthcount < minpicturebytes) 233 | { 234 | step = 3; 235 | } 236 | else if ((lengthcount % prime1) != 0) 237 | { 238 | step = 3 * prime1; 239 | } 240 | else 241 | { 242 | if ((lengthcount % prime2) != 0) 243 | { 244 | step = 3 * prime2; 245 | } 246 | else 247 | { 248 | if ((lengthcount % prime3) != 0) 249 | step = 3 * prime3; 250 | else 251 | step = 3 * prime4; 252 | } 253 | } 254 | 255 | i = 0; 256 | while (i < samplepixels) 257 | { 258 | b = (p[pix + 0] & 0xff) << netbiasshift; 259 | g = (p[pix + 1] & 0xff) << netbiasshift; 260 | r = (p[pix + 2] & 0xff) << netbiasshift; 261 | j = contest(b, g, r); 262 | 263 | altersingle(alpha, j, b, g, r); 264 | 265 | if (rad != 0) 266 | alterneigh(rad, j, b, g, r); // Alter neighbours 267 | 268 | pix += step; 269 | 270 | if (pix >= lim) 271 | pix -= lengthcount; 272 | 273 | i++; 274 | 275 | if (delta == 0) 276 | delta = 1; 277 | 278 | if (i % delta == 0) 279 | { 280 | alpha -= Std.int(alpha / alphadec); 281 | radius -= Std.int(radius / radiusdec); 282 | rad = radius >> radiusbiasshift; 283 | 284 | if (rad <= 1) 285 | rad = 0; 286 | 287 | for (j in 0...rad) 288 | radpower[j] = Std.int(alpha * (((rad * rad - j * j) * radbias) / (rad * rad))); 289 | } 290 | } 291 | } 292 | 293 | // Search for BGR values 0..255 (after net is unbiased) and return colour index 294 | public function map(b:Int, g:Int, r:Int):Int 295 | { 296 | var i:Int; 297 | var j:Int; 298 | var dist:Int; 299 | var a:Int; 300 | var bestd:Int; 301 | var best:Int; 302 | 303 | bestd = 1000; // Biggest possible dist is 256*3 304 | best = -1; 305 | i = netindex[g]; // Index on g 306 | j = i - 1; // Start at netindex[g] and work outwards 307 | 308 | while ((i < netsize) || (j >= 0)) 309 | { 310 | if (i < netsize) 311 | { 312 | dist = network[i*4 + 1] - g; // Inx key 313 | 314 | if (dist >= bestd) 315 | { 316 | i = netsize; // Stop iter 317 | } 318 | else 319 | { 320 | if (dist < 0) 321 | dist = -dist; 322 | 323 | a = network[i*4 + 0] - b; 324 | 325 | if (a < 0) 326 | a = -a; 327 | 328 | dist += a; 329 | 330 | if (dist < bestd) 331 | { 332 | a = network[i*4 + 2] - r; 333 | 334 | if (a < 0) 335 | a = -a; 336 | 337 | dist += a; 338 | 339 | if (dist < bestd) 340 | { 341 | bestd = dist; 342 | best = network[i*4 + 3]; 343 | } 344 | } 345 | 346 | i++; 347 | } 348 | } 349 | 350 | if (j >= 0) 351 | { 352 | dist = g - network[j*4 + 1]; // Inx key - reverse dif 353 | 354 | if (dist >= bestd) 355 | { 356 | j = -1; // Stop iter 357 | } 358 | else 359 | { 360 | if (dist < 0) 361 | dist = -dist; 362 | 363 | a = network[j*4 + 0] - b; 364 | 365 | if (a < 0) 366 | a = -a; 367 | 368 | dist += a; 369 | 370 | if (dist < bestd) 371 | { 372 | a = network[j*4 + 2] - r; 373 | 374 | if (a < 0) 375 | a = -a; 376 | 377 | dist += a; 378 | 379 | if (dist < bestd) 380 | { 381 | bestd = dist; 382 | best = network[j*4 + 3]; 383 | } 384 | } 385 | 386 | j--; 387 | } 388 | } 389 | } 390 | 391 | return best; 392 | } 393 | 394 | public function process():UInt8Array 395 | { 396 | learn(); 397 | unbiasnet(); 398 | inxbuild(); 399 | return colormap(); 400 | } 401 | 402 | // Unbias network to give byte values 0..255 and record position i to prepare for sort 403 | public function unbiasnet():Void 404 | { 405 | for (i in 0...netsize) 406 | { 407 | network[i*4] >>= netbiasshift; 408 | network[i*4 + 1] >>= netbiasshift; 409 | network[i*4 + 2] >>= netbiasshift; 410 | network[i*4 + 3] = i; // Record colour no 411 | } 412 | } 413 | 414 | // Move adjacent neurons by precomputed alpha*(1-((i-j)^2/[r]^2)) in radpower[|i-j|] 415 | function alterneigh(rad:Int, i:Int, b:Int, g:Int, r:Int):Void 416 | { 417 | var j:Int; 418 | var k:Int; 419 | var lo:Int; 420 | var hi:Int; 421 | var a:Int; 422 | var m:Int; 423 | 424 | lo = i - rad; 425 | 426 | if (lo < -1) 427 | lo = -1; 428 | 429 | hi = i + rad; 430 | 431 | if (hi > netsize) 432 | hi = netsize; 433 | 434 | j = i + 1; 435 | k = i - 1; 436 | m = 1; 437 | 438 | while ((j < hi) || (k > lo)) 439 | { 440 | a = radpower[m++]; 441 | 442 | if (j < hi) 443 | { 444 | network[j * 4 + 0] -= Std.int((a * (network[j * 4 + 0] - b)) / alpharadbias); 445 | network[j * 4 + 1] -= Std.int((a * (network[j * 4 + 1] - g)) / alpharadbias); 446 | network[j * 4 + 2] -= Std.int((a * (network[j * 4 + 2] - r)) / alpharadbias); 447 | j++; 448 | } 449 | 450 | if (k > lo) 451 | { 452 | network[k * 4 + 0] -= Std.int((a * (network[k * 4 + 0] - b)) / alpharadbias); 453 | network[k * 4 + 1] -= Std.int((a * (network[k * 4 + 1] - g)) / alpharadbias); 454 | network[k * 4 + 2] -= Std.int((a * (network[k * 4 + 2] - r)) / alpharadbias); 455 | k--; 456 | } 457 | } 458 | } 459 | 460 | // Move neuron i towards biased (b,g,r) by factor alpha 461 | function altersingle(alpha:Int, i:Int, b:Int, g:Int, r:Int):Void 462 | { 463 | /* Alter hit neuron */ 464 | network[i*4 + 0] -= Std.int((alpha * (network[i*4 + 0] - b)) / initalpha); 465 | network[i*4 + 1] -= Std.int((alpha * (network[i*4 + 1] - g)) / initalpha); 466 | network[i*4 + 2] -= Std.int((alpha * (network[i*4 + 2] - r)) / initalpha); 467 | } 468 | 469 | inline function make_abs(value:Int) : Int { 470 | var tmp = value >> 31; 471 | value ^= tmp; 472 | value += tmp & 1; 473 | return value; 474 | } 475 | 476 | // Search for biased BGR values 477 | static inline var bestd_init = ~(1 << 31); 478 | function contest(b:Int, g:Int, r:Int):Int 479 | { 480 | // Finds closest neuron (min dist) and updates freq 481 | // Finds best neuron (min dist-bias) and returns position 482 | // For frequently chosen neurons, freq[i] is high and bias[i] is negative 483 | // bias[i] = gamma*((1/netsize)-freq[i]) 484 | 485 | var i:Int; 486 | var dist:Int; 487 | var a:Int; 488 | var biasdist:Int; 489 | var betafreq:Int; 490 | var bestpos:Int; 491 | var bestbiaspos:Int; 492 | var bestd:Int; 493 | var bestbiasd:Int; 494 | 495 | bestd = bestd_init; 496 | bestbiasd = bestd; 497 | bestpos = -1; 498 | bestbiaspos = bestpos; 499 | 500 | for (i in 0...netsize) 501 | { 502 | var i_n = i * 4; 503 | var b_i = i_n + 0; 504 | var g_i = i_n + 1; 505 | var r_i = i_n + 2; 506 | 507 | var b_a = network[b_i]; 508 | var g_a = network[g_i]; 509 | var r_a = network[r_i]; 510 | 511 | b_a = make_abs(b_a - b); 512 | g_a = make_abs(g_a - g); 513 | r_a = make_abs(r_a - r); 514 | 515 | dist = b_a + g_a + r_a; 516 | 517 | if (dist < bestd) 518 | { 519 | bestd = dist; 520 | bestpos = i; 521 | } 522 | 523 | biasdist = dist - ((bias[i]) >> (intbiasshift - netbiasshift)); 524 | 525 | if (biasdist < bestbiasd) 526 | { 527 | bestbiasd = biasdist; 528 | bestbiaspos = i; 529 | } 530 | 531 | betafreq = (freq[i] >> betashift); 532 | freq[i] -= betafreq; 533 | bias[i] += (betafreq << gammashift); 534 | } 535 | 536 | freq[bestpos] += beta; 537 | bias[bestpos] -= betagamma; 538 | return bestbiaspos; 539 | } 540 | 541 | } 542 | --------------------------------------------------------------------------------