Displaying Animated Images With ImageMagick

The Internet Is Made For Cats

I’ve spent the day adding support for animated GIFs to Emacs via ImageMagick.  Emacs c
an display animated GIFs already, of course, but not via ImageMagick, so we couldn’t scale animated images.  Which is awkward.

An animated GIF is (basically) just a bunch of images in one blob.  However, the “complicated” thing is that all subsequent images may have transparencies.  So to compute image X, you have to unpack image 1 to X, and apply the non-transparent bits in each iteration.  Apparently.

Most of the work was googling around for how to do this, so to spare other people the same pain, I’m posting the (abbreviated) code here.

The weird indentation is because this is from the Emacs source.

To make this usable, you should also add caching so that you don’t have to compute all the overlays every time.

static MagickWand *
imagemagick_compute_animated_image (MagickWand *super_wand, int ino)
{
  int i;
  MagickWand *composite_wand;
  size_t dest_width, dest_height;

  MagickSetIteratorIndex (super_wand, 0);
  composite_wand = MagickGetImage (super_wand);

  dest_width = MagickGetImageWidth (composite_wand);
  dest_height = MagickGetImageHeight (composite_wand);

  for (i = 1; i <= ino; i++)
    {
      MagickWand *sub_wand;
      PixelIterator *source_iterator, *dest_iterator;
      PixelWand **source, **dest;
      size_t source_width, source_height;
      ssize_t source_left, source_top;
      MagickPixelPacket pixel;
      DisposeType dispose;
      int lines = 0;

      MagickSetIteratorIndex (super_wand, i);
      sub_wand = MagickGetImage (super_wand);

      MagickGetImagePage (sub_wand, &source_width, &source_height,
                          &source_left, &source_top);

      dispose = MagickGetImageDispose (sub_wand);

      source_iterator = NewPixelIterator (sub_wand);
      dest_iterator = NewPixelIterator (composite_wand);

      /* The sub-image may not start at origo, so move the destination
         iterator to where the sub-image should start. */
      if (source_top > 0)
        {
          PixelSetIteratorRow (dest_iterator, source_top);
          lines = source_top;
        }

      while ((source = PixelGetNextIteratorRow (source_iterator, &source_width))
             != NULL)
        {
          int x;

          /* Sanity check.  This shouldn’t happen, but apparently
             does in some pictures.  */
          if (++lines >= dest_height)
            break;

          dest = PixelGetNextIteratorRow (dest_iterator, &dest_width);
          for (x = 0; x < source_width; x++)
            {
              /* Sanity check.  This shouldn’t happen, but apparently
                 also does in some pictures.  */
              if (x + source_left > dest_width)
                break;
              /* Normally we only copy over non-transparent pixels,
                 but if the disposal method is “Background”, then we
                 copy over all pixels.  */
              if (dispose == BackgroundDispose ||
                  PixelGetAlpha (source[x]))
                {
                  PixelGetMagickColor (source[x], &pixel);
                  PixelSetMagickColor (dest[x + source_left], &pixel);
                }
            }
          PixelSyncIterator(dest_iterator);
        }

      DestroyPixelIterator (source_iterator);
      DestroyPixelIterator (dest_iterator);
      DestroyMagickWand (sub_wand);
    }

  return composite_wand;
}

Leave a Reply