2011-09-25

Adding Text to PNGs

(I'm putting this out here since Google queries for this situation didn't turn up anything useful for me.)

Last night I had to add some text ± 5000 PNG files and I knew about Zach Beane's ZPNG package for reading PNGs but I didn't know of a package that would read them.

A quicklisp:system-apropos turned up png-read written by Ramarren which looked simple enough to use so I went with that. After initially failing to get ZPNG to use png-read's image-data slot I gave up and wrote a little loop to copy the values over to zpng:data-array. This was succesful after one or two tries:

(asdf :png-read)
(asdf :zpng)

;; http://imgur.com/qtriH.png
(defparameter png (png-read:read-png-file "qtriH.png"))
(defparameter zpng (make-instance 'zpng:png :color-type :truecolor
                                  :width (png-read:width png)
                                  :height (png-read:height png)))

(loop for y from 0 below (zpng:height zpng)
      do (loop for x from 0 below (zpng:width zpng)
               do (loop for rgb from 0 below 3
                        do (setf (aref (zpng:data-array zpng) y x rgb)
                                 (aref (png-read:image-data png) x y rgb)))))

(zpng:write-png zpng "tmp.png")

I'm sure Zach had a good reason to reverse the x and y in ZPNG but I can't deduce it.

So, reading and writing the files was working but now I realized ZPNG didn't have any functionality for generating text. (Why should it? It shouldn't.) I started thinking of Vecto since I had used that before and also recalled that it actually used ZPNG for saving PNGs. Now all I needed to do was getting the original image into Vecto so text could be written over it.

Going through Vecto's source showed me I could get at the ZPNG object through the (shadowed) *GRAPHICS-STATE* global. It wasn't exported from the Vecto package but since I was in hack mode anyway it didn't really matter. I used the (slightly adapted) code above, drew some text, did a VECTO:SAVE-PNG and got an empty image.

In Vecto's source I noticed the ZPNG object had four channels instead of the three I was using so I set the fourth channel (alpha) to opaque and got my input PNG back with the custom text generated by Vecto. Done!

(asdf :png-read)
(asdf :vecto)

(use-package :vecto)

;; http://imgur.com/qtriH.png
(defparameter png (png-read:read-png-file "qtriH.png"))
(defparameter zpng (make-instance 'zpng:png :color-type :truecolor
                                  :width (png-read:width png)
                                  :height (png-read:height png)))

(with-canvas (:width (zpng:width zpng) :height (zpng:height zpng))
  (loop for y from 0 below (zpng:height zpng)
        do (loop for x from 0 below (zpng:width zpng)
                 do (loop for rgb from 0 below 4
                          do (if (< rgb 3)
                                 (setf (aref (zpng:data-array (vecto::image vecto::*graphics-state*)) y x rgb)
                                       (aref (png-read:image-data png) x y rgb))
                                 (setf (aref (zpng:data-array (vecto::image vecto::*graphics-state*)) y x rgb)
                                       255)))))
  (set-font (get-font "font.ttf") 32)
  (draw-centered-string (floor (/ (zpng:width zpng) 2))
                        (- (zpng:height zpng) 80)
                        "Hello, World!")
  (save-png "tmp.png"))

Labels: , , , , ,