These days Microsoft is often being hammered in both the news and in Open Source communities across the globe, so on behalf of the Clojure community I would like to submit a small tribute to the man at the wheel, Steve Ballmer.
Microsoft makes good money, but they are going through tough times. So to make life a little happier at Camp Microsoft, I've decided to write a little Image to Ascii Art converter. Demonstrating, among other things, the power of macros. I don't think I'll get any arguments, that when you conjoin clojure and ascii-art, you get claskii art - So I've named this project appropriately
To convert an image to ascii we need to look at it as a bunch of colored pixels, converting them to characters one at a time. Disregarding the details of which image-holder we will use, we always find ourselves doing some tedious java-interop when working with Java Classes. Here's a quick example of how to get the brighest color from a pixel:
(defn get-pixel [image x y] (let [color (.getRGB image x y) red (.getRed color) green (.getGreen color) blue (.getBlue color)] (apply max [red green blue])))
Very simple right? Yes, and very boring. What I would like to be able to do, is just get at the fields more directly, like:
claskii> (def image (ImageIO/read (File. "steve-ballmer.jpg"))) #'image claskii> (get-properties (Color. (.getRGB image 10 10)) .getRed .getBlue) [8 60]
Which of course isn't possible, because as soon as .getRed is evaluated I'll get a Symbol not defined error - Enter Macros! A recommended first step when writing macros is:
The second advice is
In our case we want get-properties to expand into something like
(get-properties object .getRed .getBlue)
>>> [(.getRed object) (.getBlue object)]
So to make this happen we need to walk through our method-list (ie. .getRed .getBlue) and return a sequence which results from applying the methods to the first argument, the object:
(defmacro get-properties [obj & properties] (for [property properties] (property obj)))
Because Lisp is homoiconic we treat code exactly like data in that (.getRed obj) is nothing more than a list whos first item is .getRed and the second is obj. To see what our macro expands to, call
claskii> (macroexpand-1 `(get-properties (Color. (.getRGB image 10 10)) .getRed .getBlue)) (nil nil)
Not what we wanted! The reason is, that we haven't taken control of evaluation, using the macro-characters. I recommend you checkout them all before proceeding, as these puppies give you a lot of power, but also make macro-definitions a little hard on the eyes. The backquote stops evaluation, while allowing us to prefix items with a tilde ~ for evaluation, ~@ for splicing ~@(list 1 2 3) => 1 2 3.
(defmacro get-properties [obj & properties] `(vector ~@(for [property properties] (property obj))))
Unfortunately when you check that using macroexpand-1, you'll see the exact same result as above. The reason is, that when we're passing .getRed as a symbol and symbols are similar to :keywords in that they are a function of their arguments. When you're calling the symbol on the object you get nil in return. But why is the symbol being evaluated?
The splicing ~@ forces evaluation within its body, so we need to add an extra backquote:
(defmacro get-properties [obj & properties] `(vector ~@(for [property properties] `(property obj))))
Then check it
claskii> (macroexpand-1 `(get-properties (Color. (.getRGB image 10 10)) .getRed .getBlue)) (clojure.core/vector (claskii/property claskii/obj) (claskii/property claskii/obj))
Now thats more like it! But calling it will throw an error of course, since property is not defined anywhere - Instead of taking property literally we want it evaluated to the 'property' var in the for-loop, and although direct evaluation would work in most-cases you always have to be on your toes when using macros, as one day a user will submit an argument, which clashes with one of yours. The solution is (gensym), which generates a unique name for your variables. Gensym can either be called directly or simple using the syntactic sugar #:
(defmacro get-properties [obj & properties] `(vector ~@(for [property# properties] `(~property# ~obj))))
claskii> (macroexpand-1 `(get-properties (Color. (.getRGB image 10 10)) .getRed .getBlue)) (clojure.core/vector (.getRed (java.awt.Color. (.getRGB claskii/image 10 10))) (.getBlue (java.awt.Color. (.getRGB claskii/image 10 10)))) claskii> (get-properties (Color. (.getRGB image 10 10)) .getRed .getBlue) [8 60]
Nice! It expands like we want and it gets us the result we want! There are a couple of optimizations which are begging to be done. Firstly the object is being created once for each method argument. This is partly because the macro allows it, and partly because I'm not actually passing an object but rather the code which constructs an object. Secondly, by adding at object with the syntactic `(let [obj# ~obj]) gensym, will actually change the gensym with each run of the for-loop. So the ugly, stable and fully functioning version, using manual gensym comes out looking like so:
(defmacro get-properties [obj & properties] (let [target (gensym)] `(let [~target ~obj] (vector ~@(for [property properties] `(~property ~target))))))
I realize and admit that macro definitions look awful because we need all of our syntactic weapony rolled out in order to get the expansion we want. On the other hand, if you can look past the superficial, Lisps homoiconicity lets us treat code exactly as data, which is just an incredible tool to be sitting with, as you can freely extend the very language itself by relatively simple means!
Well, to calm the nerves again, look at how easy java-interop has become. We have gone from:
(defn old-school [image] (let [w (.getWidth image) h (.getHeight image) r (.getRed image) g (.getGreen image) b (.getBlue image)]))
To:
(def new-school [image] (let [[w h] (get-properties image .getWidth .getHeight) [r g b] (get-properties (.getRGB image 10 10)l .getRed .getGreen .getBlue)]))
So making the ascii-art itself should be very simple. This is my strategy:
So begin by defining a list which you think looks good:
(def ascii-chars [\# \A \@ \% \$ \+ \= \* \: \, \. \space])
Then pick out the peak value and convert it to an index in the above data, by simple division
(defn ascii [img x y color?] (let [[red green blue] (get-properties ( Color. (.getRGB img x y)) .getRed .getGreen .getBlue) peak (apply max [red green blue]) idx (if (zero? peak) (dec (count ascii-chars)) (dec (int (+ 1/2 (* (count ascii-chars) (/ peak 255)))))) output (nth ascii-chars (if (pos? idx) idx 0)) ]
...And depending on the output-type selected by the user, return either that character or an html-version:
(if color? (html [:span {:style (format "color: rgb(%s,%s,%s);" red green blue)} output]) output)))
Now that we have a way of processing each pixels, we just need to walk them all:
(defn convert-image [uri w color?] (let [raw-image (scale-image uri w) ascii-image (->> (for [y (range (.getHeight raw-image)) x (range (.getWidth raw-image))] (ascii raw-image x y color?)) (partition w))
So that walks every X for every Y returning the ascii-representation of each pixel. When the for-loop completes you're sitting with a 1D stream of characters representing the image. In order to distinguish lines you need to partition the sequence, chopping it up every width-number-of-characters. Now we we're sitting with a sequence of lines, which we can properly format:
output (->> ascii-image (interpose (if color? "<BR/>" \newline)) flatten)]
The only difference between the html version and ascii at this point, is how to seperate the lines. Once that done we can flatten the sequence of safe printing.
(if color? (html [:pre {:style "font-size:5pt; letter-spacing:1px; line-height:4pt; font-weight:bold;"} output]) (println output))))
Adjust the html-settings to your liking - I'm no Ascii artist so consider this a raw prototype which more creative people can improve upon should they want to. Anyway, now's the time to test.
First I hit Google to get an image of the guy:
[/caption]
Second, lets try and run it directly from the REPL:
claskii> (convert-image "/home/lau/Desktop/steve.jpg" 50 nil)

Since thats cooked down to only 50 characters it doesn't really to the guy justice, so lets try the HTML rendere:
claskii> (spit "h.html" (convert-image "steve.jpg" 120 true))

Thats more like it - although I'll admit that the A's look bad.
You've seen how macros are functions that control evaluation and outputs code. Macros are both fun and tricky so use them carefully. The entire program weighs in at 55 lines and I've put it on Github: here.