"stream" animated gif frames via ImageReader instead of keeping them all around in memory before converting to ASCII
This commit is contained in:
parent
f7d2519ab1
commit
f496c450ac
|
@ -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}))))
|
||||
|
|
|
@ -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<ImageFrame> 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<ImageFrame> frames = new LinkedList<ImageFrame>();
|
||||
// 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<ImageFrame> itor = frames.iterator();
|
||||
while (itor.hasNext()) {
|
||||
if (itor.next().delay == 0)
|
||||
itor.remove();
|
||||
}
|
||||
|
||||
return frames;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue