Skip to content

Commit

Permalink
Add support for animated GIFs in NamedIcon
Browse files Browse the repository at this point in the history
  • Loading branch information
jcomuzzi committed Oct 7, 2018
1 parent b09a1fe commit 7a41ae3
Showing 1 changed file with 170 additions and 5 deletions.
175 changes: 170 additions & 5 deletions java/src/jmri/jmrit/catalog/NamedIcon.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,24 @@
import java.awt.image.ColorModel;
import java.awt.image.MemoryImageSource;
import java.awt.image.PixelGrabber;
import java.awt.image.RenderedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Iterator;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import javax.swing.ImageIcon;
import jmri.jmrit.display.PositionableLabel;
import jmri.util.FileUtil;
Expand All @@ -32,6 +47,8 @@
* @see jmri.jmrit.display.configurexml.PositionableLabelXml
* @author Bob Jacobsen Copyright 2002, 2008
* @author Pete Cressman Copyright (c) 2009, 2010
*
* Modified by Joe Comuzzi and Larry Allen to rotate animated GIFs
*/
public class NamedIcon extends ImageIcon {

Expand All @@ -42,7 +59,7 @@ public class NamedIcon extends ImageIcon {
* complete copy of pOld (no transformations done)
*/
public NamedIcon(NamedIcon pOld) {
this(pOld.mURL, pOld.mName);
this(pOld.mURL, pOld.mName, pOld.mGifInfo);
}

/**
Expand All @@ -53,7 +70,7 @@ public NamedIcon(NamedIcon pOld) {
* @param comp the container the new icon is embedded in
*/
public NamedIcon(NamedIcon pOld, Component comp) {
this(pOld.mURL, pOld.mName);
this(pOld.mURL, pOld.mName, pOld.mGifInfo);
setLoad(pOld._degrees, pOld._scale, comp);
setRotation(pOld.mRotation, comp);
}
Expand All @@ -68,6 +85,62 @@ public NamedIcon(NamedIcon pOld, Component comp) {
* @param pName Human-readable name for the icon
*/
public NamedIcon(String pUrl, String pName) {
this(pUrl, pName, null);

// See if this is a GIF file and if it is, see if it's animated. If it is,
// breakout the metadata and individual frames. Also collect the max sizes in case the
// frames aren't all the same.
try {
GIFMetadataImages gifState = new GIFMetadataImages();
Iterator<ImageReader> rIter = ImageIO.getImageReadersByFormatName("gif");
ImageReader gifReader = rIter.next();

InputStream is = FileUtil.findInputStream(mURL);
ImageInputStream iis = ImageIO.createImageInputStream(is);
gifReader.setInput(iis, false);

ImageReaderSpi spiProv = gifReader.getOriginatingProvider();
if (spiProv != null && spiProv.canDecodeInput(iis)) {

gifState.mStreamMd = gifReader.getStreamMetadata();
int numFrames = gifReader.getNumImages(true);

gifState.mFrames = new IIOImage[numFrames];
gifState.mWidth = 0;
gifState.mHeight = 0;
for (int i = 0; i < numFrames; i++) {
gifState.mFrames[i] = gifReader.readAll(i, null);
RenderedImage image = gifState.mFrames[i].getRenderedImage();
gifState.mHeight = Math.max(gifState.mHeight, image.getHeight());
gifState.mWidth = Math.max(gifState.mWidth, image.getWidth());
}

// No need to keep the GIF info if it's not animated, the old code works
// in that case.
if (numFrames > 1) {
mGifInfo = gifState;
}
}
} catch (IOException ioe) {
// If we get an exception here it's probably because the image isn't really
// a GIF. Unfortunately, there's no guarantee that it is a GIF just because
// canDecodeInput returns true.
log.debug("Exception extracting GIF Info: ", ioe);
mGifInfo = null;
}
}

/**
* Create a named icon that includes an image to be loaded from a URL.
* <p>
* The default access form is "file:", so a bare pathname to an icon file
* will also work for the URL argument.
*
* @param pUrl URL of image file to load
* @param pName Human-readable name for the icon
* @param pGifState Breakdown of GIF Image metadata and frames
*/
public NamedIcon(String pUrl, String pName, GIFMetadataImages pGifState) {
super(FileUtil.findURL(pUrl));
URL u = FileUtil.findURL(pUrl);
if (u == null) {
Expand All @@ -78,6 +151,7 @@ public NamedIcon(String pUrl, String pName) {
log.warn("Could not load image from {} (image is null)", pUrl);
}
mName = pName;
mGifInfo = pGifState;
mURL = FileUtil.getPortableFilename(pUrl);
mRotation = 0;
}
Expand Down Expand Up @@ -196,7 +270,16 @@ public void setRotation(int pRotation, Component comp) {

private String mName = null;
private String mURL = null;
private GIFMetadataImages mGifInfo = null;
private final Image mDefaultImage;

private class GIFMetadataImages {
private int mHeight;
private int mWidth;
private IIOImage mFrames[] = null;
private IIOMetadata mStreamMd;
};

/*
public Image getOriginalImage() {
return mDefaultImage;
Expand Down Expand Up @@ -323,6 +406,73 @@ public void transformImage(int w, int h, AffineTransform t, Component comp) {
}
return;
}
if (mGifInfo == null) {
setImage(transformFrame(getImage(), w, h, t, comp));
} else {
try {
String streamFormat = mGifInfo.mStreamMd.getNativeMetadataFormatName();
IIOMetadataNode streamTree = (IIOMetadataNode) mGifInfo.mStreamMd.getAsTree(streamFormat);
IIOMetadataNode logicalScreenDesc = getNode("LogicalScreenDescriptor", streamTree);
logicalScreenDesc.setAttribute("logicalScreenWidth", "" + w);
logicalScreenDesc.setAttribute("logicalScreenHeight", "" + h);

ByteArrayOutputStream oStream = new ByteArrayOutputStream();
Iterator<ImageWriter> wIter = ImageIO.getImageWritersByFormatName("gif");
ImageWriter writer = wIter.next();
ImageOutputStream ios = ImageIO.createImageOutputStream(oStream);
writer.setOutput(ios);

IIOMetadata newStreamMd = writer.getDefaultStreamMetadata(null);
newStreamMd.setFromTree(streamFormat, streamTree);
writer.prepareWriteSequence(newStreamMd);
for (int i = 0; i < mGifInfo.mFrames.length; i++) {
BufferedImage image = (BufferedImage) mGifInfo.mFrames[i].getRenderedImage();
ImageTypeSpecifier imgType = new ImageTypeSpecifier(image);
IIOMetadata imageMd = mGifInfo.mFrames[i].getMetadata();

BufferedImage bufIm = transformFrame(image, w, h, t, comp);

String imageFormat = imageMd.getNativeMetadataFormatName();
IIOMetadataNode imageMdTree = (IIOMetadataNode) imageMd.getAsTree(imageFormat);
IIOMetadataNode imageDesc = getNode("ImageDescriptor", imageMdTree);
if (imageDesc != null) {
imageDesc.setAttribute("imageWidth", "" + w);
imageDesc.setAttribute("imageHeight", "" + h);
}

IIOMetadataNode colorTable = getNode("LocalColorTable", imageMdTree);
if (colorTable != null) {
imageMdTree.removeChild(colorTable);
}

IIOMetadata newImageMd = writer.getDefaultImageMetadata(imgType, null);
newImageMd.setFromTree(imageFormat, imageMdTree);

IIOImage newImage = new IIOImage(bufIm, null, newImageMd);
writer.writeToSequence(newImage, null);
}
writer.endWriteSequence();
ios.close();

ImageIcon imageIcon = new ImageIcon(oStream.toByteArray());
setImage(imageIcon.getImage());
} catch (IOException ioe) {
log.error("Exception rotating animated GIF Image: ", ioe);
}
}
}

/**
* Private method which transforms one frame of Image
* @param frame Image frame to transform
* @param w Width
* @param h Height
* @param t Affine Transform
* @param comp
* @return
*/
private BufferedImage transformFrame(Image frame, int w, int h, AffineTransform t, Component comp) {

BufferedImage bufIm = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = bufIm.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_RENDERING,
Expand All @@ -333,9 +483,24 @@ public void transformImage(int w, int h, AffineTransform t, Component comp) {
RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
// g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, // Turned off due to poor performance, see Issue #3850 and PR #3855 for background
// RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g2d.drawImage(getImage(), t, comp);
setImage(bufIm);
g2d.drawImage(frame, t, comp);
g2d.dispose();
return bufIm;
}

/**
* Private method to manipulate DOM tree that represents image metadata.
* @param name Name of node we're searching for.
* @param root Plate to start search
* @return metadata node matching name
*/
private static IIOMetadataNode getNode(String name, IIOMetadataNode root) {
for (int i = 0; i < root.getLength(); i++) {
if (root.item(i).getNodeName().compareToIgnoreCase(name) == 0) {
return (IIOMetadataNode) root.item(i);
}
}
return null;
}

/*
Expand Down Expand Up @@ -376,7 +541,7 @@ public void rotate(int degree, Component comp) {
// this _always_ returns a value between 0 and 360...
// (and yes, it does work properly for negative numbers)
_degrees = MathUtil.wrap(degree, 0, 360);

if (_degrees == 0) {
if (Math.abs(_scale - 1.0) > .00001) {
int w = (int) Math.ceil(_scale * getIconWidth());
Expand Down

0 comments on commit 7a41ae3

Please sign in to comment.