"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
|
20
|
||||||
ms)))
|
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
|
(defn convert-animated-gif-frames
|
||||||
"converts an ImageInputStream created from an animated GIF to a series of ASCII
|
"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
|
frames representing each frame of animation in the source GIF. scale-to-width is
|
||||||
|
@ -123,10 +115,23 @@
|
||||||
([^ImageInputStream image-stream color?]
|
([^ImageInputStream image-stream color?]
|
||||||
(convert-animated-gif-frames image-stream nil color?))
|
(convert-animated-gif-frames image-stream nil color?))
|
||||||
([^ImageInputStream image-stream scale-to-width color?]
|
([^ImageInputStream image-stream scale-to-width color?]
|
||||||
(let [frames (get-ascii-gif-frames image-stream scale-to-width color?)
|
(let [converted-frames (atom '())
|
||||||
width (-> frames first :width)
|
image-props (atom nil)]
|
||||||
height (-> frames first :height)]
|
(AnimatedGif/read
|
||||||
{:width width
|
image-stream
|
||||||
:height height
|
(fn [^BufferedImage frame-image delay]
|
||||||
:color? (if color? true false) ; forcing an explicit true/false because i am nitpicky like that
|
(let [converted (convert-image frame-image scale-to-width color?)]
|
||||||
:frames (mapv #(select-keys % [:image :delay]) frames)})))
|
; 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;
|
package clj_image2ascii.java;
|
||||||
|
|
||||||
|
import clojure.lang.IFn;
|
||||||
import org.w3c.dom.NamedNodeMap;
|
import org.w3c.dom.NamedNodeMap;
|
||||||
import org.w3c.dom.Node;
|
import org.w3c.dom.Node;
|
||||||
import org.w3c.dom.NodeList;
|
import org.w3c.dom.NodeList;
|
||||||
|
@ -12,8 +13,6 @@ import javax.imageio.stream.ImageInputStream;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.IOException;
|
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.
|
* 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)
|
* @author gered (_extremely_ minor tweaks)
|
||||||
*/
|
*/
|
||||||
public class AnimatedGif {
|
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();
|
ImageReader reader = ImageIO.getImageReadersByFormatName("gif").next();
|
||||||
reader.setInput(stream, false);
|
reader.setInput(stream, false);
|
||||||
|
|
||||||
// note: using a LinkedList so we can do some quick filtering out of zero delay frames in the future
|
// will hold a copy of the last frame's "full" image which we can use to restore from a "restoreToPrevious"
|
||||||
LinkedList<ImageFrame> frames = new LinkedList<ImageFrame>();
|
// 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
|
// 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
|
// 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
|
// 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
|
// BufferedImage object for. this is necessary because some types of animation will specify some frames as
|
||||||
// because some types of animation will specify some frames as smaller images which need to be rendered at
|
// smaller images which need to be rendered at certain positions on top of the previous frame, so having a
|
||||||
// certain positions on top of the previous frame, so having a canvas to draw on makes generating the full
|
// canvas to draw on makes generating the full image for each frame much simpler
|
||||||
// image for each frame much simpler
|
|
||||||
BufferedImage canvas = null;
|
BufferedImage canvas = null;
|
||||||
Graphics2D canvasGraphics = null;
|
Graphics2D canvasGraphics = null;
|
||||||
|
|
||||||
|
@ -121,10 +121,14 @@ public class AnimatedGif {
|
||||||
// draw this frame into our canvas
|
// draw this frame into our canvas
|
||||||
canvasGraphics.drawImage(image, x, y, null);
|
canvasGraphics.drawImage(image, x, y, null);
|
||||||
|
|
||||||
// create an ImageFrame instance for this frame, using the current contents of our canvas image (which
|
// invoke the passed Clojure function given passing the delay and the current canvas image which will
|
||||||
// should at this point have the full image contents to accurately draw this frame of animation)
|
// contain a copy of this frame's "full" image. skip over this for zero-delay frames, which are just
|
||||||
BufferedImage copy = new BufferedImage(canvas.getColorModel(), canvas.copyData(null), canvas.isAlphaPremultiplied(), null);
|
// intermediate frames to "prep" the canvas for subsequent frames (i guess as a way to clear/fill the
|
||||||
frames.add(new ImageFrame(copy, delay, disposal));
|
// 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
|
// handle certain disposal methods
|
||||||
if (disposal.equals("restoreToPrevious")) {
|
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
|
// 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..."
|
// result will be that same as what it was before that frame.. etc.. etc.. etc..."
|
||||||
// -- http://www.imagemagick.org/Usage/anim_basics/#dispose
|
// -- 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
|
// reset the canvas
|
||||||
canvas = new BufferedImage(from.getColorModel(), from.copyData(null), from.isAlphaPremultiplied(), null);
|
canvas = new BufferedImage(lastFullFrame.getColorModel(), lastFullFrame.copyData(null), lastFullFrame.isAlphaPremultiplied(), null);
|
||||||
canvasGraphics = canvas.createGraphics();
|
canvasGraphics = canvas.createGraphics();
|
||||||
canvasGraphics.setBackground(new Color(0, 0, 0, 0));
|
canvasGraphics.setBackground(new Color(0, 0, 0, 0));
|
||||||
|
|
||||||
|
@ -156,20 +153,13 @@ public class AnimatedGif {
|
||||||
// ready for the next frame
|
// ready for the next frame
|
||||||
canvasGraphics.clearRect(x, y, image.getWidth(), image.getHeight());
|
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();
|
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