Hackable PDF Typesetting in Ruby with Prawn
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
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:
- the origin
- 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
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
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
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.