Convert each animated GIF frame to a separate BufferedImage

I want to be able to take an animated GIF as input, count the frames (and perhaps other metadata), and convert each to a BufferedImage. How can I do this?


If you want all the frames to be the same size (for optimized GIFs) try something like this:

try {
    String[] imageatt = new String[]{
            "imageLeftPosition",
            "imageTopPosition",
            "imageWidth",
            "imageHeight"
    };    

    ImageReader reader = (ImageReader)ImageIO.getImageReadersByFormatName("gif").next();
    ImageInputStream ciis = ImageIO.createImageInputStream(new File("house2.gif"));
    reader.setInput(ciis, false);

    int noi = reader.getNumImages(true);
    BufferedImage master = null;

    for (int i = 0; i < noi; i++) { 
        BufferedImage image = reader.read(i);
        IIOMetadata metadata = reader.getImageMetadata(i);

        Node tree = metadata.getAsTree("javax_imageio_gif_image_1.0");
        NodeList children = tree.getChildNodes();

        for (int j = 0; j < children.getLength(); j++) {
            Node nodeItem = children.item(j);

            if(nodeItem.getNodeName().equals("ImageDescriptor")){
                Map<String, Integer> imageAttr = new HashMap<String, Integer>();

                for (int k = 0; k < imageatt.length; k++) {
                    NamedNodeMap attr = nodeItem.getAttributes();
                    Node attnode = attr.getNamedItem(imageatt[k]);
                    imageAttr.put(imageatt[k], Integer.valueOf(attnode.getNodeValue()));
                }
                if(i==0){
                    master = new BufferedImage(imageAttr.get("imageWidth"), imageAttr.get("imageHeight"), BufferedImage.TYPE_INT_ARGB);
                }
                master.getGraphics().drawImage(image, imageAttr.get("imageLeftPosition"), imageAttr.get("imageTopPosition"), null);
            }
        }
        ImageIO.write(master, "GIF", new File( i + ".gif")); 
    }
} catch (IOException e) {
    e.printStackTrace();
}

None of the answers here are correct and suitable for animation. There are many problems in each solution so I wrote something that actually works with all gif files. For instance, this takes into account the actual width and height of the image instead of taking the width and height of the first frame assuming it will fill the entire canvas, no, unfortunately it's not that simple. Second, this doesn't leave any transparent pickles. Third, this takes into account disposal Methods. Fourth, this gives you delays between frames (* 10 if you want to use it in Thread.sleep()).

private ImageFrame[] readGif(InputStream stream) throws IOException{
    ArrayList<ImageFrame> frames = new ArrayList<ImageFrame>(2);

    ImageReader reader = (ImageReader) ImageIO.getImageReadersByFormatName("gif").next();
    reader.setInput(ImageIO.createImageInputStream(stream));

    int lastx = 0;
    int lasty = 0;

    int width = -1;
    int height = -1;

    IIOMetadata metadata = reader.getStreamMetadata();

    Color backgroundColor = null;

    if(metadata != null) {
        IIOMetadataNode globalRoot = (IIOMetadataNode) metadata.getAsTree(metadata.getNativeMetadataFormatName());

        NodeList globalColorTable = globalRoot.getElementsByTagName("GlobalColorTable");
        NodeList globalScreeDescriptor = globalRoot.getElementsByTagName("LogicalScreenDescriptor");

        if (globalScreeDescriptor != null && globalScreeDescriptor.getLength() > 0){
            IIOMetadataNode screenDescriptor = (IIOMetadataNode) globalScreeDescriptor.item(0);

            if (screenDescriptor != null){
                width = Integer.parseInt(screenDescriptor.getAttribute("logicalScreenWidth"));
                height = Integer.parseInt(screenDescriptor.getAttribute("logicalScreenHeight"));
            }
        }

        if (globalColorTable != null && globalColorTable.getLength() > 0){
            IIOMetadataNode colorTable = (IIOMetadataNode) globalColorTable.item(0);

            if (colorTable != null) {
                String bgIndex = colorTable.getAttribute("backgroundColorIndex");

                IIOMetadataNode colorEntry = (IIOMetadataNode) colorTable.getFirstChild();
                while (colorEntry != null) {
                    if (colorEntry.getAttribute("index").equals(bgIndex)) {
                        int red = Integer.parseInt(colorEntry.getAttribute("red"));
                        int green = Integer.parseInt(colorEntry.getAttribute("green"));
                        int blue = Integer.parseInt(colorEntry.getAttribute("blue"));

                        backgroundColor = new Color(red, green, blue);
                        break;
                    }

                    colorEntry = (IIOMetadataNode) colorEntry.getNextSibling();
                }
            }
        }
    }

    BufferedImage master = null;
    boolean hasBackround = false;

    for (int frameIndex = 0;; frameIndex++) {
        BufferedImage image;
        try{
            image = reader.read(frameIndex);
        }catch (IndexOutOfBoundsException io){
            break;
        }

        if (width == -1 || height == -1){
            width = image.getWidth();
            height = image.getHeight();
        }

        IIOMetadataNode root = (IIOMetadataNode) reader.getImageMetadata(frameIndex).getAsTree("javax_imageio_gif_image_1.0");
        IIOMetadataNode gce = (IIOMetadataNode) root.getElementsByTagName("GraphicControlExtension").item(0);
        NodeList children = root.getChildNodes();

        int delay = Integer.valueOf(gce.getAttribute("delayTime"));

        String disposal = gce.getAttribute("disposalMethod");

        if (master == null){
            master = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
            master.createGraphics().setColor(backgroundColor);
            master.createGraphics().fillRect(0, 0, master.getWidth(), master.getHeight());

        hasBackround = image.getWidth() == width && image.getHeight() == height;

            master.createGraphics().drawImage(image, 0, 0, null);
        }else{
            int x = 0;
            int y = 0;

            for (int nodeIndex = 0; nodeIndex < children.getLength(); nodeIndex++){
                Node nodeItem = children.item(nodeIndex);

                if (nodeItem.getNodeName().equals("ImageDescriptor")){
                    NamedNodeMap map = nodeItem.getAttributes();

                    x = Integer.valueOf(map.getNamedItem("imageLeftPosition").getNodeValue());
                    y = Integer.valueOf(map.getNamedItem("imageTopPosition").getNodeValue());
                }
            }

            if (disposal.equals("restoreToPrevious")){
                BufferedImage from = null;
                for (int i = frameIndex - 1; i >= 0; i--){
                    if (!frames.get(i).getDisposal().equals("restoreToPrevious") || frameIndex == 0){
                        from = frames.get(i).getImage();
                        break;
                    }
                }

                {
                    ColorModel model = from.getColorModel();
                    boolean alpha = from.isAlphaPremultiplied();
                    WritableRaster raster = from.copyData(null);
                    master = new BufferedImage(model, raster, alpha, null);
                }
            }else if (disposal.equals("restoreToBackgroundColor") && backgroundColor != null){
                if (!hasBackround || frameIndex > 1){
                    master.createGraphics().fillRect(lastx, lasty, frames.get(frameIndex - 1).getWidth(), frames.get(frameIndex - 1).getHeight());
                }
            }
            master.createGraphics().drawImage(image, x, y, null);

            lastx = x;
            lasty = y;
        }

        {
            BufferedImage copy;

            {
                ColorModel model = master.getColorModel();
                boolean alpha = master.isAlphaPremultiplied();
                WritableRaster raster = master.copyData(null);
                copy = new BufferedImage(model, raster, alpha, null);
            }
            frames.add(new ImageFrame(copy, delay, disposal, image.getWidth(), image.getHeight()));
        }

        master.flush();
    }
    reader.dispose();

    return frames.toArray(new ImageFrame[frames.size()]);
}

And the ImageFrame class:

import java.awt.image.BufferedImage;
public class ImageFrame {
    private final int delay;
    private final BufferedImage image;
    private final String disposal;
    private final int width, height;

    public ImageFrame (BufferedImage image, int delay, String disposal, int width, int height){
        this.image = image;
        this.delay = delay;
        this.disposal = disposal;
        this.width = width;
        this.height = height;
    }

    public ImageFrame (BufferedImage image){
        this.image = image;
        this.delay = -1;
        this.disposal = null;
        this.width = -1;
        this.height = -1;
    }

    public BufferedImage getImage() {
        return image;
    }

    public int getDelay() {
        return delay;
    }

    public String getDisposal() {
        return disposal;
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
            return height;
    }
}

Right, I have never done anything even slightly like this before, but a bit of Googling and fiddling in Java got me this:

public ArrayList<BufferedImage> getFrames(File gif) throws IOException{
    ArrayList<BufferedImage> frames = new ArrayList<BufferedImage>();
    ImageReader ir = new GIFImageReader(new GIFImageReaderSpi());
    ir.setInput(ImageIO.createImageInputStream(gif));
    for(int i = 0; i < ir.getNumImages(true); i++)
        frames.add(ir.getRawImageType(i).createBufferedImage(ir.getWidth(i), ir.getHeight(i)));
    return frames;
}

Edit: see Ansel Zandegran's modification to my answer.


To split an animated GIF into separate BufferedImage frames:

try {
    ImageReader reader = ImageIO.getImageReadersByFormatName("gif").next();
    File input = new File("input.gif");
    ImageInputStream stream = ImageIO.createImageInputStream(input);
    reader.setInput(stream);

    int count = reader.getNumImages(true);
    for (int index = 0; index < count; index++) {
        BufferedImage frame = reader.read(index);
        // Here you go
    }
} catch (IOException ex) {
    // An I/O problem has occurred
}

Alex's answer covers most cases, but it does have a couple of problems. It doesn't handle transparency correctly (at least according to common convention) and it is applying the current frame's disposal method to the previous frame which is incorrect. Here's a version that does handle those cases correctly:

private ImageFrame[] readGIF(ImageReader reader) throws IOException {
    ArrayList<ImageFrame> frames = new ArrayList<ImageFrame>(2);

    int width = -1;
    int height = -1;

    IIOMetadata metadata = reader.getStreamMetadata();
    if (metadata != null) {
        IIOMetadataNode globalRoot = (IIOMetadataNode) metadata.getAsTree(metadata.getNativeMetadataFormatName());

        NodeList globalScreenDescriptor = globalRoot.getElementsByTagName("LogicalScreenDescriptor");

        if (globalScreenDescriptor != null && globalScreenDescriptor.getLength() > 0) {
            IIOMetadataNode screenDescriptor = (IIOMetadataNode) globalScreenDescriptor.item(0);

            if (screenDescriptor != null) {
                width = Integer.parseInt(screenDescriptor.getAttribute("logicalScreenWidth"));
                height = Integer.parseInt(screenDescriptor.getAttribute("logicalScreenHeight"));
            }
        }
    }

    BufferedImage master = null;
    Graphics2D masterGraphics = null;

    for (int frameIndex = 0;; frameIndex++) {
        BufferedImage image;
        try {
            image = reader.read(frameIndex);
        } catch (IndexOutOfBoundsException io) {
            break;
        }

        if (width == -1 || height == -1) {
            width = image.getWidth();
            height = image.getHeight();
        }

        IIOMetadataNode root = (IIOMetadataNode) reader.getImageMetadata(frameIndex).getAsTree("javax_imageio_gif_image_1.0");
        IIOMetadataNode gce = (IIOMetadataNode) root.getElementsByTagName("GraphicControlExtension").item(0);
        int delay = Integer.valueOf(gce.getAttribute("delayTime"));
        String disposal = gce.getAttribute("disposalMethod");

        int x = 0;
        int y = 0;

        if (master == null) {
            master = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
            masterGraphics = master.createGraphics();
            masterGraphics.setBackground(new Color(0, 0, 0, 0));
        } else {
            NodeList children = root.getChildNodes();
            for (int nodeIndex = 0; nodeIndex < children.getLength(); nodeIndex++) {
                Node nodeItem = children.item(nodeIndex);
                if (nodeItem.getNodeName().equals("ImageDescriptor")) {
                    NamedNodeMap map = nodeItem.getAttributes();
                    x = Integer.valueOf(map.getNamedItem("imageLeftPosition").getNodeValue());
                    y = Integer.valueOf(map.getNamedItem("imageTopPosition").getNodeValue());
                }
            }
        }
        masterGraphics.drawImage(image, x, y, null);

        BufferedImage copy = new BufferedImage(master.getColorModel(), master.copyData(null), master.isAlphaPremultiplied(), null);
        frames.add(new ImageFrame(copy, delay, disposal));

        if (disposal.equals("restoreToPrevious")) {
            BufferedImage from = null;
            for (int i = frameIndex - 1; i >= 0; i--) {
                if (!frames.get(i).getDisposal().equals("restoreToPrevious") || frameIndex == 0) {
                    from = frames.get(i).getImage();
                    break;
                }
            }

            master = new BufferedImage(from.getColorModel(), from.copyData(null), from.isAlphaPremultiplied(), null);
            masterGraphics = master.createGraphics();
            masterGraphics.setBackground(new Color(0, 0, 0, 0));
        } else if (disposal.equals("restoreToBackgroundColor")) {
            masterGraphics.clearRect(x, y, image.getWidth(), image.getHeight());
        }
    }
    reader.dispose();

    return frames.toArray(new ImageFrame[frames.size()]);
}

private class ImageFrame {
    private final int delay;
    private final BufferedImage image;
    private final String disposal;

    public ImageFrame(BufferedImage image, int delay, String disposal) {
        this.image = image;
        this.delay = delay;
        this.disposal = disposal;
    }

    public BufferedImage getImage() {
        return image;
    }

    public int getDelay() {
        return delay;
    }

    public String getDisposal() {
        return disposal;
    }
}

There is a good description of how GIF animations work in this ImageMagick tutorial.