brendan-duncan / image

Dart Image Library for opening, manipulating, and saving various different image file formats.
MIT License
1.18k stars 267 forks source link

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

Open doubleA411 opened 1 year ago

doubleA411 commented 1 year ago

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 ?

xErik commented 1 year ago

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/

SoftWyer commented 11 months ago

@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);
}