brendan-duncan / image

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

Support ARGB in Gif Quantizers #663

Closed mbfakourii closed 1 week ago

mbfakourii commented 1 week ago

I have a problem with black background after output gif.

After checking, I realized that none of the quantizers like NeuralQuantizer, OctreeQuantizer, and BinaryQuantizer support channel 4 and argb!

brendan-duncan commented 1 week ago

Yeah, the quantizers do not support alpha channel.

mbfakourii commented 1 week ago

Yeah, the quantizers do not support alpha channel.

Is it possible to support?

brendan-duncan commented 1 week ago

I haven't had a lot of time for the Dart libraries recently, and probably for a little while. I feel bad about it, but life has been really busy.

This isn't an ideal solution of course, but maybe you could manually do some palette manipulation to get gif transparency (untested).

List<int> encodeGifWIthTransparency(Image srcImage, { int transparencyThreshold = 128 }) {
  final newImage = quantize(srcImage);

  var palette = newImage.palette;

  // Quantized palette has 3 channels...manually make a 4 channel palette
  if (palette.numChannels == 3) {
    final newPalette = new PaletteUint8(palette.numColors, 4);
    for (var i = 0; i < palette.numColors; i++) {
      newPalette.setRgba(i, palette.getRed(i), palette.getGreen(i), palette.getBlue(i), 255);
    }
    palette = newPalette;
  }

  // GifEncoder will use palette colors with a 0 alpha as transparent. Look at the pixels
  // of the original image and set the alpha of the palette color to 0 if the pixel is below
  // a transparency threshold.
  for (final p in srcImage) {
    if (p.alpha < transparencyThreshold) {
      final p2 = newImage.getPixel(p.x, p.y);
      final i = p2.index; // palette index of the pixel
      palette.setAlpha(i, 0); // Set the palette color alpha to 0
    }
  }

  newImage.data!.palette = palette;

  return encodeGif(newImage);
}
mbfakourii commented 1 week ago

@brendan-duncan

I tried to use your code but in final p2 = newImage.getPixel(p.x, p.y); The image class does not have x and y values

Also, my srcImage is empty and it gets an error in the following section

image

my code

static Future<List<int>?> _exportGif(List<RawFrame> frames) async {
  Image aa = Image.empty();

  for (final frame in frames) {
    final iAsBytes = frame.image.buffer.asUint8List();
    final decodedImage = decodePng(iAsBytes);

    if (decodedImage == null) {
      print('Skipped frame while enconding');
      continue;
    }
    decodedImage.frameDuration = frame.durationInMillis;
    aa.frames.add(decodedImage);
  }

  return encodeGifWIthTransparency(aa);
}

static List<int> encodeGifWIthTransparency(Image srcImage, { int transparencyThreshold = 128 }) {
  final newImage = quantize(srcImage);

  var palette = newImage.palette;

  // Quantized palette has 3 channels...manually make a 4 channel palette
  if (palette!.numChannels == 3) {
    final newPalette = new PaletteUint8(palette.numColors, 4);
    for (var i = 0; i < palette.numColors; i++) {
      newPalette.setRgba(i, palette.getRed(i), palette.getGreen(i), palette.getBlue(i), 255);
    }
    palette = newPalette;
  }

  // // GifEncoder will use palette colors with a 0 alpha as transparent. Look at the pixels
  // // of the original image and set the alpha of the palette color to 0 if the pixel is below
  // // a transparency threshold.
  // for (final Image p in srcImage.frames) {
  //   if (p.alpha < transparencyThreshold) {
  //     getColorIndex
  //     final p2 = newImage.getPixel(p.x, p.y);
  //     final num i = p2.index; // palette index of the pixel
  //     palette.setAlpha(i.toInt(), 0); // Set the palette color alpha to 0
  //   }
  // }

  newImage.data!.palette = palette;

  return encodeGif(newImage);
}
brendan-duncan commented 1 week ago

My example code was a pixel iterator, not a frame iterator. To iterate each pixel of each frame,

static PaletteUint8 convertPalette(Palette palette) {
  final newPalette = PaletteUint8(palette.numColors, 4);
  for (var i = 0; i < palette.numColors; i++) {
    newPalette.setRgba(i, palette.getRed(i), palette.getGreen(i), palette.getBlue(i), 255);
  }
  return newPalette;
}

static List<int> encodeGifWIthTransparency(Image srcImage, { int transparencyThreshold = 128 }) {
  final newImage = quantize(srcImage);

  // // GifEncoder will use palette colors with a 0 alpha as transparent. Look at the pixels
  // // of the original image and set the alpha of the palette color to 0 if the pixel is below
  // // a transparency threshold.
  final numFrames = srcImage.frames.length;
  for (var frameIndex = 0; frameIndex < numFrames; frameIndex++) {
    final srcFrame = srcImage.frames[frameIndex];
    final newFrame = newImage.frames[frameIndex];

    final palette = convertPalette(newImage.palette);

    for (final srcPixel in srcFrame) {
      if (srcPixel.alpha < transparencyThreshold) {
        final newPixel = newFrame.getPixel(srcPixel.x, srcPixel.y);
        final paletteIndex = newPixel.index; // palette index of the pixel
        palette.setAlpha(i.toInt(), 0); // Set the palette color alpha to 0
      }
    }

    newFrame.data!.palette = palette;
  }

  return encodeGif(newImage);
}
mbfakourii commented 1 week ago

My example code was a pixel iterator, not a frame iterator. To iterate each pixel of each frame,

static PaletteUint8 convertPalette(Palette palette) {
  final newPalette = PaletteUint8(palette.numColors, 4);
  for (var i = 0; i < palette.numColors; i++) {
    newPalette.setRgba(i, palette.getRed(i), palette.getGreen(i), palette.getBlue(i), 255);
  }
  return newPalette;
}

static List<int> encodeGifWIthTransparency(Image srcImage, { int transparencyThreshold = 128 }) {
  final newImage = quantize(srcImage);

  // // GifEncoder will use palette colors with a 0 alpha as transparent. Look at the pixels
  // // of the original image and set the alpha of the palette color to 0 if the pixel is below
  // // a transparency threshold.
  final numFrames = srcImage.frames.length;
  for (var frameIndex = 0; frameIndex < numFrames; frameIndex++) {
    final srcFrame = srcImage.frames[frameIndex];
    final newFrame = newImage.frames[frameIndex];

    final palette = convertPalette(newImage.palette);

    for (final srcPixel in srcFrame) {
      if (srcPixel.alpha < transparencyThreshold) {
        final newPixel = newFrame.getPixel(srcPixel.x, srcPixel.y);
        final paletteIndex = newPixel.index; // palette index of the pixel
        palette.setAlpha(i.toInt(), 0); // Set the palette color alpha to 0
      }
    }

    newFrame.data!.palette = palette;
  }

  return encodeGif(newImage);
}

image The Pixel class does not have an alpha value

Also, the code in quantize(srcImage); It has a problem

image

brendan-duncan commented 1 week ago

I'm typing this code without running or testing it, I'm not at a place where I can do that. You'll need to extrapolate the missing pieces.

brendan-duncan commented 1 week ago

Pixel derives from Color, and the getter for alpha is a not alpha, so use srcPixel.a.

mbfakourii commented 1 week ago

Thank you very much for your help ❤️❤️, my problem was solved as follows

static Future<List<int>?> _exportGif(List<RawFrame> frames) async {
  Image aa = Image.empty();

  for (final frame in frames) {
    final iAsBytes = frame.image.buffer.asUint8List();
    final decodedImage = decodePng(iAsBytes);

    if (decodedImage == null) {
      print('Skipped frame while enconding');
      continue;
    }
    decodedImage.frameDuration = frame.durationInMillis;
    aa.frames.add(encodeGifWIthTransparency(decodedImage));
  }

  return encodeGif(aa);
}

static PaletteUint8 convertPalette(Palette palette) {
  final newPalette = PaletteUint8(palette.numColors, 4);
  for (var i = 0; i < palette.numColors; i++) {
    newPalette.setRgba(
        i, palette.getRed(i), palette.getGreen(i), palette.getBlue(i), 255);
  }
  return newPalette;
}

static Image encodeGifWIthTransparency(Image srcImage,
    {int transparencyThreshold = 128}) {
  final newImage = quantize(srcImage);

  // // GifEncoder will use palette colors with a 0 alpha as transparent. Look at the pixels
  // // of the original image and set the alpha of the palette color to 0 if the pixel is below
  // // a transparency threshold.
  final numFrames = srcImage.frames.length;
  for (var frameIndex = 0; frameIndex < numFrames; frameIndex++) {
    final srcFrame = srcImage.frames[frameIndex];
    final newFrame = newImage.frames[frameIndex];

    final palette = convertPalette(newImage.palette!);

    for (final srcPixel in srcFrame) {
      if (srcPixel.a < transparencyThreshold) {
        final newPixel = newFrame.getPixel(srcPixel.x, srcPixel.y);
        palette.setAlpha(
            newPixel.index.toInt(), 0); // Set the palette color alpha to 0
      }
    }

    newFrame.data!.palette = palette;
  }

  return newImage;
}