minor tweaks. lots of comments
This commit is contained in:
parent
81f153e13a
commit
d99c864402
|
@ -12,7 +12,7 @@ import javax.imageio.stream.ImageInputStream;
|
|||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* Helper for extracting each frame of animation from a GIF as a separate BufferedImage.
|
||||
|
@ -24,23 +24,32 @@ import java.util.ArrayList;
|
|||
* @author gered (_extremely_ minor tweaks)
|
||||
*/
|
||||
public class AnimatedGif {
|
||||
public static ImageFrame[] read(ImageInputStream stream) throws IOException {
|
||||
public static LinkedList<ImageFrame> read(ImageInputStream stream) throws IOException {
|
||||
ImageReader reader = ImageIO.getImageReadersByFormatName("gif").next();
|
||||
reader.setInput(stream, false);
|
||||
|
||||
ArrayList<ImageFrame> frames = new ArrayList<ImageFrame>(2);
|
||||
// note: using a LinkedList so we can do some quick filtering out of zero delay frames in the future
|
||||
LinkedList<ImageFrame> frames = new LinkedList<ImageFrame>();
|
||||
|
||||
// will hold the size of the "canvas" which we will be drawing each frame into to generate complete
|
||||
// BufferedImage instances for each frames ImageFrame instance. this is the full width/height of the entire
|
||||
// animated gif
|
||||
int width = -1;
|
||||
int height = -1;
|
||||
|
||||
// get the logical screen dimensions (canvas dimensions) and set the ImageFrame width/height's using it if it
|
||||
// is present at all. if not present, we will ignore for now
|
||||
// NOTE: it's very important that we prefer to use this section's width/height rather then
|
||||
// the first frame's dimensions. it is not guaranteed that the first frame will be
|
||||
// sized to the full image size (commonly it will be, but not always)
|
||||
IIOMetadata metadata = reader.getStreamMetadata();
|
||||
if (metadata != null) {
|
||||
IIOMetadataNode globalRoot = (IIOMetadataNode) metadata.getAsTree(metadata.getNativeMetadataFormatName());
|
||||
IIOMetadataNode globalRoot = (IIOMetadataNode)metadata.getAsTree(metadata.getNativeMetadataFormatName());
|
||||
|
||||
NodeList globalScreenDescriptor = globalRoot.getElementsByTagName("LogicalScreenDescriptor");
|
||||
|
||||
if (globalScreenDescriptor != null && globalScreenDescriptor.getLength() > 0) {
|
||||
IIOMetadataNode screenDescriptor = (IIOMetadataNode) globalScreenDescriptor.item(0);
|
||||
IIOMetadataNode screenDescriptor = (IIOMetadataNode)globalScreenDescriptor.item(0);
|
||||
|
||||
if (screenDescriptor != null) {
|
||||
width = Integer.parseInt(screenDescriptor.getAttribute("logicalScreenWidth"));
|
||||
|
@ -49,35 +58,60 @@ public class AnimatedGif {
|
|||
}
|
||||
}
|
||||
|
||||
BufferedImage master = null;
|
||||
Graphics2D masterGraphics = null;
|
||||
// canvas image. this is going to be our "scratch space" which we will draw each frame into to generate a full
|
||||
// BufferedImage object for and set in an ImageFrame instance for each frame of animation. this is necessary
|
||||
// because some types of animation will specify some frames as smaller images which need to be rendered at
|
||||
// certain positions on top of the previous frame, so having a canvas to draw on makes generating the full
|
||||
// image for each frame much simpler
|
||||
BufferedImage canvas = null;
|
||||
Graphics2D canvasGraphics = null;
|
||||
|
||||
for (int frameIndex = 0;; frameIndex++) {
|
||||
for (int frameIndex = 0; ; frameIndex++) {
|
||||
BufferedImage image;
|
||||
try {
|
||||
image = reader.read(frameIndex);
|
||||
} catch (IndexOutOfBoundsException io) {
|
||||
// no more frames
|
||||
break;
|
||||
}
|
||||
|
||||
// if there was no logical screen descriptor, then will not have gotten any image dimensions by the
|
||||
// time we're here reading the first animation frame, so we can just use the first frame's dimensions,
|
||||
// after which we won't care about these anymore.
|
||||
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);
|
||||
if (canvas == null) {
|
||||
// initialize our canvas image. we do this here because the only place we are 100% guaranteed to
|
||||
// have the full animated gifs dimensions at is after we have read the first frame of animation,
|
||||
// because the LogicalScreenDescriptor might not have had it, forcing us to wait until this point.
|
||||
canvas = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||
canvasGraphics = canvas.createGraphics();
|
||||
canvasGraphics.setBackground(new Color(0, 0, 0, 0));
|
||||
}
|
||||
|
||||
// get this frame's GraphicControlExtension which has various properties we need about this frame
|
||||
IIOMetadata frameMetadata = reader.getImageMetadata(frameIndex);
|
||||
IIOMetadataNode root = (IIOMetadataNode)frameMetadata.getAsTree(frameMetadata.getNativeMetadataFormatName());
|
||||
IIOMetadataNode gce = (IIOMetadataNode)root.getElementsByTagName("GraphicControlExtension").item(0);
|
||||
|
||||
// delay specified as 1/100 of a second (*10 to get in milliseconds).
|
||||
// TODO: zero delay frames are meant as intermediate frames to "prepare" the canvas for the following
|
||||
// frames (i guess as a way to clear/fill the background for a bunch of upcoming frames which
|
||||
// don't fill the entire canvas?). we probably *do* need to add these (temporarily) to the frames
|
||||
// array so that we guarantee proper disposal method handling, but the final array we return from
|
||||
// this method probably should not include them
|
||||
// More info: http://www.imagemagick.org/Usage/anim_basics/#zero
|
||||
int delay = Integer.valueOf(gce.getAttribute("delayTime"));
|
||||
|
||||
String disposal = gce.getAttribute("disposalMethod");
|
||||
|
||||
// get the offset to render this frame's BufferedImage at on the canvas (this frame's image might be a
|
||||
// smaller image that is to overlap with the previous frame)
|
||||
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);
|
||||
|
@ -87,13 +121,21 @@ public class AnimatedGif {
|
|||
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);
|
||||
// draw this frame into our canvas
|
||||
canvasGraphics.drawImage(image, x, y, null);
|
||||
|
||||
// create an ImageFrame instance for this frame, using the current contents of our canvas image (which
|
||||
// should at this point have the full image contents to accurately draw this frame of animation)
|
||||
BufferedImage copy = new BufferedImage(canvas.getColorModel(), canvas.copyData(null), canvas.isAlphaPremultiplied(), null);
|
||||
frames.add(new ImageFrame(copy, delay, disposal));
|
||||
|
||||
// handle certain disposal methods
|
||||
if (disposal.equals("restoreToPrevious")) {
|
||||
// "When the current image is finished, return the canvas to what it looked like before the image was
|
||||
// overlaid. If the previous frame image also used a ['restoreToPrevious'] disposal method, then the
|
||||
// result will be that same as what it was before that frame.. etc.. etc.. etc..."
|
||||
// -- http://www.imagemagick.org/Usage/anim_basics/#dispose
|
||||
BufferedImage from = null;
|
||||
for (int i = frameIndex - 1; i >= 0; i--) {
|
||||
if (!frames.get(i).disposal.equals("restoreToPrevious") || frameIndex == 0) {
|
||||
|
@ -102,15 +144,26 @@ public class AnimatedGif {
|
|||
}
|
||||
}
|
||||
|
||||
master = new BufferedImage(from.getColorModel(), from.copyData(null), from.isAlphaPremultiplied(), null);
|
||||
masterGraphics = master.createGraphics();
|
||||
masterGraphics.setBackground(new Color(0, 0, 0, 0));
|
||||
// reset the canvas to the previous frame which we found above
|
||||
canvas = new BufferedImage(from.getColorModel(), from.copyData(null), from.isAlphaPremultiplied(), null);
|
||||
canvasGraphics = canvas.createGraphics();
|
||||
canvasGraphics.setBackground(new Color(0, 0, 0, 0));
|
||||
|
||||
} else if (disposal.equals("restoreToBackgroundColor")) {
|
||||
masterGraphics.clearRect(x, y, image.getWidth(), image.getHeight());
|
||||
// "When the time delay is finished for a particular frame, the area that was overlaid by that frame
|
||||
// is cleared. Not the whole canvas, just the area that was overlaid. Once that is done then the
|
||||
// resulting canvas is what is passed to the next frame of the animation, to be overlaid by that
|
||||
// frames image."
|
||||
// -- http://www.imagemagick.org/Usage/anim_basics/#dispose
|
||||
|
||||
// clears the region of the canvas that was overlapped by this frame's image, such that the canvas is
|
||||
// ready for the next frame
|
||||
canvasGraphics.clearRect(x, y, image.getWidth(), image.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
reader.dispose();
|
||||
|
||||
return frames.toArray(new ImageFrame[frames.size()]);
|
||||
return frames;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue