2018-01-23

Clj-pdf for Dummies

clj-pdf is a library for easily generating pdfs from Clojure. While the library elements used to build the pdf are pretty simplistic, you can still create some advanced things with a little bit of extra work.

In this tutorial, we’ll create a simple cover for a (non-existent) book”clj-pdf For Dummies.” Aren’t those series great?! Here’s the result pdf we’re going to create:

https://github.com/smogg/clj-pdf-for-dummies/blob/master/output/clj-pdf-for-dummies.pdf

And here’s the complete source-code: https://github.com/smogg/clj-pdf-for-dummies

Everything in the PDF was built with code except for the dummy guy, which I recreated by hand in graphics software.

Resources & setup

We are going to start with the default lein template. Run lein new clj-pdf-for-dummies from the command line and copy over resources/fonts and resources/img folders into your newly created project. Alternatively, you can clone the start branch of Github repo: https://github.com/smogg/clj-pdf-for-dummies/tree/start

Running the project

We want the end user of our app to be able to run the pdf generation from the command line using lein run. They will also be able to specify a destination/filename for the pdf in command line args, like this:

lein run --dest "your-dir/pdf-name.pdf"

Start by adding tools-cli to your project.clj:

:dependencies [[org.clojure/clojure "1.8.0"] 
                            [org.clojure/tools.cli "0.3.5"]] 
...

Next, let’s write the -main function:

src/clj-pdf-for-dummies/core.clj:

(ns clj-pdf-for-dummies.core 
  (:require [clojure.tools.cli :refer [parse-opts]] 
            [clojure.java.io :as io]) 
  (:gen-class)) 
 
(defn -main 
  "Generates the pdf at specified location (defaults to `output/clj-pdf-for-dummies.pdf`)." 
  [args] 
  (let [{{:keys [dest]} :options} 
        (parse-opts args 
                    [["-d" "--dest DEST" "Destination to output the pdf to." 
                      :default "output/clj-pdf-for-dummies.pdf"]])] 
    (io/make-parents dest) 
    (println "Generated pdf at" dest)))

We use tools-cli to parse the arguments passed by the user when they run the project from the command line. We also set a default destination and use io/make-parents function, to create the directory structure to put our pdf in (if it doesn’t already exist).

You can test that everything is working by running:

lein run --dest "test/dir/test.pdf"

You should see the test/dir directory structure created. Next, let’s introduce clj-pdf and generate some pdfs.

clj-pdf

Include [clj-pdf "2.2.30"] in your project dependencies, require it inside core.clj and create a few project variables:

project.clj

:dependencies [... 
               [clj-pdf "2.2.30"]]

src/clj-pdf-for-dummies/core.clj

(:require [clj-pdf.core :as pdf] 
          ... 
          
          
; -- PDF Sizes -----------------------------------------------------------------
(def page-size :letter) 
(def page-width 612) 
(def page-height 792) 
(def margin-left 30) 
(def margin-right 30) 
 
; -- Colors --------------------------------------------------------------------
(def black "#000000") 
(def white "#ffffff") 
(def yellow "#FCEE37") 
(def turquoise "#1ABC9C") 
 
(defn -main 
  "Generates the pdf at specified location (defaults to `output/clj-pdf-for-dummies.pdf`)." 
  [args] 
  (let [{{:keys [dest]} :options} 
        (parse-opts args 
                    [["-d" "--dest DEST" "Destination to output the pdf to." 
                      :default "output/clj-pdf-for-dummies.pdf"]])] 
    (io/make-parents dest) 
    (println "Generated pdf at" dest)))

Next, after (io/make-parents dest) line, we are gonna call thepdffunction fromclj-pdf.core` namespace:

 (pdf/pdf 
  [{:size page-size 
    :left-margin margin-left 
    :right-magin margin-right 
    :footer {:start-page 2 
             :align :center} 
    :pages true} 
   "Hello, this is my first pdf!"] 
 dest)

Above will generate a pdf with a string “Hello, this is my first pdf!” on the first (and only) pdf page. Inside the vector passed to pdf/pdf function you’ll see a map of settings for your document. I hope they are pretty much self-explanatory, but refer to clj-pdf docs for reference.

The shapes

So far our pdf is not exciting so let’s finally get to the nitty gritty and create some design. In clj-pdf you specify a cover by setting a :letterhead key on clj-pdf settings map:

(def cover) 
 
(pdf/pdf 
  [{... 
    :cover cover 
    ...} 
   [:pagebreak] 
   "Hello, this is my first pdf!"] 
 dest)

Notice we’ve also added a [:pagebreak] before the first pdf page. Otherwise, the text would appear on top of our cover.

Now, let’s display some things on the cover. We’ll start by setting the background to a yellow color. clj-pdf offers a [:graphics] element which takes a function with a single argument. That argument is a pdfgraphics2d object from an underlying Java iText library. Here’s what you’d do to display a red circle using it:

[:grahics 
  (fn [g2d] 
    (doto g2d 
      (.setColor java.awt.color/RED) 
      (.drawCircle 0 0 50 50)))]

First we called .setColor on the pdfgraphics2d object. All of the following methods will use that color to draw whatever shapes you want. Hence when we use the drawCircle method, we’ll see a red circle. You’ll notice that we’ve used a built-in color java.awt.color/RED, we’ll introduce custom colors in a bit. Let’s draw a square covering all of the cover using the yellow color we’ve defined earlier in this tutorial. Our yellow is in a hexadecimal RGB color format, so first, we have to convert it to something we can use with java.awt.Color. For that purpose we create a little helper function:

(defn hex->rgb-int 
  "Given a hexadecimal string (starting with `#`) representing a color, returns the corresponding RGB integer." 
  [c] 
  (-> c 
      (str/split #"#") 
      rest 
      first 
      (Integer/parseInt 16))) 
      
(def cover 
  [[:graphics {:under true} 
    (fn [g2d] 
      (doto g2d 
        (.setColor (-> yellow hex->rgb-int java.awt.Color.)) 
        (.fillRect 0 0 page-width page-height)))]]

Try generating your pdf now. The first page should be all yellow.

The fonts

Using fonts with [:graphics] element is a little trickier than with the rest of the elements in clj-pdf (unless you’re familiar with Java of course). First off (similar to what we did with colors) you have to create a new java.awt.Font instances loading the font .ttf files from resources:

(def special-elite-font
  (java.awt.Font/createFont
   java.awt.Font/TRUETYPE_FONT
   (-> "fonts/SpecialElite-Regular.ttf" io/resource io/input-stream)))

Sidenote: Unfortunately, this won’t work when running your project from a compiled .jar. If you need to generate pdfs with custom fonts this way you have two options - use system fonts or create temp directories, outside the jar and load the font from there.

By default, the font we just defined would be tiny. You can resize a font using .deriveFont method. Let’s create a little helper for later use:

(defn resize-font 
  [font size] 
  (.deriveFont font (float size)))

Knowing all that, let’s create another [:graphics] element for our cover:

[:graphics {:translate [0 100] :rotate -0.05} 
    (fn [g2d] 
      (let [width (page-width 50)] 
        (doto g2d 
          (.setColor (-> turquoise hex->rgb-int java.awt.Color.)) 
          (.setFont (java.awt.Font"GillSans-SemiBold" java.awt.Font/BOLD 22)) 
          (.drawString "Start making PDFs today!" margin-left 0) 
          (.setFont (java.awt.Font"GillSans-SemiBold" java.awt.Font/PLAIN 14)) 
          (.drawString 
           (str "Your absolutely useless and way too short guide to the awesome" 
                " clj-pdf Clojure library.") 
           margin-left 
           20) 
          (.setColor java.awt.Color/BLACK) 
          (.fillRect -25 35 width 300) 
          (.setColor (-> turquoise hex->rgb-int java.awt.Color.)) 
          (.fillRect -25 335 width 5))))]

In above example, we’re using system fonts to write a few sentences on the cover. Like before, we set the color, then we set the font and last but not least, we draw the string using drawString method. You should get the idea by now.

To position the [:graphics ... element we’ve also set :translate to [0 100] ([x y] position from top-left corner) as well a tilted it a bit by setting the :rotate key to -0.05 (degrees).

Two things are missing - the cover title and the dummy guy. Let’s deal with the former.

(defn cover-text 
  [g2d width] 
  (let [t1 "clj-pdf" 
        t2 "FOR" 
        t3 "DUMMIES" 
        special-elite-16 (resize-font special-elite-font 16) 
        special-elite-50 (resize-font special-elite-font 50) 
        lato-bold-italic-40 (resize-font lato-bold-italic 40)] 
    (doto g2d 
      (.setColor java.awt.Color/WHITE) 
    ; draw first string
      (.setFont lato-bold-italic-40) 
      (.drawString t1 0 50) 
 
    ; draw second string
      (.setFont special-elite-16) 
      (.drawString t2 0 100) 
 
    ; draw third string
      (.setFont special-elite-50) 
      (.drawString t3 0 170))))

Call the cover-text function inside cover and pass the g2d object as well as page-width (you’ll see why we need it in a second).

Call lein run, and you should see something like this:

clj-pdf for Dummies - unfinished cover screenshot

Next, we have to center the text. You could set the x position for the string manually, but let’s be serious… We’ll use .getFontMetrics method of g2d object to get the text width and then do some simple math to calculate desired x position of the text. Here’s how that little helper function could look like:

(defn center-font-x 
  [g2d width font string] 
  (let [font-metrics (.getFontMetrics g2d font)] 
    (int ((width 2) 
            ((.stringWidth font-metrics string) 2)))))

Let’s use it in the cover-text function:

(defn cover-text 
  [g2d width] 
  (let [t1 "clj-pdf" 
        t2 "FOR" 
        t3 "DUMMIES" 
        center (partial center-font-x g2d width) 
        special-elite-16 (resize-font special-elite-font 16) 
        special-elite-50 (resize-font special-elite-font 50) 
        lato-bold-italic-40 (resize-font lato-bold-italic 40)] 
    (doto g2d 
      (.setColor java.awt.Color/WHITE) 
    ; draw first string
      (.setFont lato-bold-italic-40) 
      (.drawString t1 (center lato-bold-italic-40 t1) 50) 
 
    ; draw second string
      (.setFont special-elite-16) 
      (.drawString t2 (center special-elite-16 t2) 100) 
 
    ; draw third string
      (.setFont special-elite-50) 
      (.drawString t3 (center special-elite-50 t3) 170))))

Generate the pdf and make sure everything is working as expected. We are getting pretty close!

The SVG

So far we’ve been only using the [:graphics] element, but there’s another way you can programmatically build graphical elements in your pdf. The [:svg] element lets you either load an svg file or render an SVG string. Since this tutorial is already getting pretty long, we’re going to do the former, but you could, for example, use a library like Hiccup to create pretty complicated things with it.

For our cover, we are gonna load, position and scale the previously created SVG file, here’s how your final cover definition should look like:

(def cover 
  [[:graphics {:under true} 
    (fn [g2d] 
      (doto g2d 
        (.setColor (-> yellow hex->rgb-int java.awt.Color.)) 
        (.fillRect 0 0 page-width page-height)))] 
   [:graphics {:translate [0 100] :rotate -0.05} 
    (fn [g2d] 
      (let [width (page-width 50)] 
        (doto g2d 
          (.setColor (-> turquoise hex->rgb-int java.awt.Color.)) 
          (.setFont (java.awt.Font"GillSans-SemiBold" java.awt.Font/BOLD 22)) 
          (.drawString "Start making PDFs today!" margin-left 0) 
          (.setFont (java.awt.Font"GillSans-SemiBold" java.awt.Font/PLAIN 14)) 
          (.drawString 
           (str "Your absolutely useless and way too short guide to the awesome" 
                " clj-pdf Clojure library.") 
           margin-left 
           20) 
          (.setColor java.awt.Color/BLACK) 
          (.fillRect -25 35 width 300) 
          (.setColor (-> turquoise hex->rgb-int java.awt.Color.)) 
          (.fillRect -25 335 width 5))))] 
   [:graphics {:translate [0 185] :rotate -0.05} 
    #(cover-text % page-width)] 
   [:svg {:scale 0.6 
          :translate [100 480]} 
    (io/resource "img/face.svg")]])

The end

And that’s pretty much it! While clj-pdf certainly has some limitations, you can build a lot of great things with it with a little bit of creativity. Hope you’ve learned something today - let me know if you have any comments and/or questions.