001/**
002 * Copyright (c) 2011, The University of Southampton and the individual contributors.
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification,
006 * are permitted provided that the following conditions are met:
007 *
008 *   *  Redistributions of source code must retain the above copyright notice,
009 *      this list of conditions and the following disclaimer.
010 *
011 *   *  Redistributions in binary form must reproduce the above copyright notice,
012 *      this list of conditions and the following disclaimer in the documentation
013 *      and/or other materials provided with the distribution.
014 *
015 *   *  Neither the name of the University of Southampton nor the names of its
016 *      contributors may be used to endorse or promote products derived from this
017 *      software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030package org.openimaj.demos.sandbox.image.gif;
031
032// 
033//  GifSequenceWriter.java
034//  
035//  Created by Elliot Kroo on 2009-04-25.
036//
037// This work is licensed under the Creative Commons Attribution 3.0 Unported
038// License. To view a copy of this license, visit
039// http://creativecommons.org/licenses/by/3.0/ or send a letter to Creative
040// Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA.
041
042import java.awt.image.BufferedImage;
043import java.awt.image.RenderedImage;
044import java.io.File;
045import java.io.FileNotFoundException;
046import java.io.IOException;
047import java.io.OutputStream;
048import java.util.Iterator;
049import java.util.List;
050
051import javax.imageio.IIOException;
052import javax.imageio.IIOImage;
053import javax.imageio.ImageIO;
054import javax.imageio.ImageTypeSpecifier;
055import javax.imageio.ImageWriteParam;
056import javax.imageio.ImageWriter;
057import javax.imageio.metadata.IIOMetadata;
058import javax.imageio.metadata.IIOMetadataNode;
059import javax.imageio.stream.FileImageOutputStream;
060import javax.imageio.stream.ImageOutputStream;
061import javax.imageio.stream.MemoryCacheImageOutputStream;
062
063import org.openimaj.image.Image;
064import org.openimaj.image.ImageUtilities;
065
066/**
067 * A {@link GifSequenceWriter} uses java {@link ImageIO} to write animated gifs.
068 * Taken wholesale from: http://elliot.kroo.net/software/java/GifSequenceWriter/
069 * and modified to play nicely with openimaj
070 * 
071 * @author Elliot Kroo (elliot[at]kroo[dot]net), Sina Samangooei
072 *         (ss@ecs.soton.ac.uk)
073 * 
074 */
075public class GifSequenceWriter {
076        protected ImageWriter gifWriter;
077        protected ImageWriteParam imageWriteParam;
078        protected IIOMetadata imageMetaData;
079        private boolean delayedInit;
080        private ImageOutputStream outputStream;
081        private int timeBetweenFramesMS;
082        private boolean loopContinuously;
083
084        /**
085         * Creates a new GifSequenceWriter
086         * 
087         * @param outputStream
088         *            the ImageOutputStream to be written to
089         * @param imageType
090         *            one of the imageTypes specified in BufferedImage
091         * @param timeBetweenFramesMS
092         *            the time between frames in miliseconds
093         * @param loopContinuously
094         *            wether the gif should loop repeatedly
095         * @throws IIOException
096         *             if no gif ImageWriters are found
097         * @throws IOException
098         */
099        public GifSequenceWriter(ImageOutputStream outputStream, int imageType,int timeBetweenFramesMS, boolean loopContinuously) throws IIOException, IOException {
100                this.delayedInit = false;
101                init(outputStream,imageType,timeBetweenFramesMS,loopContinuously);
102        }
103        
104        /**
105         * Creates a new GifSequenceWriter. Not specifying the imageType means that initialisation of the writer is delayed until the first image
106         * is written
107         * 
108         * @param outputStream
109         *            the ImageOutputStream to be written to
110         * @param timeBetweenFramesMS
111         *            the time between frames in miliseconds
112         * @param loopContinuously
113         *            whether the gif should loop repeatedly
114         * @throws IIOException
115         *             if no gif ImageWriters are found
116         * @throws IOException
117         */
118        public GifSequenceWriter(ImageOutputStream outputStream, int timeBetweenFramesMS, boolean loopContinuously) throws IIOException, IOException {
119                this.delayedInit = true;
120                this.outputStream = outputStream;
121                this.timeBetweenFramesMS = timeBetweenFramesMS;
122                this.loopContinuously = loopContinuously;
123        }
124
125        private void init(ImageOutputStream outputStream, int imageType,int timeBetweenFramesMS, boolean loopContinuously) throws IOException {
126                // my method to create a writer
127                gifWriter = getWriter();
128                imageWriteParam = gifWriter.getDefaultWriteParam();
129                ImageTypeSpecifier imageTypeSpecifier = ImageTypeSpecifier.createFromBufferedImageType(imageType);
130
131                imageMetaData = gifWriter.getDefaultImageMetadata(imageTypeSpecifier,imageWriteParam);
132
133                String metaFormatName = imageMetaData.getNativeMetadataFormatName();
134
135                IIOMetadataNode root = (IIOMetadataNode) imageMetaData.getAsTree(metaFormatName);
136
137                IIOMetadataNode graphicsControlExtensionNode = getNode(root,"GraphicControlExtension");
138
139                graphicsControlExtensionNode.setAttribute("disposalMethod", "none");
140                graphicsControlExtensionNode.setAttribute("userInputFlag", "FALSE");
141                graphicsControlExtensionNode.setAttribute("transparentColorFlag",
142                                "FALSE");
143                graphicsControlExtensionNode.setAttribute("delayTime",
144                                Integer.toString(timeBetweenFramesMS / 10));
145                graphicsControlExtensionNode.setAttribute("transparentColorIndex", "0");
146
147                IIOMetadataNode commentsNode = getNode(root, "CommentExtensions");
148                commentsNode.setAttribute("CommentExtension", "Created by MAH");
149
150                IIOMetadataNode appEntensionsNode = getNode(root,
151                                "ApplicationExtensions");
152
153                IIOMetadataNode child = new IIOMetadataNode("ApplicationExtension");
154
155                child.setAttribute("applicationID", "NETSCAPE");
156                child.setAttribute("authenticationCode", "2.0");
157
158                int loop = loopContinuously ? 0 : 1;
159
160                child.setUserObject(new byte[] { 0x1, (byte) (loop & 0xFF),
161                                (byte) ((loop >> 8) & 0xFF) });
162                appEntensionsNode.appendChild(child);
163
164                imageMetaData.setFromTree(metaFormatName, root);
165
166                gifWriter.setOutput(outputStream);
167
168                gifWriter.prepareWriteSequence(null);
169                delayedInit = false;
170        }
171
172        /**
173         * Write an image to the sequence
174         * 
175         * @param img
176         * @throws IOException
177         */
178        public void writeToSequence(Image<?, ?> img) throws IOException {
179                BufferedImage bimg = ImageUtilities.createBufferedImage(img);
180                if(delayedInit){
181                        init(outputStream, bimg.getType(), timeBetweenFramesMS, loopContinuously);
182                }
183                writeToSequence(bimg);
184        }
185
186        private void writeToSequence(RenderedImage img) throws IOException {
187                gifWriter.writeToSequence(new IIOImage(img, null, imageMetaData),
188                                imageWriteParam);
189        }
190
191        /**
192         * Close this GifSequenceWriter object. This does not close the underlying
193         * stream, just finishes off the GIF.
194         * 
195         * @throws IOException
196         */
197        public void close() throws IOException {
198                gifWriter.endWriteSequence();
199        }
200
201        /**
202         * Returns the first available GIF ImageWriter using
203         * ImageIO.getImageWritersBySuffix("gif").
204         * 
205         * @return a GIF ImageWriter object
206         * @throws IIOException
207         *             if no GIF image writers are returned
208         */
209        private static ImageWriter getWriter() throws IIOException {
210                Iterator<ImageWriter> iter = ImageIO.getImageWritersBySuffix("gif");
211                if (!iter.hasNext()) {
212                        throw new IIOException("No GIF Image Writers Exist");
213                } else {
214                        return iter.next();
215                }
216        }
217
218        /**
219         * Returns an existing child node, or creates and returns a new child node
220         * (if the requested node does not exist).
221         * 
222         * @param rootNode
223         *            the <tt>IIOMetadataNode</tt> to search for the child node.
224         * @param nodeName
225         *            the name of the child node.
226         * 
227         * @return the child node, if found or a new node created with the given
228         *         name.
229         */
230        private static IIOMetadataNode getNode(IIOMetadataNode rootNode,
231                        String nodeName) {
232                int nNodes = rootNode.getLength();
233                for (int i = 0; i < nNodes; i++) {
234                        if (rootNode.item(i).getNodeName().compareToIgnoreCase(nodeName) == 0) {
235                                return ((IIOMetadataNode) rootNode.item(i));
236                        }
237                }
238                IIOMetadataNode node = new IIOMetadataNode(nodeName);
239                rootNode.appendChild(node);
240                return (node);
241        }
242        
243        /**
244         * Write an array of images to a gif file. The type of the output gif is taken from the first frame.
245         * All future frames are assumed to be the same type.
246         * 
247         * @param frames
248         * @param timeBetweenFramesMS
249         * @param loopContinuously
250         * @param file 
251         * 
252         * @throws IOException 
253         */
254        public static void writeGif(List<Image<?,?>> frames,int timeBetweenFramesMS, boolean loopContinuously,File file) throws IOException{
255                ImageOutputStream output = new FileImageOutputStream(file);
256                try {
257                        writeGif(frames,timeBetweenFramesMS,loopContinuously,output);
258                } finally{
259                        output.close();
260                }
261        }
262        
263        /**
264         * Construct a {@link GifSequenceWriter} which writes to a file
265         * @param timeBetweenFramesMS
266         * @param loopContinuously
267         * @param f
268         * @return {@link GifSequenceWriter} instance 
269         * 
270         * @throws IOException 
271         * @throws FileNotFoundException 
272         * @throws IIOException 
273         */
274        public static GifSequenceWriter createWriter(int timeBetweenFramesMS, boolean loopContinuously, File f) throws IIOException, FileNotFoundException, IOException {
275                return new GifSequenceWriter(new FileImageOutputStream(f),timeBetweenFramesMS,loopContinuously);
276        }
277        
278        /**
279         * Write an array of images to a gif output stream
280         * 
281         * @param frames
282         * @param timeBetweenFramesMS
283         * @param loopContinuously
284         * @param stream
285         * @throws IOException 
286         */
287        public static void writeGif(List<Image<?,?>> frames,int timeBetweenFramesMS, boolean loopContinuously,OutputStream stream) throws IOException{
288                ImageOutputStream output = new MemoryCacheImageOutputStream(stream);
289                writeGif(frames,timeBetweenFramesMS,loopContinuously,output);
290        }
291
292        /**
293         * Write some frames into an {@link ImageOutputStream}
294         * @param frames
295         * @param timeBetweenFramesMS
296         * @param loopContinuously
297         * @param output
298         * @throws IOException
299         */
300        public static void writeGif(List<Image<?, ?>> frames,int timeBetweenFramesMS, boolean loopContinuously,ImageOutputStream output) throws IOException {
301                if(frames.size() == 0){
302                        throw new IOException("No frames!");
303                }
304                GifSequenceWriter writer = new GifSequenceWriter(output,timeBetweenFramesMS, loopContinuously);
305                for (Image<?, ?> image : frames) {
306                        writer.writeToSequence(image);
307                }
308                writer.close();
309        }
310}