ImageIO
| Language | Java (part of the JDK) |
|---|---|
| License | GPL-2.0 with Classpath Exception (OpenJDK) |
| Repository | github.com/openjdk/jdk (java.desktop module) |
| Documentation | javax.imageio, GIF metadata format |
| Depends on | nothing |
| Read / write | yes / 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#
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#
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#
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:
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#
- To get full frames, draw each frame onto a canvas
BufferedImageat itsimageLeftPosition/imageTopPosition, following the disposal method of the previous frame. - For transparent frames, write
TYPE_BYTE_INDEXEDimages whoseIndexColorModelhas a transparent entry, and settransparentColorFlagandtransparentColorIndexto match. - For better palettes, quantize frames yourself into
TYPE_BYTE_INDEXEDimages with anIndexColorModelbefore writing.