🦩

Address

Unknown

the paranoid times

Fun with clj-pnm library and Babashka

01 Jul 2024

With 'clj-pnm' deployed to Clojars [1] it's easier to have some fun with it. And by fun I mean Babashka! We will write a script that will take a number as an argument and spit out a Portable Bitmap (pbm) [2] file representation of it. We will use numbers as input because it is a smaller set of values to cover, but the same logic can be used for alphanumeric values.

Preparing a Babashka playground is as easy as defining an EDN file for the dependencies and firing up an editor and a REPL.

;; bb.edn
{:paths ["."]
 :deps {org.theparanoidtimes/clj-pnm {:mvn/version "0.2.0"}}}

Since we are targeting a pbm file we will write our number representations as 3x5 matrices of zeros and ones. For example, the number one will be this matrix:

(def one [[0 0 1]
          [0 1 1]
          [1 0 1]
          [0 0 1]
          [0 0 1]])

The matrix is represented as a vector of vectors of bits. Each vector of bits is the row of the matrix. The same pattern is applied to all numbers with the addition of an all-zero matrix that will represent an empty space between digits.

After reading the number argument, we split it into a vector of characters (numbers as strings) which are then mapped to the corresponding matrices. The matrices are merged and passed to 'clj-pnm'.

(require '[clj-pnm.core :as pnm]
         '[clojure.string :as st])

(defn size-with-interposing
  [c]
  (- (* 2 (count c)) 1))

(defn number-to-pbm
  [number]
  (let [nums (-> number
                 str
                 (st/split #""))
        m (merge-number-matrices nums)]
    {:type :p1
     :width (* (size-with-interposing nums) 3)
     :height 5
     :map m}))

(defn write-number-to-pbm
  [number file-name]
  (pnm/write-pnm (number-to-pbm number) file-name))

The 'write-pnm' function expects a map representing the bitmap file, let's go through it. The ':type' keyword defines the type of Netpbm file which, for the purpose of this post, is always ':p1'. ':width' defines the width in the number of bits. We will concatenate the number matrices horizontally so width must factor in the number of number matrices, the number of space matrices, and the width of each matrix which is 3. On the other hand, because we concatenate horizontally the ':height' will remain constant which is the height of one number matrix (5).

For the map element, we need to transform the number string value to a vector representation of a bitmap where each element is a row of bits. Starting with a list of digits parsed from the input argument we will map them to the respectful matrix, interpose empty space matrices in between them, and interleave them so that rows of each matrix will line up with the rows of other matrices with the same index (first row next to other first rows, second row, etc.). We calculated the new row width with 'size-with-interposing' function, so we now partition the vector by that size. The result is a vector of vectors of vectors which is one layer too many, so we will apply concat to the innermost layer to 'flatten' it. This process is easily representable by the thread-last macro:

(defn merge-number-matrices
  [nums]
  (let [s (size-with-interposing nums)]
    (->> nums
         (map number-to-matrix)
         (interpose space)
         (apply interleave)
         (partition s)
         (map (fn [r] (apply concat r))))))

We can now pass this map to 'clj-pnm' and generate our pbm image file. However, upon opening it we see that it is too small to even distinguish the digits. This is expected given that it's a bitmap file, meaning that the image size is directly linked to the number of bits in the map. To amend this we will add a function to expand the bitmap in both directions by an arbitrary number.

To expand a pbm file we will need to adjust the ':width', ':height' and ':map' values. The first two are plain multiplication, but for the bitmap itself we will need to do some acrobatics.

(defn scalar-expand
  [m s]
  (for [x m]
    (apply concat (repeat s (flatten (for [y x]
                                       (repeat s y)))))))

(defn expand-pbm
  [s pbm]
  (-> pbm
      (update-in [:width] * s)
      (update-in [:height] * s)
      (update-in [:map] scalar-expand s)))

(defn number-to-pbm
  ([number] (number-to-pbm number 1))
  ([number scale]
   (let [nums (-> number
                  str
                  (st/split #""))
         m (merge-number-matrices nums)]
     (expand-pbm scale {:type :p1
                        :width (* (size-with-interposing nums) 3)
                        :height 5
                        :map m}))))

(defn write-number-to-pbm
  [number scale file-name]
  (pnm/write-pnm (number-to-pbm number scale) file-name))

To make the image bigger we will add more bits, making the shown digits thicker. One way to expand is to replicate each row and column of the bitmap. This is done by 'scalar-expand' function (technically, this is not a good name for this function) by repeating each bit in a row vector and then repeating the whole vector.

We can now generate huge pbm images, but be careful as the file size can also be huge.

(write-number-to-pbm "1138" 1000 "test.pbm")

Upon opening the generated file we can see the digits clearly.

Hope you will have fun with 'clj-pnm'!

See the full script at the repo. [3]

[1] clj-pnm on Clojars

[2] Netpbm

[3] Script repo

Home

--

theparanoidtimes@posteo.net

PGP

https://theparanoidtimes.org

Released under CC BY-SA

Made in 🇷🇸