| import 'dart:typed_data'; |
| |
| import '../animation.dart'; |
| import '../image.dart'; |
| import '../util/dither_pixels.dart'; |
| import '../util/neural_quantizer.dart'; |
| import '../util/output_buffer.dart'; |
| import 'encoder.dart'; |
| |
| class GifEncoder extends Encoder { |
| int delay, repeat, samplingFactor; |
| DitherKernel dither; |
| bool ditherSerpentine; |
| |
| GifEncoder({ this.delay = 80, this.repeat = 0, this.samplingFactor = 10, |
| this.dither = DitherKernel.FloydSteinberg, this.ditherSerpentine = false }) |
| : _encodedFrames = 0; |
| |
| /// This adds the frame passed to [image]. |
| /// After the last frame has been added, [finish] is required to be called. |
| /// Optional frame [duration] is in 1/100 sec. |
| void addFrame(Image image, {int? duration}) { |
| if (output == null) { |
| output = OutputBuffer(); |
| |
| _lastColorMap = NeuralQuantizer(image, samplingFactor: samplingFactor); |
| _lastImage = ditherPixels(image, _lastColorMap!, |
| dither, ditherSerpentine); |
| _lastImageDuration = duration; |
| |
| _width = image.width; |
| _height = image.height; |
| return; |
| } |
| |
| if (_encodedFrames == 0) { |
| _writeHeader(_width, _height); |
| _writeApplicationExt(); |
| } |
| |
| _writeGraphicsCtrlExt(); |
| |
| _addImage(_lastImage, _width, _height, _lastColorMap!.colorMap, 256); |
| _encodedFrames++; |
| |
| _lastColorMap = NeuralQuantizer(image, samplingFactor: samplingFactor); |
| _lastImage = ditherPixels(image, _lastColorMap!, |
| dither, ditherSerpentine); |
| _lastImageDuration = duration; |
| } |
| |
| /// Encode the images that were added with [addFrame]. |
| /// After this has been called (returning the finishes GIF), |
| /// calling [addFrame] for a new animation or image is safe again. |
| /// |
| /// [addFrame] will not encode the first image passed and after that |
| /// always encode the previous image. Hence, the last image needs to be |
| /// encoded here. |
| List<int>? finish() { |
| List<int>? bytes; |
| if (output == null) { |
| return bytes; |
| } |
| |
| if (_encodedFrames == 0) { |
| _writeHeader(_width, _height); |
| _writeApplicationExt(); |
| } else { |
| _writeGraphicsCtrlExt(); |
| } |
| |
| _addImage(_lastImage, _width, _height, _lastColorMap!.colorMap, 256); |
| |
| output!.writeByte(TERMINATE_RECORD_TYPE); |
| |
| _lastImage = null; |
| _lastColorMap = null; |
| _encodedFrames = 0; |
| |
| bytes = output!.getBytes(); |
| output = null; |
| return bytes; |
| } |
| |
| /// Encode a single frame image. |
| @override |
| List<int> encodeImage(Image image) { |
| addFrame(image); |
| return finish()!; |
| } |
| |
| /// Does this encoder support animation? |
| @override |
| bool get supportsAnimation => true; |
| |
| /// Encode an animation. |
| @override |
| List<int>? encodeAnimation(Animation anim) { |
| repeat = anim.loopCount; |
| for (var f in anim) { |
| addFrame( |
| f, |
| duration: f.duration ~/ 10, // Convert ms to 1/100 sec. |
| ); |
| } |
| return finish(); |
| } |
| |
| void _addImage(Uint8List? image, int width, int height, Uint8List colorMap, |
| int numColors) { |
| // Image desc |
| output!.writeByte(IMAGE_DESC_RECORD_TYPE); |
| output!.writeUint16(0); // image position x,y = 0,0 |
| output!.writeUint16(0); |
| output!.writeUint16(width); // image size |
| output!.writeUint16(height); |
| |
| // Local Color Map |
| // (0x80: Use LCM, 0x07: Palette Size (7 = 8-bit)) |
| output!.writeByte(0x87); |
| output!.writeBytes(colorMap); |
| for (var i = numColors; i < 256; ++i) { |
| output!.writeByte(0); |
| output!.writeByte(0); |
| output!.writeByte(0); |
| } |
| |
| _encodeLZW(image, width, height); |
| } |
| |
| void _encodeLZW(Uint8List? image, int width, int height) { |
| _curAccum = 0; |
| _curBits = 0; |
| _blockSize = 0; |
| _block = Uint8List(256); |
| |
| const initCodeSize = 8; |
| output!.writeByte(initCodeSize); |
| |
| final hTab = Int32List(HSIZE); |
| final codeTab = Int32List(HSIZE); |
| var remaining = width * height; |
| var curPixel = 0; |
| |
| _initBits = initCodeSize + 1; |
| _nBits = _initBits; |
| _maxCode = (1 << _nBits) - 1; |
| _clearCode = 1 << (_initBits - 1); |
| _EOFCode = _clearCode + 1; |
| _clearFlag = false; |
| _freeEnt = _clearCode + 2; |
| |
| int _nextPixel() { |
| if (remaining == 0) { |
| return EOF; |
| } |
| --remaining; |
| return image![curPixel++] & 0xff; |
| } |
| |
| var ent = _nextPixel(); |
| |
| var hshift = 0; |
| for (var fcode = HSIZE; fcode < 65536; fcode *= 2) { |
| hshift++; |
| } |
| hshift = 8 - hshift; |
| |
| const hSizeReg = HSIZE; |
| for (var i = 0; i < hSizeReg; ++i) { |
| hTab[i] = -1; |
| } |
| |
| _output(_clearCode); |
| |
| var outerLoop = true; |
| while (outerLoop) { |
| outerLoop = false; |
| |
| var c = _nextPixel(); |
| while (c != EOF) { |
| final fcode = (c << BITS) + ent; |
| var i = (c << hshift) ^ ent; // xor hashing |
| |
| if (hTab[i] == fcode) { |
| ent = codeTab[i]; |
| c = _nextPixel(); |
| continue; |
| } else if (hTab[i] >= 0) { |
| // non-empty slot |
| var disp = hSizeReg - i; // secondary hash (after G. Knott) |
| if (i == 0) { |
| disp = 1; |
| } |
| do { |
| if ((i -= disp) < 0) { |
| i += hSizeReg; |
| } |
| |
| if (hTab[i] == fcode) { |
| ent = codeTab[i]; |
| outerLoop = true; |
| break; |
| } |
| } while (hTab[i] >= 0); |
| if (outerLoop) { |
| break; |
| } |
| } |
| |
| _output(ent); |
| ent = c; |
| |
| if (_freeEnt < (1 << BITS)) { |
| codeTab[i] = _freeEnt++; // code -> hashtable |
| hTab[i] = fcode; |
| } else { |
| for (var i = 0; i < HSIZE; ++i) { |
| hTab[i] = -1; |
| } |
| _freeEnt = _clearCode + 2; |
| _clearFlag = true; |
| _output(_clearCode); |
| } |
| |
| c = _nextPixel(); |
| } |
| } |
| |
| _output(ent); |
| _output(_EOFCode); |
| |
| output!.writeByte(0); |
| } |
| |
| void _output(int? code) { |
| _curAccum &= MASKS[_curBits]; |
| |
| if (_curBits > 0) { |
| _curAccum |= (code! << _curBits); |
| } else { |
| _curAccum = code!; |
| } |
| |
| _curBits += _nBits; |
| |
| while (_curBits >= 8) { |
| _addToBlock(_curAccum & 0xff); |
| _curAccum >>= 8; |
| _curBits -= 8; |
| } |
| |
| // If the next entry is going to be too big for the code size, |
| // then increase it, if possible. |
| if (_freeEnt > _maxCode || _clearFlag) { |
| if (_clearFlag) { |
| _nBits = _initBits; |
| _maxCode = (1 << _nBits) - 1; |
| _clearFlag = false; |
| } else { |
| ++_nBits; |
| if (_nBits == BITS) { |
| _maxCode = 1 << BITS; |
| } else { |
| _maxCode = (1 << _nBits) - 1; |
| } |
| } |
| } |
| |
| if (code == _EOFCode) { |
| // At EOF, write the rest of the buffer. |
| while (_curBits > 0) { |
| _addToBlock(_curAccum & 0xff); |
| _curAccum >>= 8; |
| _curBits -= 8; |
| } |
| _writeBlock(); |
| } |
| } |
| |
| void _writeBlock() { |
| if (_blockSize > 0) { |
| output!.writeByte(_blockSize); |
| output!.writeBytes(_block, _blockSize); |
| _blockSize = 0; |
| } |
| } |
| |
| void _addToBlock(int c) { |
| _block[_blockSize++] = c; |
| if (_blockSize >= 254) { |
| _writeBlock(); |
| } |
| } |
| |
| void _writeApplicationExt() { |
| output!.writeByte(EXTENSION_RECORD_TYPE); |
| output!.writeByte(APPLICATION_EXT); |
| output!.writeByte(11); // data block size |
| output!.writeBytes('NETSCAPE2.0'.codeUnits); // app identifier |
| output!.writeBytes([0x03, 0x01]); |
| output!.writeUint16(repeat); // loop count |
| output!.writeByte(0); // block terminator |
| } |
| |
| void _writeGraphicsCtrlExt() { |
| output!.writeByte(EXTENSION_RECORD_TYPE); |
| output!.writeByte(GRAPHIC_CONTROL_EXT); |
| output!.writeByte(4); // data block size |
| |
| const transparency = 0; |
| const dispose = 0; // dispose = no action |
| |
| // packed fields |
| output!.writeByte(0 | // 1:3 reserved |
| dispose | // 4:6 disposal |
| 0 | // 7 user input - 0 = none |
| transparency); // 8 transparency flag |
| |
| output!.writeUint16(_lastImageDuration ?? delay); // delay x 1/100 sec |
| output!.writeByte(0); // transparent color index |
| output!.writeByte(0); // block terminator |
| } |
| |
| // GIF header and Logical Screen Descriptor |
| void _writeHeader(int width, int height) { |
| output!.writeBytes(GIF89_STAMP.codeUnits); |
| output!.writeUint16(width); |
| output!.writeUint16(height); |
| output!.writeByte(0); // global color map parameters (not being used). |
| output!.writeByte(0); // background color index. |
| output!.writeByte(0); // aspect |
| } |
| |
| Uint8List? _lastImage; |
| int? _lastImageDuration; |
| NeuralQuantizer? _lastColorMap; |
| late int _width; |
| late int _height; |
| int _encodedFrames; |
| |
| int _curAccum = 0; |
| int _curBits = 0; |
| int _nBits = 0; |
| int _initBits = 0; |
| int _EOFCode = 0; |
| int _maxCode = 0; |
| int _clearCode = 0; |
| int _freeEnt = 0; |
| bool _clearFlag = false; |
| late Uint8List _block; |
| int _blockSize = 0; |
| |
| OutputBuffer? output; |
| |
| static const GIF89_STAMP = 'GIF89a'; |
| |
| static const IMAGE_DESC_RECORD_TYPE = 0x2c; |
| static const EXTENSION_RECORD_TYPE = 0x21; |
| static const TERMINATE_RECORD_TYPE = 0x3b; |
| |
| static const APPLICATION_EXT = 0xff; |
| static const GRAPHIC_CONTROL_EXT = 0xf9; |
| |
| static const EOF = -1; |
| static const BITS = 12; |
| static const HSIZE = 5003; // 80% occupancy |
| static const MASKS = [ |
| 0x0000, |
| 0x0001, |
| 0x0003, |
| 0x0007, |
| 0x000F, |
| 0x001F, |
| 0x003F, |
| 0x007F, |
| 0x00FF, |
| 0x01FF, |
| 0x03FF, |
| 0x07FF, |
| 0x0FFF, |
| 0x1FFF, |
| 0x3FFF, |
| 0x7FFF, |
| 0xFFFF |
| ]; |
| } |