<div dir="auto"><div>Hey Jeremy,</div><div dir="auto"><br></div><div dir="auto">Yeah, that's not even close to the performance that Java is capable of.</div><div dir="auto"><br></div><div dir="auto">If you want to see something that is used in Production using only Java code, look at iCafe. The code is all open source, and I know it can do what you are trying to do because I did it too. It'll give you an idea of what is going wrong here.</div><div dir="auto"><br><div class="gmail_quote gmail_quote_container" dir="auto"><div dir="ltr" class="gmail_attr">On Fri, Feb 14, 2025, 5:04 PM Jeremy Wood <<a href="mailto:mickleness@gmail.com">mickleness@gmail.com</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex">
<div style="line-break:after-white-space">
<div></div><div>I have a generic question for the group. I’m trying to implement a method resembling:</div><div><br></div><div><div id="m_5975792960531701509x3a7cf97c41bc4858b167a4bb585d98cf"><span style="color:rgb(8,8,8);font-family:monospace;font-size:13.066667px;margin:13.066667px 0px">public </span><span style="color:rgb(8,8,8);font-family:monospace;font-size:13.066667px;margin:13.066667px 0px">BufferedImage seek(File gifFile, </span><span style="color:rgb(8,8,8);font-family:monospace;font-size:13.066667px;margin:13.066667px 0px">int </span><span style="color:rgb(8,8,8);font-family:monospace;font-size:13.066667px;margin:13.066667px 0px">millis) </span></div></div><div><br></div><div>Ideally I’d like to:</div><div>1. Not add a 3rd party jar to our class path</div><div>2. Not write a new file</div><div>3. Not load the entire gif file into memory as a byte array</div><div>4. Use well-tested/stable code to handle gif frame disposal, custom color palettes, and any other obscure gif parsing challenges.</div><div><br></div><div>ImageIO doesn’t really work without a lot of intervention. It can return the individual frames of a GIF, but it becomes the caller’s responsibility to handle frame disposal/placement.</div><div><br></div><div>So I tried working with ToolkitImages. What I wrote (see below) functionally works, but it’s not an acceptable solution because it’s too slow. The problem now is sun.awt.image.GifImageDecoder calls Thread.sleep(delay). So it acts more like a player than a parser. If my gif file contains 10 one-second frames, then this code takes at least 9 seconds.</div><div><br></div><div>This feels way too hard for such a simple ask. Is there a solution / toolset I’m missing? All the code I need already exists in the desktop module, but I seem unable to leverage it.</div><div><br></div><div>Regards,</div><div> - Jeremy</div><div><br></div><div>———</div><div><div style="color:rgb(8,8,8);font-family:"JetBrains Mono",monospace;font-size:9.8pt"><pre><span style="color:#0033b3">import </span>javax.imageio.ImageIO;<br><span style="color:#0033b3">import </span>javax.imageio.ImageReader;<br><span style="color:#0033b3">import </span>javax.imageio.metadata.IIOMetadataNode;<br><span style="color:#0033b3">import </span>java.awt.*;<br><span style="color:#0033b3">import </span>java.awt.image.*;<br><span style="color:#0033b3">import </span>java.io.*;<br><span style="color:#0033b3">import </span>java.util.ArrayList;<br><span style="color:#0033b3">import </span>java.util.Hashtable;<br><span style="color:#0033b3">import </span>java.util.Objects;<br><span style="color:#0033b3">import </span>java.util.List;<br><span style="color:#0033b3">import </span>java.util.concurrent.Semaphore;<br><span style="color:#0033b3">import </span>java.util.concurrent.atomic.AtomicInteger;<br><span style="color:#0033b3">import </span>java.util.concurrent.atomic.AtomicReference;<br><br><span style="color:#8c8c8c;font-style:italic">/**<br></span><span style="color:#8c8c8c;font-style:italic"> * This uses a combination of java.awt.Image classes and ImageIO classes<br></span><span style="color:#8c8c8c;font-style:italic"> * to convert a GIF image to a series of frames.<br></span><span style="color:#8c8c8c;font-style:italic"> */<br></span><span style="color:#0033b3">public class </span>GifReader {<br><br> <span style="color:#0033b3">public enum </span>FrameConsumerResult {<br> CONTINUE, STOP, SKIP<br> }<br><br> <span style="color:#0033b3">public final static class </span>Info {<br> <span style="color:#0033b3">public final int </span>width, height, numberOfFrames, duration;<br> <span style="color:#0033b3">public final boolean </span>isLooping;<br><br> <span style="color:#0033b3">public </span>Info(<span style="color:#0033b3">int </span>width, <span style="color:#0033b3">int </span>height, <span style="color:#0033b3">int </span>numberOfFrames, <span style="color:#0033b3">int </span>duration, <span style="color:#0033b3">boolean </span>isLooping) {<br> <span style="color:#0033b3">this</span>.width = width;<br> <span style="color:#0033b3">this</span>.height = height;<br> <span style="color:#0033b3">this</span>.numberOfFrames = numberOfFrames;<br> <span style="color:#0033b3">this</span>.duration = duration;<br> <span style="color:#0033b3">this</span>.isLooping = isLooping;<br> }<br> }<br><br> <span style="color:#8c8c8c;font-style:italic">/**<br></span><span style="color:#8c8c8c;font-style:italic"> * Consumes information about the frames of a GIF animation.<br></span><span style="color:#8c8c8c;font-style:italic"> */<br></span><span style="color:#8c8c8c;font-style:italic"> </span><span style="color:#0033b3">public interface </span>GifFrameConsumer {<br> <span style="color:#8c8c8c;font-style:italic">/**<br></span><span style="color:#8c8c8c;font-style:italic"> * This provides meta information about a GIF image.<br></span><span style="color:#8c8c8c;font-style:italic"> *<br></span><span style="color:#8c8c8c;font-style:italic"> * @return true if the reader should start supplying frame data, or false if this meta information<br></span><span style="color:#8c8c8c;font-style:italic"> * is all the consumer wanted.<br></span><span style="color:#8c8c8c;font-style:italic"> */<br></span><span style="color:#8c8c8c;font-style:italic"> </span><span style="color:#0033b3">boolean </span>startImage(<span style="color:#0033b3">int </span>imageWidth, <span style="color:#0033b3">int </span>imageHeight, <span style="color:#0033b3">int </span>numberOfFrames, <span style="color:#0033b3">int </span>durationMillis, <span style="color:#0033b3">boolean </span>isLooping);<br><br> <span style="color:#8c8c8c;font-style:italic">/**<br></span><span style="color:#8c8c8c;font-style:italic"> * @param frameIndex the frame index (starting at 0)<br></span><span style="color:#8c8c8c;font-style:italic"> * @param numberOfFrames the total number of frames in the GIF image.<br></span><span style="color:#8c8c8c;font-style:italic"> * @param startTimeMillis the start time of this frame (relative to the start time of the animation)<br></span><span style="color:#8c8c8c;font-style:italic"> * @param durationMillis the duration of this frame<br></span><span style="color:#8c8c8c;font-style:italic"> * @return if this returns CONTINUE then this consumer expects {@link #consumeFrame(BufferedImage, int, int, int, int, boolean)}<br></span><span style="color:#8c8c8c;font-style:italic"> * to be called next. If this returns STOP then this consumer expects to stop all reading. If this returns SKIP<br></span><span style="color:#8c8c8c;font-style:italic"> * then this consumer is not interested in this frame, but it expects to be asked about `frameIndex + 1`.<br></span><span style="color:#8c8c8c;font-style:italic"> */<br></span><span style="color:#8c8c8c;font-style:italic"> </span><span style="color:#0033b3">default </span>FrameConsumerResult startFrame(<span style="color:#0033b3">int </span>frameIndex, <span style="color:#0033b3">int </span>numberOfFrames, <span style="color:#0033b3">int </span>startTimeMillis, <span style="color:#0033b3">int </span>durationMillis) {<br> <span style="color:#0033b3">return </span>FrameConsumerResult.CONTINUE;<br> }<br><br> <span style="color:#8c8c8c;font-style:italic">/**<br></span><span style="color:#8c8c8c;font-style:italic"> * Consume a new frame from a GIF image.<br></span><span style="color:#8c8c8c;font-style:italic"> *<br></span><span style="color:#8c8c8c;font-style:italic"> * @param frame an INT_ARGB image. This BufferedImage reference will be reused with each<br></span><span style="color:#8c8c8c;font-style:italic"> * call to this method, so if you want to keep these images in memory you<br></span><span style="color:#8c8c8c;font-style:italic"> * need to clone this image.<br></span><span style="color:#8c8c8c;font-style:italic"> * @param startTimeMillis the start time of this frame (relative to the start time of the animation)<br></span><span style="color:#8c8c8c;font-style:italic"> * @param frameIndex the current frame index (starting at 0).<br></span><span style="color:#8c8c8c;font-style:italic"> * @param numberOfFrames the total number of frames in the GIF image.<br></span><span style="color:#8c8c8c;font-style:italic"> * @param frameDurationMillis the duration of this frame in milliseconds<br></span><span style="color:#8c8c8c;font-style:italic"> * @param isDone if true then this method will not be called again.<br></span><span style="color:#8c8c8c;font-style:italic"> * @return true if the reader should continue reading additional frames, or false if the reader should<br></span><span style="color:#8c8c8c;font-style:italic"> * immediately stop. This return value is ignored if `isDone` is true.<br></span><span style="color:#8c8c8c;font-style:italic"> */<br></span><span style="color:#8c8c8c;font-style:italic"> </span><span style="color:#0033b3">boolean </span>consumeFrame(BufferedImage frame, <span style="color:#0033b3">int </span>startTimeMillis, <span style="color:#0033b3">int </span>frameDurationMillis, <span style="color:#0033b3">int </span>frameIndex, <span style="color:#0033b3">int </span>numberOfFrames, <span style="color:#0033b3">boolean </span>isDone);<br> }<br><br> <span style="color:#8c8c8c;font-style:italic">/**<br></span><span style="color:#8c8c8c;font-style:italic"> * Read a GIF image.<br></span><span style="color:#8c8c8c;font-style:italic"> *<br></span><span style="color:#8c8c8c;font-style:italic"> * @param gifFile the GIF image file to read.<br></span><span style="color:#8c8c8c;font-style:italic"> * @param waitUntilFinished if true then this method will not return until the GifFrameConsumer<br></span><span style="color:#8c8c8c;font-style:italic"> * has received every frame.<br></span><span style="color:#8c8c8c;font-style:italic"> * @param frameConsumer the consumer that will consume the image data.<br></span><span style="color:#8c8c8c;font-style:italic"> */<br></span><span style="color:#8c8c8c;font-style:italic"> </span><span style="color:#0033b3">public void </span>read(<span style="color:#0033b3">final </span>File gifFile, <span style="color:#0033b3">final boolean </span>waitUntilFinished, <span style="color:#0033b3">final </span>GifFrameConsumer frameConsumer) <span style="color:#0033b3">throws </span>IOException {<br> Objects.requireNonNull(frameConsumer);<br> <span style="color:#0033b3">final </span>Semaphore semaphore = <span style="color:#0033b3">new </span>Semaphore(<span style="color:#1750eb">1</span>);<br> semaphore.acquireUninterruptibly();<br><br> <span style="color:#0033b3">try </span>(FileInputStream gifFileIn = <span style="color:#0033b3">new </span>FileInputStream(gifFile)) {<br> List<Integer> frameDurationsMillis = readFrameDurationMillis(gifFileIn);<br> Image image = Toolkit.getDefaultToolkit().createImage(gifFile.getPath());<br> ImageConsumer consumer = <span style="color:#0033b3">new </span>ImageConsumer() {<br> <span style="color:#0033b3">private </span>BufferedImage bi;<br> <span style="color:#0033b3">private boolean </span>isActive = <span style="color:#0033b3">true</span>;<br> <span style="color:#0033b3">private int </span>frameCtr, imageWidth, imageHeight, currentFrameStartTime;<br> <span style="color:#0033b3">private boolean </span>ignoreCurrentFrame;<br><br> @Override<br> <span style="color:#0033b3">public void </span>setDimensions(<span style="color:#0033b3">int </span>width, <span style="color:#0033b3">int </span>height) {<br> imageWidth = width;<br> imageHeight = height;<br><br> <span style="color:#8c8c8c;font-style:italic">// if this gif loops:<br></span><span style="color:#8c8c8c;font-style:italic"> // the sun.awt.image.GifImageDecoder calls ImageFetcher.startingAnimation, which<br></span><span style="color:#8c8c8c;font-style:italic"> // changes the name of this thread. We don't know how many times it's supposed<br></span><span style="color:#8c8c8c;font-style:italic"> // to loop, but in my experience gifs either don't loop at all or they loop forever;<br></span><span style="color:#8c8c8c;font-style:italic"> // they aren't asked to loop N-many times anymore<br></span><span style="color:#8c8c8c;font-style:italic"> </span><span style="color:#0033b3">boolean </span>isLooping = Thread.currentThread().getName().contains(<span style="color:#067d17">"Image Animator"</span>);<br><br> <span style="color:#0033b3">try </span>{<br> <span style="color:#0033b3">int </span>totalDuration = <span style="color:#1750eb">0</span>;<br> <span style="color:#0033b3">for </span>(<span style="color:#0033b3">int </span>frameDuration : frameDurationsMillis)<br> totalDuration += frameDuration;<br><br> <span style="color:#0033b3">if </span>(!frameConsumer.startImage(width, height, frameDurationsMillis.size(), totalDuration, isLooping))<br> stop(<span style="color:#0033b3">null</span>);<br> } <span style="color:#0033b3">catch</span>(Exception e) {<br> stop(e);<br> }<br> }<br><br> @Override<br> <span style="color:#0033b3">public void </span>setProperties(Hashtable<?, ?> props) {}<br><br> @Override<br> <span style="color:#0033b3">public void </span>setColorModel(ColorModel model) {}<br><br> @Override<br> <span style="color:#0033b3">public void </span>setHints(<span style="color:#0033b3">int </span>hintflags) {<br> <span style="color:#8c8c8c;font-style:italic">// this is called before every frame starts:<br></span><span style="color:#8c8c8c;font-style:italic"> </span><span style="color:#0033b3">int </span>frameDuration = frameDurationsMillis.get(frameCtr);<br> FrameConsumerResult r = frameConsumer.startFrame(frameCtr, frameDurationsMillis.size(), currentFrameStartTime, frameDuration);<br> <span style="color:#0033b3">if </span>(r == FrameConsumerResult.STOP) {<br> stop(<span style="color:#0033b3">null</span>);<br> } <span style="color:#0033b3">else </span>{<br> ignoreCurrentFrame = r == FrameConsumerResult.SKIP;<br> }<br> }<br><br> <span style="color:#0033b3">private int</span>[] argbRow;<br> <span style="color:#0033b3">private int</span>[] colorModelRGBs;<br> <span style="color:#0033b3">private </span>IndexColorModel lastModel;<br><br> @Override<br> <span style="color:#0033b3">public void </span>setPixels(<span style="color:#0033b3">final int </span>x, <span style="color:#0033b3">final int </span>y, <span style="color:#0033b3">final int </span>w, <span style="color:#0033b3">final int </span>h,<br> <span style="color:#0033b3">final </span>ColorModel model, <span style="color:#0033b3">final byte</span>[] pixels, <span style="color:#0033b3">final int </span>off, <span style="color:#0033b3">final int </span>scansize) {<br> <span style="color:#8c8c8c;font-style:italic">// Even if ignoreCurrentFrame is true we still need to update the image every iteration.<br></span><span style="color:#8c8c8c;font-style:italic"> // (This is because each frame in a GIF has a "disposal method", and some of them rely<br></span><span style="color:#8c8c8c;font-style:italic"> // on the previous frame.) In theory we *may* be able to skip this method *sometimes*<br></span><span style="color:#8c8c8c;font-style:italic"> // depending on the disposal methods in use, but that would take some more research.<br></span><span style="color:#8c8c8c;font-style:italic"><br></span><span style="color:#8c8c8c;font-style:italic"> </span><span style="color:#0033b3">try </span>{<br> <span style="color:#8c8c8c;font-style:italic">// ImageConsumer javadoc says:<br></span><span style="color:#8c8c8c;font-style:italic"> // Pixel (m,n) is stored in the pixels array at index (n * scansize + m + off)<br></span><span style="color:#8c8c8c;font-style:italic"><br></span><span style="color:#8c8c8c;font-style:italic"> </span><span style="color:#0033b3">final int </span>yMax = y + h;<br> <span style="color:#0033b3">final int </span>xMax = x + w;<br><br> <span style="color:#0033b3">if </span>(model <span style="color:#0033b3">instanceof </span>IndexColorModel icm) {<br> <span style="color:#0033b3">if </span>(icm != lastModel) {<br> colorModelRGBs = <span style="color:#0033b3">new int</span>[icm.getMapSize()];<br> icm.getRGBs(colorModelRGBs);<br> }<br> lastModel = icm;<br> } <span style="color:#0033b3">else </span>{<br> colorModelRGBs = <span style="color:#0033b3">null</span>;<br> }<br><br> <span style="color:#0033b3">if </span>(bi == <span style="color:#0033b3">null</span>) {<br> bi = <span style="color:#0033b3">new </span>BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB);<br> argbRow = <span style="color:#0033b3">new int</span>[imageWidth];<br> }<br><br> <span style="color:#0033b3">for </span>(<span style="color:#0033b3">int </span>y_ = y; y_ < yMax; y_++) {<br> <span style="color:#8c8c8c;font-style:italic">// we're not told to use (off-x), but empirically this is what we get/need:<br></span><span style="color:#8c8c8c;font-style:italic"> </span><span style="color:#0033b3">int </span>i = y_ * scansize + x + (off - x);<br> <span style="color:#0033b3">for </span>(<span style="color:#0033b3">int </span>x_ = x; x_ < xMax; x_++, i++) {<br> <span style="color:#0033b3">int </span>pixel = pixels[i] & <span style="color:#1750eb">0xff</span>;<br> <span style="color:#0033b3">if </span>(colorModelRGBs != <span style="color:#0033b3">null</span>) {<br> argbRow[x_ - x] = colorModelRGBs[pixel];<br> } <span style="color:#0033b3">else </span>{<br> <span style="color:#8c8c8c;font-style:italic">// I don't think we ever resort to this:<br></span><span style="color:#8c8c8c;font-style:italic"> </span>argbRow[x_ - x] = <span style="color:#1750eb">0xff000000 </span>+ (model.getRed(pixel) << <span style="color:#1750eb">16</span>) + (model.getGreen(pixel) << <span style="color:#1750eb">8</span>) + (model.getBlue(pixel));<br> }<br> }<br> bi.getRaster().setDataElements(x, y_, w, <span style="color:#1750eb">1</span>, argbRow);<br> }<br> } <span style="color:#0033b3">catch</span>(RuntimeException e) {<br> <span style="color:#8c8c8c;font-style:italic">// we don't expect this to happen, but if something goes wrong nobody else<br></span><span style="color:#8c8c8c;font-style:italic"> // will print our stacktrace for us:<br></span><span style="color:#8c8c8c;font-style:italic"> </span>stop(e);<br> <span style="color:#0033b3">throw </span>e;<br> }<br> }<br><br> @Override<br> <span style="color:#0033b3">public void </span>setPixels(<span style="color:#0033b3">int </span>x, <span style="color:#0033b3">int </span>y, <span style="color:#0033b3">int </span>w, <span style="color:#0033b3">int </span>h, ColorModel model, <span style="color:#0033b3">int</span>[] pixels, <span style="color:#0033b3">int </span>off, <span style="color:#0033b3">int </span>scansize) {<br> <span style="color:#8c8c8c;font-style:italic">// we never expect this for a GIF image<br></span><span style="color:#8c8c8c;font-style:italic"> </span><span style="color:#0033b3">throw new </span>UnsupportedOperationException();<br> }<br><br> @Override<br> <span style="color:#0033b3">public void </span>imageComplete(<span style="color:#0033b3">int </span>status) {<br> <span style="color:#0033b3">try </span>{<br> <span style="color:#0033b3">int </span>numberOfFrames = frameDurationsMillis.size();<br> <span style="color:#0033b3">int </span>frameDuration = frameDurationsMillis.get(frameCtr);<br> <span style="color:#0033b3">boolean </span>consumeResult;<br> <span style="color:#0033b3">if </span>(ignoreCurrentFrame) {<br> consumeResult = <span style="color:#0033b3">true</span>;<br> } <span style="color:#0033b3">else </span>{<br> consumeResult = frameConsumer.consumeFrame(bi, currentFrameStartTime, frameDuration,<br> frameCtr, numberOfFrames, frameCtr + <span style="color:#1750eb">1 </span>>= numberOfFrames);<br> }<br> frameCtr++;<br> currentFrameStartTime += frameDuration;<br><br> <span style="color:#8c8c8c;font-style:italic">// if we don't remove this ImageConsumer the animating thread will loop forever<br></span><span style="color:#8c8c8c;font-style:italic"> </span><span style="color:#0033b3">if </span>(frameCtr == numberOfFrames || !consumeResult)<br> stop(<span style="color:#0033b3">null</span>);<br> } <span style="color:#0033b3">catch</span>(Exception e) {<br> stop(e);<br> }<br> }<br><br> <span style="color:#0033b3">private void </span>stop(Exception e) {<br> <span style="color:#0033b3">synchronized </span>(<span style="color:#0033b3">this</span>) {<br> <span style="color:#0033b3">if </span>(!isActive)<br> <span style="color:#0033b3">return</span>;<br> isActive = <span style="color:#0033b3">false</span>;<br> }<br> <span style="color:#0033b3">if </span>(e != <span style="color:#0033b3">null</span>)<br> e.printStackTrace();<br> image.getSource().removeConsumer(<span style="color:#0033b3">this</span>);<br> image.flush();<br> <span style="color:#0033b3">if </span>(bi != <span style="color:#0033b3">null</span>)<br> bi.flush();<br> semaphore.release();<br><br> }<br> };<br> image.getSource().startProduction(consumer);<br> }<br> <span style="color:#0033b3">if </span>(waitUntilFinished)<br> semaphore.acquireUninterruptibly();<br> }<br><br> <span style="color:#8c8c8c;font-style:italic">/**<br></span><span style="color:#8c8c8c;font-style:italic"> * Return the frame at a given time in an animation.<br></span><span style="color:#8c8c8c;font-style:italic"> */<br></span><span style="color:#8c8c8c;font-style:italic"> </span><span style="color:#0033b3">public </span>BufferedImage seek(File gifFile, <span style="color:#0033b3">int </span>millis) <span style="color:#0033b3">throws </span>IOException {<br> AtomicInteger seekTime = <span style="color:#0033b3">new </span>AtomicInteger();<br> AtomicReference<BufferedImage> returnValue = <span style="color:#0033b3">new </span>AtomicReference<>();<br> read(gifFile, <span style="color:#0033b3">true</span>, <span style="color:#0033b3">new </span>GifFrameConsumer() {<br> @Override<br> <span style="color:#0033b3">public boolean </span>startImage(<span style="color:#0033b3">int </span>imageWidth, <span style="color:#0033b3">int </span>imageHeight, <span style="color:#0033b3">int </span>numberOfFrames, <span style="color:#0033b3">int </span>durationMillis, <span style="color:#0033b3">boolean </span>isLooping) {<br> seekTime.set(millis%durationMillis);<br> <span style="color:#0033b3">return true</span>;<br> }<br><br> @Override<br> <span style="color:#0033b3">public </span>FrameConsumerResult startFrame(<span style="color:#0033b3">int </span>frameIndex, <span style="color:#0033b3">int </span>numberOfFrames, <span style="color:#0033b3">int </span>startTimeMillis, <span style="color:#0033b3">int </span>durationMillis) {<br> <span style="color:#0033b3">if </span>(numberOfFrames == <span style="color:#1750eb">1 </span>||<br> (startTimeMillis <= seekTime.get() && seekTime.get() < startTimeMillis + durationMillis))<br> <span style="color:#0033b3">return </span>FrameConsumerResult.CONTINUE;<br><br> <span style="color:#0033b3">if </span>(startTimeMillis + durationMillis <= seekTime.get())<br> <span style="color:#0033b3">return </span>FrameConsumerResult.SKIP;<br> <span style="color:#0033b3">return </span>FrameConsumerResult.STOP;<br> }<br><br> @Override<br> <span style="color:#0033b3">public boolean </span>consumeFrame(BufferedImage frame, <span style="color:#0033b3">int </span>startTimeMillis, <span style="color:#0033b3">int </span>frameDurationMillis, <span style="color:#0033b3">int </span>frameIndex, <span style="color:#0033b3">int </span>numberOfFrames, <span style="color:#0033b3">boolean </span>isDone) {<br> returnValue.set(frame);<br> <span style="color:#0033b3">return false</span>;<br> }<br> });<br> <span style="color:#0033b3">return </span>returnValue.get();<br> }<br><br> <span style="color:#8c8c8c;font-style:italic">/**<br></span><span style="color:#8c8c8c;font-style:italic"> * Return basic information about a GIF image/animation.<br></span><span style="color:#8c8c8c;font-style:italic"> */<br></span><span style="color:#8c8c8c;font-style:italic"> </span><span style="color:#0033b3">public </span>Info getInfo(File gifFile) <span style="color:#0033b3">throws </span>IOException {<br> AtomicReference<Info> returnValue = <span style="color:#0033b3">new </span>AtomicReference<>();<br> read(gifFile, <span style="color:#0033b3">true</span>, <span style="color:#0033b3">new </span>GifFrameConsumer() {<br> @Override<br> <span style="color:#0033b3">public boolean </span>startImage(<span style="color:#0033b3">int </span>imageWidth, <span style="color:#0033b3">int </span>imageHeight, <span style="color:#0033b3">int </span>numberOfFrames, <span style="color:#0033b3">int </span>durationMillis, <span style="color:#0033b3">boolean </span>isLooping) {<br> returnValue.set(<span style="color:#0033b3">new </span>Info(imageWidth, imageHeight, numberOfFrames, durationMillis, isLooping));<br> <span style="color:#0033b3">return false</span>;<br> }<br><br> @Override<br> <span style="color:#0033b3">public boolean </span>consumeFrame(BufferedImage frame, <span style="color:#0033b3">int </span>startTimeMillis, <span style="color:#0033b3">int </span>frameDurationMillis, <span style="color:#0033b3">int </span>frameIndex, <span style="color:#0033b3">int </span>numberOfFrames, <span style="color:#0033b3">boolean </span>isDone) {<br> <span style="color:#0033b3">return false</span>;<br> }<br> });<br> <span style="color:#0033b3">return </span>returnValue.get();<br> }<br><br> <span style="color:#8c8c8c;font-style:italic">/**<br></span><span style="color:#8c8c8c;font-style:italic"> * Read the frame durations of a gif. This relies on ImageIO classes.<br></span><span style="color:#8c8c8c;font-style:italic"> */<br></span><span style="color:#8c8c8c;font-style:italic"> </span><span style="color:#0033b3">private </span>List<Integer> readFrameDurationMillis(InputStream stream) <span style="color:#0033b3">throws </span>IOException {<br> ImageReader reader = ImageIO.getImageReadersByFormatName(<span style="color:#067d17">"gif"</span>).next();<br> <span style="color:#0033b3">try </span>{<br> reader.setInput(ImageIO.createImageInputStream(stream));<br> <span style="color:#0033b3">int </span>frameCount = reader.getNumImages(<span style="color:#0033b3">true</span>);<br> List<Integer> frameDurations = <span style="color:#0033b3">new </span>ArrayList<>(frameCount);<br> <span style="color:#0033b3">for </span>(<span style="color:#0033b3">int </span>frameIndex = <span style="color:#1750eb">0</span>; frameIndex < frameCount; frameIndex++) {<br> IIOMetadataNode root = (IIOMetadataNode) reader.getImageMetadata(frameIndex).getAsTree(<span style="color:#067d17">"javax_imageio_gif_image_1.0"</span>);<br> IIOMetadataNode gce = (IIOMetadataNode) root.getElementsByTagName(<span style="color:#067d17">"GraphicControlExtension"</span>).item(<span style="color:#1750eb">0</span>);<br> <span style="color:#0033b3">int </span>delay = Integer.parseInt(gce.getAttribute(<span style="color:#067d17">"delayTime"</span>));<br> frameDurations.add( delay * <span style="color:#1750eb">10 </span>);<br> }<br> <span style="color:#0033b3">return </span>frameDurations;<br> } <span style="color:#0033b3">finally </span>{<br> reader.dispose();<br> }<br> }<br>}<br></pre></div></div></div></blockquote></div></div></div>