All formats

ImageIO

LanguageJava (part of the JDK)
LicenseGPL-2.0 with Classpath Exception (OpenJDK)
Repositorygithub.com/openjdk/jdk (java.desktop module)
Documentationjavax.imageio, GIF metadata format
Depends onnothing
Read / writeyes / yes; animation through write sequences and metadata trees, transparency, comments

The JDK has a GIF reader and writer in javax.imageio. ImageIO.read returns the first frame. For animations, an ImageReader gives each frame with read(i) and its metadata with getImageMetadata(i); an ImageWriter writes frames with writeToSequence.

Frame data is exposed as a DOM tree in the javax_imageio_gif_image_1.0 format: ImageDescriptor (position and size), GraphicControlExtension (delay, disposal, transparency), ApplicationExtensions (the Netscape loop block). Frames are returned as stored, often smaller than the canvas; you composite them yourself. The writer converts RGB images to a palette with its own octree quantizer, with no dithering control.

Installation#

Nothing to install. In a modular application add requires java.desktop;.

Basic usage: read all frames with delays#

Java
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

public class ReadGif {
    public static void main(String[] args) throws IOException {
        ImageReader reader = ImageIO.getImageReadersByFormatName("gif").next();
        try (ImageInputStream in = ImageIO.createImageInputStream(new File("anim.gif"))) {
            reader.setInput(in, false);
            int count = reader.getNumImages(true);
            System.out.println(count + " frames");
            for (int i = 0; i < count; i++) {
                BufferedImage frame = reader.read(i);
                IIOMetadataNode root = (IIOMetadataNode)
                        reader.getImageMetadata(i).getAsTree("javax_imageio_gif_image_1.0");
                IIOMetadataNode desc = (IIOMetadataNode) root.getElementsByTagName("ImageDescriptor").item(0);
                IIOMetadataNode gce = (IIOMetadataNode) root.getElementsByTagName("GraphicControlExtension").item(0);
                String delay = gce != null ? gce.getAttribute("delayTime") : "0";      // 1/100 s
                String disposal = gce != null ? gce.getAttribute("disposalMethod") : "none";
                System.out.printf("frame %d: %dx%d at (%s,%s), delay %s, disposal %s%n", i,
                        frame.getWidth(), frame.getHeight(),
                        desc.getAttribute("imageLeftPosition"), desc.getAttribute("imageTopPosition"),
                        delay, disposal);
            }
        } finally {
            reader.dispose();
        }
    }
}

Disposal values are none, doNotDispose, restoreToBackgroundColor and restoreToPrevious.

How to write an animated GIF that loops forever#

Java
import javax.imageio.*;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.stream.ImageOutputStream;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.List;

public class WriteGif {
    static IIOMetadataNode child(IIOMetadataNode root, String name) {
        for (int i = 0; i < root.getLength(); i++) {
            if (root.item(i).getNodeName().equals(name)) return (IIOMetadataNode) root.item(i);
        }
        IIOMetadataNode node = new IIOMetadataNode(name);
        root.appendChild(node);
        return node;
    }

    static void write(List<BufferedImage> frames, File file, int delayCs) throws IOException {
        ImageWriter writer = ImageIO.getImageWritersByFormatName("gif").next();
        ImageWriteParam param = writer.getDefaultWriteParam();
        ImageTypeSpecifier type = ImageTypeSpecifier.createFromRenderedImage(frames.get(0));
        try (ImageOutputStream out = ImageIO.createImageOutputStream(file)) {
            writer.setOutput(out);
            writer.prepareWriteSequence(null);
            for (int i = 0; i < frames.size(); i++) {
                IIOMetadata meta = writer.getDefaultImageMetadata(type, param);
                String format = meta.getNativeMetadataFormatName();   // javax_imageio_gif_image_1.0
                IIOMetadataNode root = (IIOMetadataNode) meta.getAsTree(format);

                IIOMetadataNode gce = child(root, "GraphicControlExtension");
                gce.setAttribute("disposalMethod", "doNotDispose");
                gce.setAttribute("userInputFlag", "FALSE");
                gce.setAttribute("transparentColorFlag", "FALSE");
                gce.setAttribute("transparentColorIndex", "0");
                gce.setAttribute("delayTime", Integer.toString(delayCs));

                if (i == 0) {   // Netscape loop block: loop count 0 = forever
                    IIOMetadataNode app = new IIOMetadataNode("ApplicationExtension");
                    app.setAttribute("applicationID", "NETSCAPE");
                    app.setAttribute("authenticationCode", "2.0");
                    app.setUserObject(new byte[] {1, 0, 0});
                    child(root, "ApplicationExtensions").appendChild(app);
                }
                meta.setFromTree(format, root);
                writer.writeToSequence(new IIOImage(frames.get(i), null, meta), param);
            }
            writer.endWriteSequence();
        } finally {
            writer.dispose();
        }
    }
}

How to convert the first frame of a GIF to PNG#

Java
BufferedImage first = ImageIO.read(new File("anim.gif"));
ImageIO.write(first, "png", new File("first.png"));

How to read the logical screen size and loop count#

Stream metadata holds the canvas size; the loop count is in the first frame’s application extensions:

Java
IIOMetadataNode stream = (IIOMetadataNode)
        reader.getStreamMetadata().getAsTree("javax_imageio_gif_stream_1.0");
IIOMetadataNode screen = (IIOMetadataNode) stream.getElementsByTagName("LogicalScreenDescriptor").item(0);
System.out.println(screen.getAttribute("logicalScreenWidth") + " x " + screen.getAttribute("logicalScreenHeight"));

IIOMetadataNode first = (IIOMetadataNode) reader.getImageMetadata(0).getAsTree("javax_imageio_gif_image_1.0");
var apps = first.getElementsByTagName("ApplicationExtension");
for (int i = 0; i < apps.getLength(); i++) {
    IIOMetadataNode app = (IIOMetadataNode) apps.item(i);
    if ("NETSCAPE".equals(app.getAttribute("applicationID"))) {
        byte[] data = (byte[]) app.getUserObject();
        int loops = (data[1] & 0xFF) | ((data[2] & 0xFF) << 8);
        System.out.println("loop count: " + loops + " (0 = forever)");
    }
}

Tips#