Gif.java
/*
* ..::jDrawingLib::..
*
* Copyright (C) Federico Vera 2012 - 2023 <[email protected]>
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or any later
* version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.dkt.graphics.utils;
import com.dkt.graphics.canvas.Canvas;
import com.dkt.graphics.exceptions.InvalidArgumentException;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.stream.FileImageOutputStream;
import javax.imageio.stream.ImageOutputStream;
import org.w3c.dom.Node;
/**
* The purpose of this class is to create animated GIFs from a series of canvas
* snapshots
*
* @author Federico Vera {@literal<[email protected]>}
* @since 0.1.10
*/
public class Gif {
private final ArrayList<Wrapper> snapshots;
private final Canvas canvas;
private int delay = 10;
private Exception exception;
/**
* Creates a new Gif object.
*
* @param canvas the canvas from which to create the images
* @throws IllegalArgumentException if {@code canvas} is {@code null}
* @see Gif#write(String, int, BufferedImage...)
*/
public Gif (Canvas canvas) throws IllegalArgumentException {
if (canvas == null) {
throw new IllegalArgumentException("The canvas can't be null");
}
this.canvas = canvas;
snapshots = new ArrayList<>(10);
}
/**
* Retrieves the number of snapshots taken
*
* @return number of snapshots
*/
public int size() {
return snapshots.size();
}
/**
* Removes on of the snapshots
*
* @param idx index of the snapshot
* @return the image that was removed or {@code null} if the index is out
* of bounds.
*/
public BufferedImage remove(int idx) {
try {
return snapshots.remove(idx).image;
} catch (IndexOutOfBoundsException ex) {
return null;
}
}
/**
* Retrieves one of the snapshots
*
* @param idx index of th image
* @return snapshot or {@code null} if the index is out of bounds
*/
public BufferedImage get(int idx) {
try {
return snapshots.get(idx).image;
} catch (IndexOutOfBoundsException ex) {
return null;
}
}
/**
* Retrieves the delay between images in ms.<br>
* The default value is 100ms.
*
* @return delay between images
* @see Gif#setDelay(int)
*/
public int getDelay() {
return delay * 10;
}
/**
* The delay between images in ms. Note that since the <a href=
* "http://www.w3.org/Graphics/GIF/spec-gif89a.txt">GIF specification</a>
* sets the delay in hundredths of a second the number of ms is actually
* divided by ten.
*
* @param delay delay in ms
* @see Gif#getDelay()
*/
public void setDelay(int delay) {
this.delay = delay / 10;
}
/**
* This method should be called every time you want a snapshot of the canvas
* to be taken. You must take at least 1 in order for the save method to
* do something.
*
* @see Gif#snapshot(int)
*/
public void snapshot() {
snapshot(1);
}
/**
* This method calls {@link Gif#snapshot()} a given number of times, which
* results in (besides an increase in size) the image staying on the gif
* {@code num} times {@code delay} seconds.
*
* @param num number of snapshots
* @throws InvalidArgumentException if {@code num} is less than 1
* @see Gif#snapshot()
*/
public void snapshot(int num) {
if (num < 1) {
throw new InvalidArgumentException("num must be bigger than 1");
}
synchronized(canvas) {
snapshots.add(new Wrapper(Utils.getImage(canvas, true), num));
}
}
/**
* Writes the image to a file, if this method fails it will return {@code
* false} and the exception will be saved.
*
* @param path {@link String} representing the path and name of the file
* @return {@code true} id the {@code gif} was correctly written and {@code
* false} if something goes wrong.
* @throws IllegalArgumentException if {@code path} is {@code null}
* @see Gif#getLastException()
* @see Gif#setDelay(int)
*/
public boolean write(String path) throws IllegalArgumentException {
if (path == null) {
throw new IllegalArgumentException("The path can't be null");
}
exception = null;
ArrayList<BufferedImage> imgs = new ArrayList<>(size() * 2);
for (Wrapper wrap : snapshots) {
for (int i = 0; i < wrap.num; i++) {
imgs.ensureCapacity(imgs.size() + wrap.num);
imgs.add(wrap.image);
}
}
try {
write(path, delay, imgs.toArray(new BufferedImage[0]));
} catch (IOException ex) {
Logger.getLogger("Gif").log(Level.SEVERE, null, ex);
exception = ex;
return false;
}
return true;
}
/**
* Retrieves the last exception in case such exception exists. <br>
* This method will always return {@code null} before
* {@link Gif#write(String)} is called for the first time.
*
* @return the exception or {@code null} if none exists.
* @see Gif#write(String)
*/
public Exception getLastException() {
return exception;
}
/**
* Creates a new {@code gif} from an array of {@link BufferedImage}.
*
* @param path The path in which to save the {@code gif}
* @param delay The delay between frames in hundredths of a second
* @param imgs Array of images
* @throws IllegalArgumentException if {@code path} is {@code null}
* @throws IOException if something goes wrong when saving the image
*/
public static void write(
String path,
int delay,
BufferedImage... imgs) throws IllegalArgumentException, IOException
{
if (path == null) {
throw new IllegalArgumentException("The path can't be null");
}
final File file = new File(path);
try (ImageOutputStream fos = new FileImageOutputStream(file);
Writer gsw = new Writer(fos, delay)) {
for (BufferedImage img : imgs) {
gsw.add(img);
}
}
}
private record Wrapper(BufferedImage image, int num) {
}
/**
* This class is based on the one created by Elliot Kroo. The original code
* can be found <a href="http://elliot.kroo.net/software/java/">here</a>.
*/
private static class Writer implements Closeable {
private final ImageWriter writer;
private final ImageWriteParam iwparam;
private final IIOMetadata mdata;
private Writer(ImageOutputStream os, int delay) throws IOException {
final int type = BufferedImage.TYPE_INT_ARGB;
//Get a writer for the GIF
writer = ImageIO.getImageWritersByMIMEType("image/gif").next();
iwparam = writer.getDefaultWriteParam();
//Generate the necessary metadata
final ImageTypeSpecifier typeSpec;
typeSpec = ImageTypeSpecifier.createFromBufferedImageType(type);
mdata = writer.getDefaultImageMetadata(typeSpec, iwparam);
//Set the necessary attributes
final String fname = mdata.getNativeMetadataFormatName();
final Node tree = mdata.getAsTree(fname);
setGCEAttributes(getNode(tree, "GraphicControlExtension"), delay);
getNode(tree, "ApplicationExtensions").appendChild(new LoopNode());
mdata.setFromTree(fname, tree);
writer.setOutput(os);
writer.prepareWriteSequence(null);
}
private void add(RenderedImage img) throws IOException {
writer.writeToSequence(new IIOImage(img, null, mdata), iwparam);
}
@Override
public void close() throws IOException {
writer.endWriteSequence();
}
private static IIOMetadataNode getNode(Node root, String name) {
Node node = root.getFirstChild();
for (;node != null; node = node.getNextSibling()) {
if (node.getNodeName().equalsIgnoreCase(name)) {
return (IIOMetadataNode) node;
}
}
final IIOMetadataNode nnode = new IIOMetadataNode(name);
root.appendChild(nnode);
return nnode;
}
private static void setGCEAttributes(IIOMetadataNode gcen, int delay) {
gcen.setAttribute("disposalMethod", "none");
gcen.setAttribute("userInputFlag", "FALSE");
gcen.setAttribute("transparentColorFlag", "TRUE");
gcen.setAttribute("delayTime", Integer.toString(delay));
gcen.setAttribute("transparentColorIndex", "0");
}
}
private static class LoopNode extends IIOMetadataNode {
public LoopNode () {
super("ApplicationExtension");
setAttribute("applicationID", "NETSCAPE");
setAttribute("authenticationCode", "2.0");
setUserObject(new byte[]{1,0,0,0});
}
}
}