image icon indicating copy to clipboard operation
image copied to clipboard

There is no documentation for how to convert multiple images into animated GiF

Open doubleA411 opened this issue 1 year ago • 2 comments

I'm trying to converting a list of frames captured from the RepaintBoundary of Flutter to a small Animated GiF. But the docs only has an example for single Image conversion and Also the guide in the Animated Image was not clear.

Can you help by providing a clear guide or something ?

doubleA411 avatar Sep 08 '23 14:09 doubleA411

Had a similar problem.

GifDecoder seems to return the (relative) frames as they are stored in the GIF, not as they are rendered. To get the absolute frames, the code below works for Flutter.

static Future<(List<Uint8List>, List<int>)> extractGifFrames(
    Uint8List data) async {
  final List<int> durations = [];

  final List<Uint8List> frames = <Uint8List>[];

  final ui.Codec codec = await ui.instantiateImageCodec(data);

  final int frameCount = codec.frameCount;
  print('Total frameCount: $frameCount');

  for (int i = 0; i < frameCount; i++) {
    final ui.FrameInfo fi = await codec.getNextFrame();

    final frame = await _loadImage(fi.image);

    if (frame != null) {
      print(' -- extracted frame $i');
      frames.add(frame);
      durations.add(fi.duration.inMilliseconds);
    }
  }

  return (frames, durations);
}

These absolute frames can be easily combined with the GifEncoder:

static Future<Uint8List> pngBytesToGifBytes(
    List<Uint8List> pngBytes, List<int> durationsMillis) async {
  return compute(_pngBytesToGifBytes,
      {'pngBytes': pngBytes, 'durationsMillis': durationsMillis});
}

static Uint8List _pngBytesToGifBytes(Map<String, dynamic> param) {
  final pngBytes = param['pngBytes']!;
  final durationsMillis = param['durationsMillis']!;

  final encode = img.GifEncoder();

  for (var i = 0; i < pngBytes.length; i++) {
    final dur = (durationsMillis.elementAt(i)) ~/ 10;
    print('  - adding frame $i with duration $dur/100 s');

    encode.addFrame(img.decodePng(pngBytes.elementAt(i))!, duration: dur);
  }

  return encode.finish()!;
}

static Future<Uint8List?> _loadImage(ui.Image image) async {
  final ByteData? byteData =
      await image.toByteData(format: ui.ImageByteFormat.png);
  return byteData?.buffer.asUint8List();
}

This needs more testing, a GIF inspector can be found here:

https://movableink.github.io/gif-inspector/

xErik avatar Sep 09 '23 12:09 xErik

@xErik Thanks for that code snippet. I was transforming to animated PNG and needed to tweak the pngBytesToGifBytes method slightly. When building the apng it was necessary to add the frames to the first image, rather than the encoder.

For reference, here's the code I used:

Uint8List _imageFramesToPngIsolate((List<Uint8List> pngBytes, List<int> durationsMillis) param) {
  final (pngBytes, durationsMillis) = param;

  final encoder = PngEncoder()
    ..repeat = -1
    ..isAnimated = pngBytes.length > 1;

  final imageToBeEncoded = decodePng(pngBytes.first);
  if (imageToBeEncoded == null) {
    throw Exception('First image cannot be decoded');
  }

  for (var i = 1; i < pngBytes.length; i++) {
    final dur = durationsMillis.elementAt(i);
    print('  - adding frame $i with duration ${dur}ms');

    imageToBeEncoded.addFrame(
      decodePng(pngBytes.elementAt(i))!
        ..frameDuration = dur
        ..frameIndex = i
        ..frameType = FrameType.animation,
    );
  }

  return encoder.encode(imageToBeEncoded);
}

SoftWyer avatar Dec 03 '23 15:12 SoftWyer