Animate gifs in TImage

This commit is contained in:
Autumn Lamonte 2022-01-05 20:11:57 -06:00
parent 2484a4da2d
commit dd557079e8
5 changed files with 603 additions and 16 deletions

View file

@ -30,6 +30,7 @@ package jexer;
import java.awt.image.BufferedImage;
import jexer.bits.Animation;
import jexer.bits.Cell;
import jexer.bits.ImageUtils;
import jexer.event.TCommandEvent;
@ -40,7 +41,7 @@ import static jexer.TCommand.*;
import static jexer.TKeypress.*;
/**
* TImage renders a piece of a bitmap image on screen.
* TImage renders a piece of a bitmap image or an animated image on screen.
*/
public class TImage extends TWidget implements EditMenuUser {
@ -151,6 +152,11 @@ public class TImage extends TWidget implements EditMenuUser {
*/
private int lastTextHeight = -1;
/**
* Animation to display.
*/
private Animation animation;
// ------------------------------------------------------------------------
// Constructors -----------------------------------------------------------
// ------------------------------------------------------------------------
@ -168,8 +174,8 @@ public class TImage extends TWidget implements EditMenuUser {
* @param top top row of the image. 0 is the top-most row.
*/
public TImage(final TWidget parent, final int x, final int y,
final int width, final int height,
final BufferedImage image, final int left, final int top) {
final int width, final int height, final BufferedImage image,
final int left, final int top) {
this(parent, x, y, width, height, image, left, top, null);
}
@ -188,9 +194,8 @@ public class TImage extends TWidget implements EditMenuUser {
* @param clickAction function to call when mouse is pressed
*/
public TImage(final TWidget parent, final int x, final int y,
final int width, final int height,
final BufferedImage image, final int left, final int top,
final TAction clickAction) {
final int width, final int height, final BufferedImage image,
final int left, final int top, final TAction clickAction) {
// Set parent and window
super(parent, x, y, width, height);
@ -204,6 +209,56 @@ public class TImage extends TWidget implements EditMenuUser {
sizeToImage(true);
}
/**
* Public constructor.
*
* @param parent parent widget
* @param x column relative to parent
* @param y row relative to parent
* @param width number of text cells for width of the image
* @param height number of text cells for height of the image
* @param animation the animation to display
* @param left left column of the image. 0 is the left-most column.
* @param top top row of the image. 0 is the top-most row.
*/
public TImage(final TWidget parent, final int x, final int y,
final int width, final int height, final Animation animation,
final int left, final int top) {
this(parent, x, y, width, height, animation, left, top, null);
}
/**
* Public constructor.
*
* @param parent parent widget
* @param x column relative to parent
* @param y row relative to parent
* @param width number of text cells for width of the image
* @param height number of text cells for height of the image
* @param animation the animation to display
* @param left left column of the image. 0 is the left-most column.
* @param top top row of the image. 0 is the top-most row.
* @param clickAction function to call when mouse is pressed
*/
public TImage(final TWidget parent, final int x, final int y,
final int width, final int height, final Animation animation,
final int left, final int top, final TAction clickAction) {
// Set parent and window
super(parent, x, y, width, height);
setCursorVisible(false);
animation.start(getApplication());
this.animation = animation;
this.originalImage = animation.getFrame();
this.left = left;
this.top = top;
this.clickAction = clickAction;
sizeToImage(true);
}
// ------------------------------------------------------------------------
// Event handlers ---------------------------------------------------------
// ------------------------------------------------------------------------
@ -339,6 +394,16 @@ public class TImage extends TWidget implements EditMenuUser {
}
}
/**
* Stop the animation on close.
*/
@Override
public void close() {
if (animation != null) {
animation.stop();
}
}
// ------------------------------------------------------------------------
// TWidget ----------------------------------------------------------------
// ------------------------------------------------------------------------
@ -348,7 +413,18 @@ public class TImage extends TWidget implements EditMenuUser {
*/
@Override
public void draw() {
sizeToImage(false);
if (animation != null) {
BufferedImage newFrame = animation.getFrame();
if (newFrame != originalImage) {
originalImage = newFrame;
image = null;
sizeToImage(true);
} else {
sizeToImage(false);
}
} else {
sizeToImage(false);
}
// We have already broken the image up, just draw the previously
// created set of cells.
@ -545,6 +621,27 @@ public class TImage extends TWidget implements EditMenuUser {
this.originalImage = image;
this.image = null;
sizeToImage(true);
if (animation != null) {
animation.stop();
animation = null;
}
}
/**
* Set the image space to an animation, and reprocess to make the visible
* image.
*
* @param animation the new animation
*/
public void setAnimation(final Animation animation) {
if (this.animation != null) {
this.animation.stop();
this.animation = null;
}
this.animation = animation;
originalImage = animation.getFrame();
image = null;
sizeToImage(true);
}
/**

View file

@ -34,6 +34,8 @@ import java.io.IOException;
import java.util.ResourceBundle;
import javax.imageio.ImageIO;
import jexer.bits.Animation;
import jexer.bits.ImageUtils;
import jexer.event.TKeypressEvent;
import jexer.event.TMouseEvent;
import jexer.event.TResizeEvent;
@ -102,10 +104,18 @@ public class TImageWindow extends TScrollableWindow {
super(parent, file.getName(), x, y, width, height, RESIZABLE);
BufferedImage image = ImageIO.read(file);
BufferedImage image = null;
Animation animation = null;
if (file.getName().toLowerCase().endsWith(".gif")) {
animation = ImageUtils.getAnimation(file);
imageField = addImage(0, 0, getWidth() - 2, getHeight() - 2,
animation, 0, 0);
} else {
image = ImageIO.read(file);
imageField = addImage(0, 0, getWidth() - 2, getHeight() - 2,
image, 0, 0);
}
imageField = addImage(0, 0, getWidth() - 2, getHeight() - 2,
image, 0, 0);
setTitle(file.getName());
setupAfterImage();

View file

@ -34,6 +34,7 @@ import java.util.List;
import java.util.ArrayList;
import jexer.backend.Screen;
import jexer.bits.Animation;
import jexer.bits.Cell;
import jexer.bits.CellAttributes;
import jexer.bits.Clipboard;
@ -2919,7 +2920,6 @@ public abstract class TWidget implements Comparable<TWidget> {
moveAction, singleClickAction);
}
/**
* Convenience function to add an image to this container/window.
*
@ -2932,8 +2932,8 @@ public abstract class TWidget implements Comparable<TWidget> {
* @param top top row of the image. 0 is the top-most row.
*/
public final TImage addImage(final int x, final int y,
final int width, final int height,
final BufferedImage image, final int left, final int top) {
final int width, final int height, final BufferedImage image,
final int left, final int top) {
return new TImage(this, x, y, width, height, image, left, top);
}
@ -2951,14 +2951,51 @@ public abstract class TWidget implements Comparable<TWidget> {
* @param clickAction function to call when mouse is pressed
*/
public final TImage addImage(final int x, final int y,
final int width, final int height,
final BufferedImage image, final int left, final int top,
final TAction clickAction) {
final int width, final int height, final BufferedImage image,
final int left, final int top, final TAction clickAction) {
return new TImage(this, x, y, width, height, image, left, top,
clickAction);
}
/**
* Convenience function to add an image to this container/window.
*
* @param x column relative to parent
* @param y row relative to parent
* @param width number of text cells for width of the image
* @param height number of text cells for height of the image
* @param animation the animation to display
* @param left left column of the image. 0 is the left-most column.
* @param top top row of the image. 0 is the top-most row.
*/
public final TImage addImage(final int x, final int y,
final int width, final int height, final Animation animation,
final int left, final int top) {
return new TImage(this, x, y, width, height, animation, left, top);
}
/**
* Convenience function to add an image to this container/window.
*
* @param x column relative to parent
* @param y row relative to parent
* @param width number of text cells for width of the image
* @param height number of text cells for height of the image
* @param animation the animation to display
* @param left left column of the image. 0 is the left-most column.
* @param top top row of the image. 0 is the top-most row.
* @param clickAction function to call when mouse is pressed
*/
public final TImage addImage(final int x, final int y,
final int width, final int height, final Animation animation,
final int left, final int top, final TAction clickAction) {
return new TImage(this, x, y, width, height, animation, left, top,
clickAction);
}
/**
* Convenience function to add an editable 2D data table to this
* container/window.

View file

@ -0,0 +1,213 @@
/*
* Jexer - Java Text User Interface
*
* The MIT License (MIT)
*
* Copyright (C) 2021 Autumn Lamonte
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
* @author Autumn Lamonte [AutumnWalksTheLake@gmail.com] Trans Liberation Now
* @version 1
*/
package jexer.bits;
import java.awt.image.BufferedImage;
import java.util.List;
import jexer.TAction;
import jexer.TApplication;
import jexer.TTimer;
/**
* An animation that can be displayed in an image.
*/
public class Animation {
/**
* List of images for an animation.
*/
private List<BufferedImage> frames;
/**
* The index of the frame that is currently visible.
*/
private int currentFrame = 0;
/**
* Whether or not the current frame has been returned.
*/
private boolean gotFrame = false;
/**
* Number of millis to wait until next animation frame.
*/
private int frameDelay = 0;
/**
* Number of times to loop the animation. 1 means play it once. 0 means
* to play it forever.
*/
private int frameLoops = 0;
/**
* The number of loops executed so far.
*/
private int loopCount = 0;
/**
* If true, the animation is running.
*/
private boolean running;
/**
* The timer that is incrementing the current frame index.
*/
private TTimer timer;
/**
* Public constructor.
*
* @param frames the frames
* @param frameDelay the number of millis to wait until next animation
* frame
* @param frameLoops the number of times to loop the animation. 0 means
* play it once. -1 means to play it forever.
*/
public Animation(final List<BufferedImage> frames, final int frameDelay,
final int frameLoops) {
assert (frames != null);
assert (frames.size() > 0);
this.frames = frames;
this.frameDelay = frameDelay;
this.frameLoops = frameLoops;
}
/**
* Check if this animation is running.
*
* @return true if the animation is running
*/
public boolean isRunning() {
return running;
}
/**
* Start the animation.
*
* @param application the application
*/
public void start(final TApplication application) {
if (running) {
return;
}
running = true;
assert (application != null);
/*
System.err.printf("start() %d frames loops %d delay %d\n",
frames.size(), frameLoops, frameDelay);
*/
if (frames.size() > 1) {
timer = application.addTimer(frameDelay, true,
new TAction() {
public void DO() {
if (running) {
if (gotFrame) {
currentFrame++;
gotFrame = false;
}
if (currentFrame >= frames.size()) {
currentFrame = 0;
}
loopCount++;
if ((frameLoops > 0) && (loopCount >= frameLoops)) {
if (timer != null) {
timer.setRecurring(false);
}
}
} else {
if (timer != null) {
timer.setRecurring(false);
}
}
application.doRepaint();
}
});
}
}
/**
* Stop the animation.
*/
public void stop() {
running = false;
}
/**
* Reset the animation.
*/
public void reset() {
loopCount = 0;
}
/**
* Get the number of frames in this animation.
*
* @return the number of frames
*/
public int count() {
return frames.size();
}
/**
* Get the number of the current frame in view.
*
* @return the frame number
*/
public int currentFrameNumber() {
return currentFrame;
}
/**
* Get a frame by number.
*
* @param frameNumber the frame number
* @return the frame
*/
public BufferedImage getFrame(final int frameNumber) {
gotFrame = true;
return frames.get(frameNumber);
}
/**
* Get the frame currently in view.
*
* @return the frame
*/
public BufferedImage getFrame() {
gotFrame = true;
return frames.get(currentFrame);
}
}

View file

@ -28,7 +28,22 @@
*/
package jexer.bits;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.stream.ImageInputStream;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* ImageUtils contains methods to:
@ -36,6 +51,8 @@ import java.awt.image.BufferedImage;
* - Check if an image is fully transparent.
*
* - Scale an image and preserve aspect ratio.
*
* - Open an animated image as an Animation.
*/
public class ImageUtils {
@ -178,4 +195,217 @@ public class ImageUtils {
return newImage;
}
/**
* Open an image as an Animation.
*
* @param filename the name of the file that contains an animation
* @return the animation, or null on error
*/
public static Animation getAnimation(final String filename) {
return getAnimation(new File(filename));
}
/**
* Open an image as an Animation.
*
* @param file the file that contains an animation
* @return the animation, or null on error
*/
public static Animation getAnimation(final File file) {
try {
List<BufferedImage> frames = new LinkedList<BufferedImage>();
List<String> disposals = new LinkedList<String>();
int delays = 0;
/*
* Assume infinite loop. Finite-count looping in GIFs is an
* Application Extension made popular by Netscape 2.0: see
* http://giflib.sourceforge.net/whatsinagif/bits_and_bytes.html
* .
*
* Unfortunately the Sun GIF decoder did not read and expose
* this.
*/
int loopCount = 0;
ImageReader reader = null;
ImageInputStream stream;
stream = ImageIO.createImageInputStream(new FileInputStream(file));
Iterator<ImageReader> iter = ImageIO.getImageReaders(stream);
while (iter.hasNext()) {
reader = iter.next();
break;
}
if (reader == null) {
return null;
}
reader.setInput(stream);
int width = -1;
int height = -1;
java.awt.Color backgroundColor = null;
IIOMetadata metadata = reader.getStreamMetadata();
if (metadata != null) {
IIOMetadataNode gblRoot;
gblRoot = (IIOMetadataNode) metadata.getAsTree(metadata.
getNativeMetadataFormatName());
NodeList gblScreenDesc;
gblScreenDesc = gblRoot.getElementsByTagName(
"LogicalScreenDescriptor");
if ((gblScreenDesc != null)
&& (gblScreenDesc.getLength() > 0)
) {
IIOMetadataNode screenDescriptor;
screenDescriptor = (IIOMetadataNode) gblScreenDesc.item(0);
if (screenDescriptor != null) {
width = Integer.parseInt(screenDescriptor.
getAttribute("logicalScreenWidth"));
height = Integer.parseInt(screenDescriptor.
getAttribute("logicalScreenHeight"));
}
}
NodeList gblColorTable = gblRoot.getElementsByTagName(
"GlobalColorTable");
if ((gblColorTable != null)
&& (gblColorTable.getLength() > 0)
) {
IIOMetadataNode colorTable = (IIOMetadataNode) gblColorTable.item(0);
if (colorTable != null) {
String bgIndex = colorTable.getAttribute(
"backgroundColorIndex");
IIOMetadataNode color;
color = (IIOMetadataNode) colorTable.getFirstChild();
while (color != null) {
if (color.getAttribute("index").equals(bgIndex)) {
int red = Integer.parseInt(
color.getAttribute("red"));
int green = Integer.parseInt(
color.getAttribute("green"));
int blue = Integer.parseInt(
color.getAttribute("blue"));
backgroundColor = new java.awt.Color(red,
green, blue);
break;
}
color = (IIOMetadataNode) color.getNextSibling();
}
}
}
}
BufferedImage master = null;
Graphics2D masterGraphics = null;
int lastx = 0;
int lasty = 0;
boolean hasBackround = false;
for (int frameIndex = 0; ; frameIndex++) {
BufferedImage image;
try {
image = reader.read(frameIndex);
} catch (IndexOutOfBoundsException io) {
break;
}
assert (image != null);
if (width == -1 || height == -1) {
width = image.getWidth();
height = image.getHeight();
}
IIOMetadataNode root;
root = (IIOMetadataNode) reader.getImageMetadata(frameIndex).
getAsTree("javax_imageio_gif_image_1.0");
IIOMetadataNode gce;
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 java.awt.Color(0, 0, 0, 0));
if ((image.getWidth() == width)
&& (image.getHeight() == height)
) {
hasBackround = true;
}
} 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);
lastx = x;
lasty = y;
BufferedImage copy = new BufferedImage(master.getColorModel(),
master.copyData(null), master.isAlphaPremultiplied(), null);
frames.add(copy);
disposals.add(disposal);
delays += delay;
if (disposal.equals("restoreToPrevious")) {
BufferedImage from = null;
for (int i = frameIndex - 1; i >= 0; i--) {
if (!disposals.get(i).equals("restoreToPrevious")
|| (frameIndex == 0)
) {
from = frames.get(i);
break;
}
}
master = new BufferedImage(from.getColorModel(),
from.copyData(null), from.isAlphaPremultiplied(), null);
masterGraphics = master.createGraphics();
masterGraphics.setBackground(new java.awt.Color(0, 0, 0, 0));
} else if (disposal.equals("restoreToBackgroundColor")
&& (backgroundColor != null)) {
if (!hasBackround || (frameIndex > 1)) {
master.createGraphics().fillRect(lastx, lasty,
frames.get(frameIndex - 1).getWidth(),
frames.get(frameIndex - 1).getHeight());
}
}
}
reader.dispose();
if (frames.size() == 1) {
loopCount = 1;
}
if (frames.size() == 0) {
return null;
}
Animation animation = new Animation(frames,
(delays * 10 / frames.size()), loopCount);
return animation;
} catch (IOException e) {
// SQUASH
return null;
}
}
}