diff --git a/src/clojure/clj_image2ascii/core.clj b/src/clojure/clj_image2ascii/core.clj index 5353ade..20d82fc 100644 --- a/src/clojure/clj_image2ascii/core.clj +++ b/src/clojure/clj_image2ascii/core.clj @@ -90,14 +90,6 @@ 20 ms))) -(defn- get-ascii-gif-frames [^ImageInputStream image-stream scale-to-width color?] - (->> (AnimatedGif/read image-stream) - (mapv - (fn [^ImageFrame frame] - (-> (.image frame) - (convert-image scale-to-width color?) - (assoc :delay (fix-gif-frame-delay (.delay frame)))))))) - (defn convert-animated-gif-frames "converts an ImageInputStream created from an animated GIF to a series of ASCII frames representing each frame of animation in the source GIF. scale-to-width is @@ -123,10 +115,23 @@ ([^ImageInputStream image-stream color?] (convert-animated-gif-frames image-stream nil color?)) ([^ImageInputStream image-stream scale-to-width color?] - (let [frames (get-ascii-gif-frames image-stream scale-to-width color?) - width (-> frames first :width) - height (-> frames first :height)] - {:width width - :height height - :color? (if color? true false) ; forcing an explicit true/false because i am nitpicky like that - :frames (mapv #(select-keys % [:image :delay]) frames)}))) + (let [converted-frames (atom '()) + image-props (atom nil)] + (AnimatedGif/read + image-stream + (fn [^BufferedImage frame-image delay] + (let [converted (convert-image frame-image scale-to-width color?)] + ; on the first image, we should use it's properties to populate the general image properties map + ; (AnimatedGif/read will see to it that all gif frames will have the same width/height) + (if (nil? @image-props) + (reset! image-props + {:color? (if color? true false) ; forcing an explicit true/false because i am nitpicky like that + :width (:width converted) + :height (:height converted)})) + + ; and append the converted frame's ascii to the list + (swap! converted-frames conj {:image (:image converted) + :delay delay})))) + (merge + @image-props + {:frames @converted-frames})))) diff --git a/src/java/clj_image2ascii/java/AnimatedGif.java b/src/java/clj_image2ascii/java/AnimatedGif.java index 4c56eb6..e14f7ed 100644 --- a/src/java/clj_image2ascii/java/AnimatedGif.java +++ b/src/java/clj_image2ascii/java/AnimatedGif.java @@ -1,5 +1,6 @@ package clj_image2ascii.java; +import clojure.lang.IFn; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @@ -12,8 +13,6 @@ import javax.imageio.stream.ImageInputStream; import java.awt.*; import java.awt.image.BufferedImage; import java.io.IOException; -import java.util.Iterator; -import java.util.LinkedList; /** * Helper for extracting each frame of animation from a GIF as a separate BufferedImage. @@ -25,12 +24,14 @@ import java.util.LinkedList; * @author gered (_extremely_ minor tweaks) */ public class AnimatedGif { - public static LinkedList read(ImageInputStream stream) throws IOException { + public static void read(ImageInputStream stream, IFn fn) throws IOException { ImageReader reader = ImageIO.getImageReadersByFormatName("gif").next(); reader.setInput(stream, false); - // note: using a LinkedList so we can do some quick filtering out of zero delay frames in the future - LinkedList frames = new LinkedList(); + // will hold a copy of the last frame's "full" image which we can use to restore from a "restoreToPrevious" + // disposal method if found in a subsequent frame. this will constantly be changed as we read through the + // gif's frames and come across non-restoreToPrevious disposal method frames. + BufferedImage lastFullFrame = null; // 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 @@ -60,10 +61,9 @@ public class AnimatedGif { } // 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 object for. 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; @@ -121,10 +121,14 @@ public class AnimatedGif { // 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)); + // invoke the passed Clojure function given passing the delay and the current canvas image which will + // contain a copy of this frame's "full" image. skip over this for zero-delay frames, which are just + // intermediate frames to "prep" the canvas for subsequent frames (i guess as a way to clear/fill the + // background for a bunch of upcoming frames which don't fill the entire canvas? anyway, we don't need them + // anymore as they aren't meant to be displayed). + // More info: http://www.imagemagick.org/Usage/anim_basics/#zero + if (delay > 0) + fn.invoke(canvas, delay); // handle certain disposal methods if (disposal.equals("restoreToPrevious")) { @@ -132,16 +136,9 @@ public class AnimatedGif { // 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) { - from = frames.get(i).image; - break; - } - } - // reset the canvas to the previous frame which we found above - canvas = new BufferedImage(from.getColorModel(), from.copyData(null), from.isAlphaPremultiplied(), null); + // reset the canvas + canvas = new BufferedImage(lastFullFrame.getColorModel(), lastFullFrame.copyData(null), lastFullFrame.isAlphaPremultiplied(), null); canvasGraphics = canvas.createGraphics(); canvasGraphics.setBackground(new Color(0, 0, 0, 0)); @@ -156,20 +153,13 @@ public class AnimatedGif { // ready for the next frame canvasGraphics.clearRect(x, y, image.getWidth(), image.getHeight()); } + + // keep a copy of the current canvas image if this frame can be used to recover from a "restoreToPrevious" + // disposal method in a future frame + if (!disposal.equals("restoreToPrevious") || lastFullFrame == null) + lastFullFrame = new BufferedImage(canvas.getColorModel(), canvas.copyData(null), canvas.isAlphaPremultiplied(), null);; } reader.dispose(); - - // remove zero-delay frames, which are just intermediate frames to "prep" the canvas for subsequent frames - // (i guess as a way to clear/fill the background for a bunch of upcoming frames which don't fill the entire - // canvas? anyway, we don't need them anymore as they aren't meant to be displayed) - // More info: http://www.imagemagick.org/Usage/anim_basics/#zero - Iterator itor = frames.iterator(); - while (itor.hasNext()) { - if (itor.next().delay == 0) - itor.remove(); - } - - return frames; } }