Ever created some silly border-line pointless libraries just for fun? Well, I have two more to add to that particular
pile.
clj-figlet
This library allows you to load FIGlet fonts (for which there is a
large database of them available) and render any string of text. So, you can do
stuff like:
(use 'clj-figlet.core)
(println
(render-to-string
(load-flf "/Users/gered/standard.flf")
"Hello, world!"))
And get output like:
_ _ _ _ _ _ _
| | | | ___ | | | | ___ __ __ ___ _ __ | | __| | | |
| |_| | / _ \ | | | | / _ \ \ \ /\ / / / _ \ | '__| | | / _` | | |
| _ | | __/ | | | | | (_) | _ \ V V / | (_) | | | | | | (_| | |_|
|_| |_| \___| |_| |_| \___/ ( ) \_/\_/ \___/ |_| |_| \__,_| (_)
|/
And that's pretty much all there is to it!
Right now clj-figlet only supports the "Full Size" style of rendering FIGlets. The other "smushing" style will be added
in an upcoming release.
clj-image2ascii
This library was inspired by Claskii. The author also has a great post
detailing it's development here.
So, why would you want to use this library? Well, what a silly question. I mean, who among us has not ever wanted to
change this:
into this:
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
........................................$###########################:...$##:........................
................................A#############################################......................
...........................*################$.............................=####.....................
........................###########............#####+.....###................###....................
.....................#######=:*@####@A######@.................##..............###...................
...................#####.........................................##............###..................
.................####....+##A................A###,.......####......#+...........###.................
................###,....%.....:#############%.................##.....#...........###................
..............####...............................................##...##..........##................
.............####..........................................A##@....#%...#.........###...............
............@##%..........##=.....###*.............#............##...#...:=........##...............
............A#A.........#.........................#................#..#....#.......###..............
............##.........#.........................#..................#...#...........##..............
............##........A.............#............#.......................#..........##%.............
............##........,..........................@........................$..........##.............
...........###..........................................#########....................##A............
...........##,.......................................###############..................##............
..........###.............#####=...................#####..###########.................###...........
.........###............###########...............###.....############.................###..........
........###............###....#######............###.......#######...##..#........#########.........
.......###...#####A##..###############..........###....################....##....#####A..###,.......
......###,.A#:...........#################......###..######+......#####.................#..##A......
......###.#A.....................:#######........#######.....#...............#######......#.###.....
.....###.#...#......................###...........###........###...........###########.......##.....
.....###.A.#....$#..................@#.......................,###........######.....###.......##....
.....##.#..+..########..............@#........................#####=..#######...#.....##......###...
.....##.#....###########..@#........@#..........................##########$....##......##...:..##...
.....##..............#######........##.........................................##......##...#..##...
.....##...........#..%#####=......+###........................................###@......##..#..##@..
.....##...........##.............####.............###...,..................*#######.....##..#..+##..
.....##.+.........##............###..............######..................#####@.####....##..#..%##..
......###..#,....###...........###..................,##.$######........#####....######..##..,..###..
......###....#...###.........#####...................##.............A#####......##.A##.##......##...
......A###......####........#.#####.........#######..##...........######%......###.....##......##...
.......##..##...#####.....#......###........#######.###........########.......*##......#...#..###...
.......###...#..##.###............###...............##=.....#######*.##......####..........#..##....
........##=....########............###.##................#######.....##.....#####.........#..###....
........###....#####.####...........####@............#######%.......*##..#######........#...###.....
.........##....#####.#######..........##.........########...........###########............###......
.........##....####..##.########%.........+###########%##..........#######..###..........####.......
.........%##...####..##...#######################......###.......#######...###...........###........
..........##...####..##...##..,##########$..##..........##...$#########....##...........###.........
..........##...####.###...##.......##.......##..........##@#########.##...##A..........###..........
..........##...########..###.......##.......##..........##########..##A..###...........##...........
..........##...##########.##.......##.......##......############.....##.###...........###...........
..........##...##############################################........#####............##............
..........##...############################################..........%####............##............
..........##...:#######################################..##..........####............##.............
..........##....####################################.....##.........####.............##.............
..........##....##.############################*.........###......####@.............##..............
..........##....######.####################=..#...........##.....####..............###..............
..........##.....##.##.###..+##....###........#,..........###..#####..............###...............
..........#@.....##A##..##...##.....##.......,##...........##+####...............###................
.........##......*####..###..###....##.......+##...........######...............###.................
.........##.......####...##...##.....##.......##........#######......#=...##...###..................
.........##........########...##*....##.......##.....########.....$#....##...A###...................
.........##..........#############,..##.....+#############......##....##%...####....................
.........##.............###############################.......##....##%...####......................
.........##...................=################%............##....##....A####.......................
.........##...............................................##....##....=####.........................
.........##.........#,.........@@@@@@@$::A###...........##...##@....@####...........................
.........##..........##..............................##...A##.....#####.............................
........,##....#.......###.......................###...###......#####...............................
........=##.....#.........,#####%==%###+............##........#####.................................
........=##......##............................,##..........#####...................................
........,##........##A....................###.............####+.....................................
.........##...........=#############=...................####,.......................................
.........##...........................................####..........................................
..........##........................................####=...........................................
..........###................................#########$.............................................
...........###............................A#########................................................
............####.......................##########...................................................
.............:#####...............#########.........................................................
...............########################.............................................................
..................################..................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
....................................................................................................
Seems like a real no-brainer to me.
clj-image2ascii can also convert animated GIFs to a set of ASCII frames with frame delay information so you can set up
your own animation of ASCII images.
Performance
Working on this library was a nice exercise in writing performant Clojure code for me. I've been working with Clojure
now for about a year on mostly just CRUD web apps where writing super performant code is not that important. After
fiddling around with the original Claskii code, I decided to benchmark it (using
Criterium mostly) and found that it was quite slow. Which isn't all that
surprising if you take a look at the code.
For one, there are no type hints anywhere, so Clojure will end up doing quite a bit of reflection lookups at runtime.
From what I've seen in other bits of code before working on this project, adding type hints -- even just by itself --
can make quite a significant difference in performance. However, we can still do better.
For example, Claskii is doing this for each pixel it reads from the source image it is converting:
(get-properties
(Color. (.getRGB img x y))
.getRed .getGreen .getBlue)
get-properties
is a macro the author had which just returns a vector of the values from each of the property getters
listed. Essentially this is creating a new Color
object for each pixel (obtained via BufferedImage.getRGB
which
returns a 32bit packed ARGB integer). Obviously creating a new object for each pixel is not performant.
Later on, to convert the pixel to an ASCII character with HTML representation including color information, it does this:
(html [:span {:style (format "color: rgb(%s,%s,%s);" red green blue)} output])
Which is using hiccup to generate an HTML string from Clojure data structures, and of course that call to format
which internally uses String.format
which performs memory allocations and is somewhat slow to be calling in an inner
loop.
There are a few more examples, but you get the idea. My intention is not to pick apart Claskii. It does work, and it
works quite well. It could just be faster, and I figured what the heck, lets give it a shot.
Eventually I ended up with the following set of functions:
(defn add-pixel [Integer argb StringBuilder sb color?]
(let [r (bit-shift-right (bit-and 0x00ff0000 argb) 16)
g (bit-shift-right (bit-and 0x0000ff00 argb) 8)
b (bit-and 0x000000ff argb)
peak (int
(Math/sqrt
(+ (* r r 0.241)
(* g g 0.691)
(* b b 0.068))))
char-index (if (zero? peak)
(dec num-ascii-chars)
(dec (int (* num-ascii-chars (/ peak 255)))))
pixel-char (nth ascii-chars (if (pos? char-index) char-index 0))]
(if color?
(doto sb
(.append "<span style=\"color: rgb(")
(.append r)
(.append ",")
(.append g)
(.append ",")
(.append b)
(.append ");\">")
(.append pixel-char)
(.append "</span>"))
pixel-char)))
(defn pixels->ascii [BufferedImage image color?]
(let [width (.getWidth image)
height (.getHeight image)
sb (StringBuilder. (+ (* width height 47) (* height 4)))
ints pixels (.getRGB image 0 0 width height nil 0 width)]
(dotimes [y height]
(dotimes [x width]
(add-pixel (aget pixels (+ x (* y width))) sb color?))
(.append sb (if color? "<br>" \newline)))
(.toString sb)))
I arrived at this after doing quite a bit of reading into writing fast Clojure code. It's probably not the fastest or
best code, but it is still very fast. Key factors that make this performant:
- Type hints.
(set! *warn-on-reflection* true)
is your friend!
- Using a
StringBuilder
, and initializing it with a capacity that will be large enough to hold the entire string we
will be building up.
- Accessing the raw pixels in
BufferedImage
via its getRGB
method which returns the pixels as an int[]
.
- Bit shifts to decompose the packed 32-bit ARGB integer used for each pixel in the image.
Using Criterium for benchmarking on my computer, I found this had a mean execution time of about 86ms when converting a
300x300 24-bit color PNG with color information being included in the output ASCII string. Very good, considering my
first unoptimized version of Claskii's algorithm ran with a mean execution time of over 800ms!
But the question left in my mind was, if I wrote this in pure Java, would it be faster? So, I went ahead and wrote a
Java version:
public class ImageToAscii {
static final char[] asciiChars = {'#', 'A', '@', '%', '$', '+', '=', '*', ':', ',', '.', ' '};
static final int spanLength = "<span style=\"color:rgb(255,255,255);\">X</span>".length();
static final int lineTerminatorLength = "<br>".length();
public static String convert(BufferedImage image, boolean useColor) {
int width = image.getWidth();
int height = image.getHeight();
int maxLength = (useColor ?
(width * height * spanLength) + (height * lineTerminatorLength) :
(width * height) + height);
StringBuilder sb = new StringBuilder(maxLength);
int[] pixels = image.getRGB(0, 0, width, height, null, 0, width);
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
int argb = pixels[(y * width) + x];
int r = (0x00ff0000 & argb) >> 16;
int g = (0x0000ff00 & argb) >> 8;
int b = (0x000000ff & argb);
int brightness = (int)Math.sqrt((r * r * 0.241f) +
(g * g * 0.691f) +
(b * b * 0.068f));
int charIndex;
if (brightness == 0.0f)
charIndex = asciiChars.length - 1;
else
charIndex = (int)((brightness / 255.0f) * asciiChars.length) - 1;
char pixelChar = asciiChars[charIndex > 0 ? charIndex : 0];
if (useColor) {
sb.append("<span style=\"color:rgb(");
sb.append(r);
sb.append(',');
sb.append(g);
sb.append(',');
sb.append(b);
sb.append(");\">");
sb.append(pixelChar);
sb.append("</span>");
} else
sb.append(pixelChar);
}
if (useColor)
sb.append("<br>");
else
sb.append('\n');
}
return sb.toString();
}
}
The same benchmark (with Criterium, using a Clojure wrapper function that called this Java version) showed me that yes,
indeed Java was still faster for raw performance: mean execution time of 16ms.
I'm honestly not sure how much faster I could get the pure Clojure version to run, but I suspect any possible speed
improvements (if there are any) would end up making the code really un-idiomatic and harder to read. I don't find the
Java version hard to read at all, so I'm happy arriving at the conclusion that for raw performance, it does make sense
to at least consider using bits of Java in a predominantly Clojure project, especially given that interop between Java
and Clojure is extremely easy.