/*
* This file is part of HoloAPI.
*
* HoloAPI 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
* (at your option) any later version.
*
* HoloAPI 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 HoloAPI. If not, see <http://www.gnu.org/licenses/>.
*/
package com.dsh105.holoapi.image;
import com.google.common.collect.ImmutableList;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
/**
* Represents a generator used to produce animated image frames from either a GIF or set of images
*/
public class AnimatedImageGenerator implements Generator {
// https://github.com/aadnk/DisplayFloatingImages/blob/master/DisplayFloatingImage/src/main/java/com/comphenix/example/nametags/GifImageMessage.java
protected ImmutableList<GIFFrame> frames;
private String key;
private int maxHeight;
private GIFFrame largestFrame;
/**
* Constructs an AnimatedImageGenerator for use in an AnimatedHologram
*
* @param frameRate frame rate of the hologram display
* @param imageFrames frames to use in the animated hologram
*/
public AnimatedImageGenerator(int frameRate, ImageGenerator... imageFrames) {
for (ImageGenerator generator : imageFrames) {
frames.add(new GIFFrame(generator, frameRate));
}
}
/**
* Constructs an AnimatedImageGenerator for use in an AnimatedHologram
*
* @param gifFile GIF file used to generate the display
* @param frameRate frame rate of the hologram display
* @param height height of the display
* @param imgChar {@link com.dsh105.holoapi.image.ImageChar} of the display
* @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
*/
public AnimatedImageGenerator(File gifFile, int frameRate, int height, ImageChar imgChar) {
this(gifFile, frameRate, height, imgChar, false);
}
/**
* Constructs an AnimatedImageGenerator for use in an AnimatedHologram
*
* @param gifFile GIF file used to generate the display
* @param frameRate frame rate of the hologram display
* @param height height of the display
* @param imgChar {@link com.dsh105.holoapi.image.ImageChar} of the display
* @param requiresBorder whether the display requires a border
* @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
*/
public AnimatedImageGenerator(File gifFile, int frameRate, int height, ImageChar imgChar, boolean requiresBorder) {
try {
this.frames = this.readGif(gifFile);
} catch (IOException e) {
throw new RuntimeException("Cannot read file " + gifFile.getPath(), e);
}
this.prepare(height, imgChar, requiresBorder);
this.prepareFrameRate(frameRate);
}
/**
* Constructs an AnimatedImageGenerator for use in an AnimatedHologram
*
* @param gifFile GIF file used to generate the display
* @param height height of the display
* @param imgChar {@link com.dsh105.holoapi.image.ImageChar} of the display
* @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
*/
public AnimatedImageGenerator(File gifFile, int height, ImageChar imgChar) {
this(gifFile, height, imgChar, false);
}
/**
* Constructs an AnimatedImageGenerator for use in an AnimatedHologram
*
* @param gifFile GIF file used to generate the display
* @param height height of the display
* @param imgChar {@link com.dsh105.holoapi.image.ImageChar} of the display
* @param requiresBorder whether the display requires a border
* @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
*/
public AnimatedImageGenerator(File gifFile, int height, ImageChar imgChar, boolean requiresBorder) {
try {
this.frames = this.readGif(gifFile);
} catch (IOException e) {
throw new RuntimeException("Cannot read file " + gifFile.getPath(), e);
}
this.prepare(height, imgChar, requiresBorder);
}
/**
* Constructs an AnimatedImageGenerator for use in an AnimatedHologram
*
* @param input {@link java.io.InputStream} used to generate the display
* @param frameRate frame rate of the hologram display
* @param height height of the display
* @param imgChar {@link com.dsh105.holoapi.image.ImageChar} of the display
* @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
*/
public AnimatedImageGenerator(InputStream input, int frameRate, int height, ImageChar imgChar) {
this(input, frameRate, height, imgChar, false);
}
/**
* Constructs an AnimatedImageGenerator for use in an AnimatedHologram
*
* @param input {@link java.io.InputStream} used to generate the display
* @param frameRate frame rate of the hologram display
* @param height height of the display
* @param imgChar {@link com.dsh105.holoapi.image.ImageChar} of the display
* @param requiresBorder whether the display requires a border
* @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
*/
public AnimatedImageGenerator(InputStream input, int frameRate, int height, ImageChar imgChar, boolean requiresBorder) {
try {
this.frames = this.readGif(input);
} catch (IOException e) {
throw new RuntimeException("Cannot read input", e);
}
this.prepare(height, imgChar, requiresBorder);
this.prepareFrameRate(frameRate);
}
/**
* Constructs an AnimatedImageGenerator for use in an AnimatedHologram
*
* @param input {@link java.io.InputStream} used to generate the display
* @param height height of the display
* @param imgChar {@link com.dsh105.holoapi.image.ImageChar} of the display
* @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
*/
public AnimatedImageGenerator(InputStream input, int height, ImageChar imgChar) {
this(input, height, imgChar, false);
}
/**
* Constructs an AnimatedImageGenerator for use in an AnimatedHologram
*
* @param input {@link java.io.InputStream} used to generate the display
* @param height height of the display
* @param imgChar {@link com.dsh105.holoapi.image.ImageChar} of the display
* @param requiresBorder whether the display requires a border
* @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
*/
public AnimatedImageGenerator(InputStream input, int height, ImageChar imgChar, boolean requiresBorder) {
try {
this.frames = this.readGif(input);
} catch (IOException e) {
throw new RuntimeException("Cannot read input", e);
}
this.prepare(height, imgChar, requiresBorder);
}
/**
* Constructs an AnimatedImageGenerator for use in an AnimatedHologram
*
* @param key key to store the generator under. Only used for recognition of stored animation generators
* @param frameRate frame rate of the hologram display
* @param imageFrames frames to use in the animated hologram
*/
public AnimatedImageGenerator(String key, int frameRate, ImageGenerator... imageFrames) {
this(frameRate, imageFrames);
this.key = key;
}
/**
* Constructs an AnimatedImageGenerator for use in an AnimatedHologram
*
* @param key key to store the generator under. Only used for recognition of stored animation generators
* @param gifFile GIF file used to generate the display
* @param frameRate frame rate of the hologram display
* @param height height of the display
* @param imgChar {@link com.dsh105.holoapi.image.ImageChar} of the display
* @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
*/
public AnimatedImageGenerator(String key, File gifFile, int frameRate, int height, ImageChar imgChar) {
this(key, gifFile, frameRate, height, imgChar, false);
}
/**
* Constructs an AnimatedImageGenerator for use in an AnimatedHologram
*
* @param key key to store the generator under. Only used for recognition of stored animation generators
* @param gifFile GIF file used to generate the display
* @param frameRate frame rate of the hologram display
* @param height height of the display
* @param imgChar {@link com.dsh105.holoapi.image.ImageChar} of the display
* @param requiresBorder whether the display requires a border
* @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
*/
public AnimatedImageGenerator(String key, File gifFile, int frameRate, int height, ImageChar imgChar, boolean requiresBorder) {
this(gifFile, frameRate, height, imgChar, requiresBorder);
this.key = key;
}
/**
* Constructs an AnimatedImageGenerator for use in an AnimatedHologram
*
* @param key key to store the generator under. Only used for recognition of stored animation generators
* @param gifFile GIF file used to generate the display
* @param height height of the display
* @param imgChar {@link com.dsh105.holoapi.image.ImageChar} of the display
* @throws java.io.IOException If an input exception occurred or the image could not be found
*/
public AnimatedImageGenerator(String key, File gifFile, int height, ImageChar imgChar) throws IOException {
this(key, gifFile, height, imgChar, false);
}
/**
* Constructs an AnimatedImageGenerator for use in an AnimatedHologram
*
* @param key key to store the generator under. Only used for recognition of stored animation generators
* @param gifFile GIF file used to generate the display
* @param height height of the display
* @param imgChar {@link com.dsh105.holoapi.image.ImageChar} of the display
* @param requiresBorder whether the display requires a border
* @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
*/
public AnimatedImageGenerator(String key, File gifFile, int height, ImageChar imgChar, boolean requiresBorder) {
this(gifFile, height, imgChar, requiresBorder);
this.key = key;
}
/**
* Constructs an AnimatedImageGenerator for use in an AnimatedHologram
*
* @param key key to store the generator under. Only used for recognition of stored animation generators
* @param input {@link java.io.InputStream} used to generate the display
* @param frameRate frame rate of the hologram display
* @param height height of the display
* @param imgChar {@link com.dsh105.holoapi.image.ImageChar} of the display
* @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
*/
public AnimatedImageGenerator(String key, InputStream input, int frameRate, int height, ImageChar imgChar) {
this(key, input, frameRate, height, imgChar, false);
}
/**
* Constructs an AnimatedImageGenerator for use in an AnimatedHologram
*
* @param key key to store the generator under. Only used for recognition of stored animation generators
* @param input {@link java.io.InputStream} used to generate the display
* @param frameRate frame rate of the hologram display
* @param height height of the display
* @param imgChar {@link com.dsh105.holoapi.image.ImageChar} of the display
* @param requiresBorder whether the display requires a border
* @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
*/
public AnimatedImageGenerator(String key, InputStream input, int frameRate, int height, ImageChar imgChar, boolean requiresBorder) {
this(input, frameRate, height, imgChar, requiresBorder);
this.key = key;
}
/**
* Constructs an AnimatedImageGenerator for use in an AnimatedHologram
*
* @param key key to store the generator under. Only used for recognition of stored animation generators
* @param input {@link java.io.InputStream} used to generate the display
* @param height height of the display
* @param imgChar {@link com.dsh105.holoapi.image.ImageChar} of the display
* @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
*/
public AnimatedImageGenerator(String key, InputStream input, int height, ImageChar imgChar) {
this(key, input, height, imgChar, false);
}
/**
* Constructs an AnimatedImageGenerator for use in an AnimatedHologram
*
* @param key key to store the generator under. Only used for recognition of stored animation generators
* @param input {@link java.io.InputStream} used to generate the display
* @param height height of the display
* @param imgChar {@link com.dsh105.holoapi.image.ImageChar} of the display
* @param requiresBorder whether the display requires a border
* @throws java.lang.RuntimeException If an input exception occurred or the image could not be found
*/
public AnimatedImageGenerator(String key, InputStream input, int height, ImageChar imgChar, boolean requiresBorder) {
this(input, height, imgChar, requiresBorder);
this.key = key;
}
protected AnimatedImageGenerator(String key) {
this.key = key;
}
protected void prepare(int height, ImageChar imgChar) {
this.prepare(height, imgChar, false);
}
protected void prepare(int height, ImageChar imgChar, boolean requiresBorder) {
this.calculateMaxHeight();
for (GIFFrame frame : frames) {
int imageHeight = (int) ((frame.image.getHeight() / (double) this.maxHeight) * height);
frame.imageGenerator = new ImageGenerator(frame.image, imageHeight, imgChar, requiresBorder);
}
}
protected void prepareFrameRate(int frameRate) {
for (GIFFrame frame : this.frames) {
frame.delay = frameRate;
}
}
protected void calculateMaxHeight() {
int maxHeight = 0;
GIFFrame largestFrame = this.frames.get(0);
for (GIFFrame frame : frames) {
if (frame.image.getHeight() > maxHeight) {
maxHeight = frame.image.getHeight();
largestFrame = frame;
}
}
this.maxHeight = maxHeight;
this.largestFrame = largestFrame;
}
protected ImmutableList<GIFFrame> readGif(InputStream input) throws IOException {
GIFFrame[] frames;
ImageReader reader = ImageIO.getImageReadersBySuffix("GIF").next();
reader.setInput(ImageIO.createImageInputStream(input));
frames = readGIF(reader);
return ImmutableList.copyOf(frames);
}
private ImmutableList<GIFFrame> readGif(File inputFile) throws IOException {
GIFFrame[] frames;
ImageReader reader = ImageIO.getImageReadersBySuffix("GIF").next();
reader.setInput(ImageIO.createImageInputStream(inputFile));
frames = readGIF(reader);
return ImmutableList.copyOf(frames);
}
// Source: https://stackoverflow.com/questions/8933893/convert-animated-gif-frames-to-separate-bufferedimages-java
private GIFFrame[] readGIF(ImageReader reader) throws IOException {
ArrayList<GIFFrame> frames = new ArrayList<>(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 GIFFrame(copy, (int) Math.ceil(delay / 2.5D), disposal));
if (disposal.equals("restoreToPrevious")) {
BufferedImage from = null;
for (int i = frameIndex - 1; i >= 0; i--) {
if (!frames.get(i).disposal.equals("restoreToPrevious") || frameIndex == 0) {
from = frames.get(i).image;
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 GIFFrame[frames.size()]);
}
@Override
public String getKey() {
return key;
}
/**
* Gets the frames of the generator
*
* @return frames of the generator
*/
public ImmutableList<GIFFrame> getFrames() {
return this.frames;
}
/**
* Gets the maximum frame height
*
* @return maximum frame height
*/
public int getMaxHeight() {
return maxHeight;
}
/**
* Gets the largest frame of the generator
*
* @return largest frame of the generator
*/
public GIFFrame getLargestFrame() {
return largestFrame;
}
}