🤯 50% Off! 700+ courses, assessments, and books

Hackable PDF Typesetting in Ruby with Prawn

Robert Qualls
Share

cover

Generating portable documents can be tricky. For the most part, this task has moved from typesetting languages to WYSIWYG editors like Scribus and Adobe InDesign.

But automation has its own benefits. Instead of the kitchen-sink, it’s nice to have a hackable platform that can be easily tweaked to get us what we want.

Prawn is the spiritual successor to PDF::Writer. It is currently well-maintained and, like many other Ruby projects, Prawn is intended as a platform on which tools can be built.

LaTeX

Before we get into Prawn, the elephant deserves mentioning.

TeX was written by Donald Knuth and released in 1978. At that time, digital printing was fairly new and Knuth had just gotten back a galley proof of his second volume of “The Art of Computer Programming.” He was shocked by the quality and decided he could do a better job in a few months. It ultimately took him a decade (source).

TeX is famous for being bug free. Knuth had schemes in which those who found bugs in his documents or programs were paid a fee. The TeX program’s rewards followed the Wheat and Chessboard Problem, starting at $1.28 and reaching $327.68. Typical of Knuth, the version number of TeX is converging to π.

LaTeX (pronounced “lahtehk”) is a macro package for TeX that makes it easier to produce standard documents. These days, TeX is often referred to as LaTeX, and it is the de facto standard for creating scientific or mathematical documents in academia.

Here’s an example of LaTeX:

\documentclass{article}
\title{LaTeX Hello World}
\author{Robert Qualls}
\today
\begin{document}
\maketitle
Hello World
\end{document}

Those interested in LaTeX packages would want to check out CTAN, the Comprehensive TeX Archive Network, and the LaTeX rubygems. Bonus points if you recognize the similarity to CPAN: the Comprehensive Perl Archive Network.

Installing Prawn

If you’re running a Rails operation, LaTeX isn’t the most convenient approach for generating documents. There are some gems available, but it’s really nice when the logic and the presentation are in the same language. That’s what Prawn supplies.

First, we need to install the Prawn gem:

gem install prawn

We can verify that Prawn is working with the following test:

require "prawn"

Prawn::Document.generate("hello.pdf") do
  text "Hello World!"
end

Making Text

Most of Prawn’s commands are fairly minimalistic and what you would expect:

require "prawn"

Prawn::Document.generate("styling_text.pdf") do
  text "Default text styling"
  text "Blue 16pt Helvetica", size: 16, font: "Helvetica", color: "0000FF"
  text "Aligned Center", align: :center

  font_size 12
  font "Courier" do
    text "Size 12 Courier"
    font_size 10 do
      text "Slightly smaller Courier"
    end
  end
  text "Default font size 12"

  font "Helvetica"
  3.times do |i|
    text "Helvetica with leading 10 line #{i}", leading: 10
  end
end

text

Consistent with Ruby, Prawn provides multiple ways to accomplish the same thing. Most DSL methods can optionally be scoped with a block.

Moving Around

There are two important geometric locations in Prawn:

  1. the origin
  2. the cursor

The origin, [0,0] is at the bottom-left corner of the document. This can be confusing as the cursor starts in the top-left corner.

“[0,0] what?”, you might ask. The default unit in Prawn is a PDF Point, where one PDF Point is equal to 1/72 of an inch. Because America.

Moving the cursor around is as simple as move_down, move_up, or move_cursor_to:

require "prawn"

Prawn::Document.generate("moving_around.pdf") do
  text "#A At the top, cursor position #{cursor}"

  move_down 50
  text "#B Down 50, cursor position #{cursor}"

  move_cursor_to bounds.bottom + font_size
  text "#C at the bottom, cursor position #{cursor}"

  move_up 50
  text "#D Up 50 from #C, cursor position #{cursor}"

  move_cursor_to bounds.top / 2
  text "#E In the middle, cursor position #{cursor}"
end

moving_around

Adding Images

Prawn lets us use either local paths or external URLs with open-uri:

require 'prawn'
require 'open-uri'

Prawn::Document.generate("image.pdf") do
  text "Dog", align: :center, color: "333333", size: 42
  move_down 20
  text "Homo sapiens' best friend", align: :center, color: "555555", size: 26
  url = "https://pixabay.com/static/uploads/photo/2014/03/14/20/13/dog-287420_960_720.jpg"
  image open(url), fit: [500, 500], position: :center
end

image

It also supports backgrounds. Unfortunately, Prawn doesn’t seem to offer any way to fit the background when done this way, so you may need to tinker with the size. This can be done with ImageMagick and the mini_magick gem:

$ brew install imagemagick
$ gem install mini_magick

I decided to go with 650×950 for the image I used:

require 'prawn'
require "mini_magick"

url = "https://pixabay.com/static/uploads/photo/2015/11/19/08/12/milky-way-1050526_960_720.jpg"
filename = "fitted_background.jpg"
image = MiniMagick::Image.open(url)
image.resize "650x950"
image.write filename
background = filename

Prawn::Document.generate("background.pdf", background: background) do
  options = { align: :center, valign: :center, leading: 25, color: "C1C1C1" }
  text "\"Somewhere, something incredible is waiting to be known\"", options.merge({ size: 20 })
  text "- Carl Sagan", options.merge({ size: 18 })
end

background

Book Cover

Here is the source for the fake book cover.

require "prawn"

Prawn::Document.generate("oruby_cover.pdf") do
  move_down 60

  image "shrimp.png", fit: [500, 400], position: :center

  move_cursor_to bounds.top

  shape_color = "008888"
  font "Times-Roman"

  fill_color shape_color
  fill_rectangle [0, bounds.top], bounds.width, 20

  move_down 25

  fill_color "000000"
  text "Because InDesign is for scrubs", :size => 20, :style => :italic, :align => :center

  bounding_box([0, bounds.top - 50], :width => bounds.width, :height => bounds.height) do
    move_down 380
    text "using", :size => 40, :style => :italic
    move_up 380

    fill_color shape_color
    fill_rectangle [0, 300], 550, 200

    fill_color "FFFFFF"
    move_down 410
    font "Times-Roman"
    text "Prawn", :size => 165, :align => :center

    fill_color "000000"
    font "Helvetica"
    draw_text "O'RUBY", :at => [0, bounds.bottom + 50], :size => 30

    move_to [0, bounds.bottom + 100]
    font "Times-Roman"
    draw_text "Robert Qualls", :at => [bounds.right - 100, bounds.bottom + 50], :size => 20, :style => :italic
  end
end

Image courtesy of Pearson Scott Foresman(Source)

Conclusion

If you want to learn more about Prawn, the Prawn team has a nice manual.

Prawn’s DSL approach isn’t for everybody nor the best solution for all situations. In fact, HTML+CSS to PDF is probably the future of programmatic typesetting. Hardcoding instead of flexible design feels very 20th century, but Prawn is fairly popular, so there’s a good chance you will encounter it from time to time. Thankfully, we have wkhtmltopdf wrapped by the PDFKit gem. If you want to see a comparison, be sure to read PDF Generation in Rails.

CSS Master, 3rd Edition