A few words from Agical

ImageMagick + Pango + Babashka = ♥️

If you want to conveniently compose pictures and text programmatically you may want to have a look at ImageMagick. Especially when powered by Pango markup. The markup is SGML and the resulting text line breaks and flows automatically. It gets quite a lot easier to lay your text out this way, than to use ImageMagic’s built in text facilities. And some things do not only get easier, they become possible.

However, first you need to get the ImageMagick + Pango pair installed. Depending on your machine this can get complicated. I still haven’t figured out how to do it on my Mac. (See here for how I worked around it.) As my real use case was to get it to happen in a CI build step, I searched for ImageMagic + Pango Docker images and couldn’t find one that actually worked.

So I created and pushed one: cospaia/magick-pango-babashka

The Dockerfile is super simple. It is based on the babashka/babashka image, which means I can then just apt install two things, and that’s about it.

I guess the real work here was to test that it worked for the purpose. But that was just plain fun! Basing my image on the Babashka image means I can use my favorite programming language to create compositions.

Let’s start with a more boring test. Over at the ImageMagick site we find some Pango usage information and examples. Those were the ones I couldn’t get working. But now this works (Docker required, naturally):

docker run --rm -v $(pwd)/output:/output cospaia/magick-pango-babashka
convert -background lightblue pango:"Anthony Thyssen"
/output/pango.gif

You’ll have to imagine the awesome non-animated GIF. Now to a more fun example. Also demonstrated in this video:

More fun because it is a bit more realistic, and because I get to use Babashka. To try it yourself: go to this repository and copy the examples directory to your computer. It contains a Babashka script compose.clj and some images. The script creates the picture shown above and looks like so:

#!/usr/bin/env -S bb -cp ./bin
(ns compose
  (:require [babashka.fs :as fs]
            [babashka.process :as p]))

(defn compose! [output-path]
  (let [output-dir (-> output-path fs/file fs/parent)]
    (when output-dir
      (fs/create-dirs output-dir))
    (let [tmp-dir (fs/path (fs/temp-dir) (name (gensym "compose-")))
          pango (str (fs/path tmp-dir "pango.png"))
          montage-1 (str (fs/path tmp-dir "montage-1.png"))
          montage-2 (str (fs/path tmp-dir "montage-2.png"))]
      (fs/create-dirs tmp-dir)
      (println "Writing pango output to: " pango)
      (p/sh "convert" "-background" "white" "-size" "1140x"
            (str "pango:<span font_size='36000'>"
                 "<b>magick-pango-babashka<tt>:latest</tt></b></span>"
                 "\n"
                 "<span font_size='28000'>"
                 "A Docker image to power your pictures + text "
                 "compositions.</span>"
                 "\n\nExample:\n"
                 "<tt>docker run -v \"$(pwd)\":/work -w /work "
                 "cospaia/magick-pango-babashka examples/compose.clj "
                 "output/composition.png</tt>")
            "-bordercolor" "white" "-border" "30"
            pango)
      (println "Writing first montage output to: " montage-1)
      (p/sh "montage" "-resize" "350x"
            "examples/assets/ImageMagick.png" "examples/assets/pango-name.png"
            "examples/assets/babashka.png"
            "-geometry" "+0+0" "-gravity" "center"
            "-background" "white" "-tile" "x1"
            "-mode" "Concatenate"
            montage-1)
      (println "Writing second montage output to: " montage-2)
      (p/sh "convert" montage-1
            "-gravity" "north" "-extent" "1200x383+0-20"
            montage-2)
      (println "Writing result output to: " output-path)
      (p/sh "montage" montage-2 pango
            "-tile" "x2" "-mode" "concatenate"
            output-path))))

(comment
  (compose! "output/composition.png")
  :rcf)

(if (not= *file* (System/getProperty "babashka.file"))
  (println "Use the REPL, Padawan")
  (if-let [[output-path] *command-line-args*]
    (compose! output-path)
    (println "Usage: ./examples/compose <output-file>")))

I’m an ImageMagick noob so this can probably be done in much better ways. What I do is to:

  1. Create the text as an image using the convert command and Pango markup. This image has a thick border, because I couldn’t figure out how to later compose it with margins.

  2. Tile the three logos on a row using the montage command. I size them all so that the row should fit well within 1200px width, with some padding. I also center them vertically.

  3. Size the image to 1200x383 px adding the 20px of padding, using the convert command.

  4. Create a new image with the montage and the text images on top of each other using the montage command.

Here is how you can run it from the command line, even if you are on a Mac and, like me, fail to install ImageMagick with Pango support on your machine.

docker run -v "$(pwd)":/work -w /work cospaia/magick-pango-babashka
examples/compose.clj output/composition.png

Please use the script as a base for your compositions. And please tell me if you know the smarter ways to compose that image. Why not retweet or comment here? 🙏