├── 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 |
--------------------------------------------------------------------------------