Создание анимированных GIF средствами Java

Недавно в целях демонстрации работы алгоритма фрактального сжатия мне понадобилось создать GIF-анимацию средствами Java. Стандартная библиотека простого способа это сделать не предоставляла. В связи с этим встала необходимость найти подходящий сниппет. Однако решений в интернете оказалось немного, да и у каждого из них были свои нюансы. С целью облегчить задачу по поиску оптимального решения и была написана эта статья.

Для начала упомяну платное решение — библиотеку Gif4J. Согласно приведенной на главной странице проекта информации, библиотека позволяет не только создавать анимированные GIF-изображения, но и изменять их размер, настраивать цветовой баланс, ставить водяные знаки, получать метаданные из изображений, добавлять текст и т.п. Словом, охватывает весь спектр необходимых действий по работе с GIF-изображениями.

Поиск решения более узкой задачи — поясню, что требуется только создать зацикленную анимацию, состоящую из последовательности изображений одного размера — дал следующие результаты.

Класс GifSequenceWriter, разработанный Эллиотом Кру, который можно скомпилировать и запустить (он содержит метод main), преобразует несколько файлов одинакового размера в GIF-анимацию. В качестве настроек можно указать, нужно ли зацикливать анимацию или нет, и длительность каждого кадра. Именно им я и воспользовался для решения своей задачи:

Прим. В комментариях в коде неверно описаны параметры:

// create a gif sequence with the type of the first image, 1 second
// between frames, which loops continuously
GifSequenceWriter writer = 
    new GifSequenceWriter(output, firstImage.getType(), 1, false);

На самом деле третий параметр задает задержку перед сменой кадра в сотых долях секунды, а четвертый — нужно ли зацикливать анимацию, и значение false указывает, что зацикливания не будет. Поэтому по умолчанию анимация не зацикливается и кадры сменяются каждую сотую долю секунды.

Коротко поясню, как работает GifSequenceWriter. В нем GIF-файл описывается через метод setAttribute класса IIOMetadataNode в соответствии со спецификациями w3.org, после чего результат записывается ImageWriter’ом в выходной файл.

Однако у реализованного подхода есть два недостатка. Во-первых, он совершенно не сжимает выходной файл, а потому его размер оказывается неоправданно большим. Помочь справиться с этой проблемой вам может один из специальных инструментов, доступных онлайн — например, Compressor.io.

Во-вторых, GifSequenceWriter, как и многие другие реализации, не решает проблемы с прозрачными пикселями: в каждом следующем кадре сквозь них просвечивают пиксели предыдущего. Например, вот эти две картинки:

pic3  pic5

Объединяются в следующую анимацию:

Иногда это может быть неудобно. Для решения этой проблемы существует класс GifFrame. Он индексирует один из цветов как прозрачный (по умолчанию в примере это 0xFF00FF, то есть пурпурный). Результат для вышеупомянутой пары изображений будет иметь следующий вид:

Кстати, в коде, приведенном по ссылке не хватает import‘ов. Для вашего удобства перечислю их здесь:

import org.w3c.dom.Node;
import javax.imageio.*;
import javax.imageio.metadata.*;
import javax.imageio.stream.ImageOutputStream;
import java.awt.*;
import java.awt.image.*;
import java.util.List;
import java.util.ArrayList;
import java.io.*;

Для обеспечения аналогичной функциональности вышеупомянутому классу GifSequenceWriter можете добавить следующий метод main:

public static void main(String[] args) throws Exception {
	java.util.List<GifFrame> gifFrames = new ArrayList<GifFrame>();
	for(int i=0; i<args.length-1; i++) {
		BufferedImage image = ImageIO.read(new File(args[i]));
		int transparantColor = 0xFF00FF; // purple
		BufferedImage gif = ImageUtil.convertRGBAToGIF(image, transparantColor);
		long delay = 1000; // every frame takes 1s
		String disposal = GifFrame.RESTORE_TO_BGCOLOR; // make transparent pixels not 'shine through'
		gifFrames.add(new GifFrame(gif, delay, disposal));
	}
	OutputStream outputStream = new FileOutputStream(new File(args[args.length - 1]));

	int loopCount = 0; // loop indefinitely
	ImageUtil.saveAnimatedGIF(outputStream, gifFrames, loopCount);
}

Также набор интересных сниппетов по работе с GIF изображениями на Java приведен по этой ссылке.