360 lines
11 KiB
Haxe
360 lines
11 KiB
Haxe
package iron.format.gif;
|
|
|
|
/*
|
|
* No copyright asserted on the source code of this class. May be used
|
|
* for any purpose.
|
|
*
|
|
* Original code by Kevin Weiner, FM Software.
|
|
* Adapted by Thomas Hourdel (https://github.com/Chman/Moments)
|
|
* Ported to Haxe by Tilman Schmidt and Sven Bergstr├╢m
|
|
*/
|
|
|
|
import haxe.io.UInt8Array;
|
|
import haxe.io.BytesOutput;
|
|
|
|
@:enum abstract GifRepeat(Int)
|
|
from Int to Int {
|
|
var None = 0;
|
|
var Infinite = -1;
|
|
}
|
|
|
|
@:enum abstract GifQuality(Int)
|
|
from Int to Int {
|
|
var Best = 1;
|
|
var VeryHigh = 10;
|
|
var QuiteHigh = 20;
|
|
var High = 35;
|
|
var Mid = 50;
|
|
var Low = 65;
|
|
var QuiteLow = 80;
|
|
var VeryLow = 90;
|
|
var Worst = 100;
|
|
}
|
|
|
|
class GifEncoder {
|
|
|
|
var width: Int;
|
|
var height: Int;
|
|
var framerate: Float = 24; // used if frame.delay < 0
|
|
var repeat: Int = -1; // -1: infinite, 0: none, >0: repeat count
|
|
|
|
var colorDepth: Int = 8; // Number of bit planes
|
|
var paletteSize: Int = 7; // Color table size (bits-1)
|
|
var sampleInterval: Int = 10; // Default sample interval for quantizer
|
|
|
|
//caches
|
|
var pixels: UInt8Array;
|
|
var indexedPixels: UInt8Array; // Converted frame indexed to palette
|
|
var colorTab: UInt8Array; // RGB palette
|
|
var usedEntry: Array<Bool>; // Active palette entries
|
|
//
|
|
var nq: NeuQuant;
|
|
var lzwEncoder: LzwEncoder;
|
|
//internal
|
|
var started: Bool = false;
|
|
var first_frame: Bool = true;
|
|
|
|
//:todo: error handling could be better - but throw inside of another thread on cpp is too quiet
|
|
|
|
/** Allows a custom print handler for error messages.
|
|
Defaults to Sys.println on sys targets, and trace otherwise. */
|
|
public var print: Dynamic->Void;
|
|
|
|
// Public API
|
|
|
|
/** Construct a gif encoder with options:
|
|
|
|
frame width/height:
|
|
Default is 0, required
|
|
|
|
framerate:
|
|
This is used if an added frame has a delay that is negative.
|
|
|
|
repeat:
|
|
Default is 0 (no repeat); -1 means play indefinitely.
|
|
Use GifRepeat for clarity
|
|
|
|
quality:
|
|
Sets quality of color quantization (conversion of images to
|
|
the maximum 256 colors allowed by the GIF specification). Lower values (minimum = 1)
|
|
produce better colors, but slow processing significantly. Higher values will speed
|
|
up the quantization pass at the cost of lower image quality (maximum = 100). */
|
|
public function new(
|
|
_frame_width:Int,
|
|
_frame_height:Int,
|
|
_framerate:Float,
|
|
_repeat:Int = GifRepeat.Infinite,
|
|
_quality:Int = 10
|
|
) {
|
|
|
|
#if sys
|
|
print = Sys.println;
|
|
#else
|
|
print = function(v) { trace(v); }
|
|
#end
|
|
|
|
width = _frame_width;
|
|
height = _frame_height;
|
|
framerate = _framerate;
|
|
repeat = _repeat;
|
|
|
|
sampleInterval = Std.int(clamp(_quality, 1, 100));
|
|
usedEntry = [for (i in 0...256) false];
|
|
|
|
pixels = new UInt8Array(width * height * 3);
|
|
indexedPixels = new UInt8Array(width * height);
|
|
|
|
nq = new NeuQuant();
|
|
lzwEncoder = new LzwEncoder();
|
|
|
|
} //new
|
|
|
|
public function start(output:BytesOutput) : Void {
|
|
|
|
if(output == null) {
|
|
print("gif: start() output must not be null.");
|
|
return;
|
|
}
|
|
|
|
output.writeString("GIF89a");
|
|
|
|
write_LSD(output);
|
|
|
|
started = true;
|
|
|
|
} //start
|
|
|
|
public function add(output:BytesOutput, frame:GifFrame) : Void {
|
|
|
|
if(output == null) {
|
|
print("gif: add() output must not be null.");
|
|
return;
|
|
}
|
|
|
|
if(!started) {
|
|
print("gif: add() requires start to be called before adding frames.");
|
|
return;
|
|
}
|
|
|
|
var pixels = get_pixels(frame);
|
|
analyze(pixels);
|
|
|
|
if(first_frame) {
|
|
|
|
write_palette(output);
|
|
|
|
if(repeat != GifRepeat.None) {
|
|
write_NetscapeExt(output);
|
|
}
|
|
|
|
first_frame = false;
|
|
|
|
} //first_frame
|
|
|
|
var delay = if(frame.delay < 0) {
|
|
1.0/framerate;
|
|
} else {
|
|
frame.delay;
|
|
}
|
|
|
|
write_GraphicControlExt(output, delay);
|
|
write_image_desc(output, first_frame);
|
|
|
|
if(!first_frame) {
|
|
write_palette(output);
|
|
}
|
|
|
|
write_pixels(output);
|
|
|
|
} //add
|
|
|
|
public function commit(output:BytesOutput) : Void {
|
|
|
|
if(output == null) {
|
|
print("gif: commit() output must be not null.");
|
|
return;
|
|
}
|
|
|
|
if(!started) {
|
|
print("gif: commit() called without start() being called first.");
|
|
return;
|
|
}
|
|
|
|
output.writeByte(0x3b); // Gif trailer
|
|
output.flush();
|
|
output.close();
|
|
|
|
started = false;
|
|
first_frame = true;
|
|
|
|
} //commit
|
|
|
|
//helpers
|
|
|
|
function get_pixels(frame:GifFrame):UInt8Array {
|
|
|
|
//if not flipped we can use the data as is
|
|
if (!frame.flippedY) return frame.data;
|
|
|
|
//otherwise flip it, and return the cached array
|
|
var stride = width * 3;
|
|
for(y in 0...height) {
|
|
var begin = (height - 1 - y) * stride;
|
|
pixels.view.buffer.blit(y * stride, frame.data.view.buffer, begin, stride);
|
|
}
|
|
|
|
return pixels;
|
|
|
|
} //get_pixels
|
|
|
|
function analyze(pixels:UInt8Array) {
|
|
|
|
// Create reduced palette
|
|
nq.reset(pixels, pixels.length, sampleInterval);
|
|
colorTab = nq.process();
|
|
|
|
// Map image pixels to new palette
|
|
var k:Int = 0;
|
|
for (i in 0...(width * height)) {
|
|
var r = pixels[k++] & 0xff;
|
|
var g = pixels[k++] & 0xff;
|
|
var b = pixels[k++] & 0xff;
|
|
var index = nq.map(r, g,b);
|
|
usedEntry[index] = true;
|
|
indexedPixels[i] = index;
|
|
}
|
|
|
|
} //analyze
|
|
|
|
//writers
|
|
//
|
|
|
|
/** Writes Logical Screen Descriptor. */
|
|
function write_LSD(output:BytesOutput) {
|
|
//
|
|
|
|
// Logical screen size
|
|
output.writeInt16(width);
|
|
output.writeInt16(height);
|
|
|
|
// Packed fields
|
|
output.writeByte(0x80 | // 1 : global color table flag = 1 (gct used)
|
|
0x70 | // 2-4 : color resolution = 7
|
|
0x00 | // 5 : gct sort flag = 0
|
|
paletteSize); // 6-8 : gct size
|
|
|
|
output.writeByte(0); // Background color index
|
|
output.writeByte(0); // Pixel aspect ratio - assume 1:1
|
|
|
|
} //write_LSD
|
|
|
|
/** Writes Netscape application extension to define repeat count. */
|
|
function write_NetscapeExt(output:BytesOutput):Void {
|
|
|
|
var repeats = repeat;
|
|
if(repeats == GifRepeat.Infinite || repeats < 0) repeats = 0;
|
|
if(repeats == GifRepeat.None) repeats = -1;
|
|
|
|
output.writeByte(0x21); // Extension introducer
|
|
output.writeByte(0xff); // App extension label
|
|
output.writeByte(11); // Block size
|
|
output.writeString("NETSCAPE" + "2.0"); // App id + auth code
|
|
output.writeByte(3); // Sub-block size
|
|
output.writeByte(1); // Loop sub-block id
|
|
output.writeInt16(repeats); // Loop count (extra iterations, 0=repeat forever)
|
|
output.writeByte(0); // Block terminator
|
|
|
|
} //write_NetscapeExt
|
|
|
|
/** Write color table. */
|
|
function write_palette(output:BytesOutput):Void {
|
|
|
|
output.write(colorTab.view.buffer);
|
|
|
|
var n:Int = (3 * 256) - colorTab.length;
|
|
|
|
for (i in 0...n) {
|
|
output.writeByte(0);
|
|
}
|
|
|
|
} //write_palette
|
|
|
|
/** Encodes and writes pixel data. */
|
|
function write_pixels(output:BytesOutput):Void {
|
|
|
|
lzwEncoder.reset(indexedPixels, colorDepth);
|
|
lzwEncoder.encode(output);
|
|
|
|
} //write_pixels
|
|
|
|
/** Writes Image Descriptor. */
|
|
function write_image_desc(output:BytesOutput, first:Bool):Void {
|
|
|
|
output.writeByte(0x2c); // Image separator
|
|
output.writeInt16(0); // Image position x = 0
|
|
output.writeInt16(0); // Image position y = 0
|
|
output.writeInt16(width); // Image width
|
|
output.writeInt16(height); // Image height
|
|
|
|
//Write LCT, or GCT
|
|
|
|
if(first) {
|
|
|
|
output.writeByte(0); // No LCT - GCT is used for first (or only) frame
|
|
|
|
} else {
|
|
|
|
output.writeByte(0x80 | // 1 local color table 1=yes
|
|
0 | // 2 interlace - 0=no
|
|
0 | // 3 sorted - 0=no
|
|
0 | // 4-5 reserved
|
|
paletteSize); // 6-8 size of color table
|
|
|
|
} //else
|
|
|
|
} //write_image_desc
|
|
|
|
/** Writes Graphic Control Extension. Delay is in seconds, floored and converted to 1/100 of a second */
|
|
function write_GraphicControlExt(output:BytesOutput, delay:Float):Void {
|
|
|
|
output.writeByte(0x21); // Extension introducer
|
|
output.writeByte(0xf9); // GCE label
|
|
output.writeByte(4); // data block size
|
|
|
|
// Packed fields
|
|
output.writeByte(0 | // 1:3 reserved
|
|
0 | // 4:6 disposal
|
|
0 | // 7 user input - 0 = none
|
|
0 ); // 8 transparency flag
|
|
|
|
//convert to 1/100 sec
|
|
var delay_val = Math.floor(delay * 100);
|
|
|
|
output.writeInt16(delay_val); // Delay x 1/100 sec
|
|
output.writeByte(0); // Transparent color index
|
|
output.writeByte(0); // Block terminator
|
|
|
|
} //write_GraphicControlExt
|
|
|
|
/** Clamp a value between a and b and return the clamped version */
|
|
static inline public function clamp(value:Float, a:Float, b:Float):Float
|
|
{
|
|
return ( value < a ) ? a : ( ( value > b ) ? b : value );
|
|
}
|
|
|
|
} //GifEncoder
|
|
|
|
|
|
typedef GifFrame = {
|
|
|
|
/** Delay of the frame in seconds. This value gets floored
|
|
when encoded due to gif format requirements. If this value is negative,
|
|
the default encoder frame rate will be used. */
|
|
var delay: Float;
|
|
/** Whether or not this frame should be flipped on the Y axis */
|
|
var flippedY: Bool;
|
|
/** Pixels data in unsigned bytes, rgb format */
|
|
var data: UInt8Array;
|
|
|
|
}
|