You want to create a text or graphical document as a PDF, where you have complete control over the layout.
Use Austin Zeiglers PDF::Writer library, available as the pdf-writer gem. Its API gives you fine-grained control over the placement of text, images, and shapes.
This code uses PDF::Writer to produce a simple flyer with an image and a border (Figure 12-7). It assumes youve got a graphic called sue.png to insert into the document:
require ubygems require pdf/writer # => false # Putting "false" on the next line suppresses a huge output dump when # you run this code in irb. pdf = PDF::Writer.new; false pdf.text("LOST DINOSAUR", :justification => :center, :font_size => 42, :left => 50, :right => 50) pdf.image("sue.png", :left=> 100, :justification => :center, :resize => 0.75) pdf.text(%{Three-year-old Tyrannosaurus rex Spayed Responds to "Sue"}, :left => 80, :font_size => 20, :justification => :left) pdf.text("(555) 010-7829", :justification => :center, :font_size => 36) pdf.rectangle(pdf.left_margin + 25, pdf.y-25, pdf.margin_width-50, pdf.margin_height-pdf.y+50).stroke; false pdf.save_as(flyer.pdf)
So long as you e only calling Writer#text and Writer#image, PDF generation is easy. PDF automatically adds new text and images to the bottom of the current text, creating new pages as needed.
It gets tricky when you want to do something more complex, like draw shapes. Then you need to specify the placement and dimensions in coordinates.
Take as an example the Writer#rectangle call in the Solution:
pdf.rectangle(pdf.left_margin, pdf.y-25, pdf.margin_width, pdf.margin_height-pdf.y+25).stroke
The first two arguments are coordinates: the left edge of the rectangle and the bottom edge of the rectangle. The second two arguments are the width and height of the rectangle.
The width is simple enough: my box starts at the left margin and its width is pdf.margin_width user space units.[6] That is, my box takes up the entire width of the page except for the margin. The height is a little more tricky, because I do my own margins (25 user space units above and below the text), and because PDF coordinates start from the bottom-left of the page, not the top-left. Think of a Cartesian plane: the point (0,0)is below the point (0,1)and left of the point (1,0). Thats how it is on a PDF page.
[6] A PDF user space unit is 1/72 of an inch.
Writer#y gives you the current position of the PDF::Writer "cursor:" the y-coordinate of the space directly under the most recently added text or image. I use this to place the bottom of the box just under the text.
If you want to generate many PDF documents from a template, you don need to generate the whole document from scratch each time. You can create a PDF::Writer containing the skeleton of a document (say, just the corporate letterhead), then use Marshal.dump to save it to a binary string. You can then use Marshal.load as many times as necessary to get new documents, and fill in the blanks separately for each document.[7]
[7] Yes, this is kind of hacky. The best we can say is that the author of PDF::Writer himself recommends it (see "Creating Printable Documents with Ruby," cited in the following See Also section).
Heres a Ruby class that generates personalized certificates of achievement. We generate the PDF ahead of time with generate_pdf, leaving a blank space for the name. We can then fill in names by calling award_to. Instead of rerunning the PDF generation code every time, award_to copies the predefined PDF over and over again by loading it from its marshalled format.
require ubygems require pdf/writer class Certificate def initialize(achievement) @without_name = Marshal.dump(generate_pdf(achievement)) end def award_to(name) pdf = Marshal.load(@without_name) pdf.move_pointer(-225) pdf.text("#{name}", :font_size => 64, :justification => :center) return pdf end private def generate_pdf(achievement) pdf = PDF::Writer.new( :orientation => :landscape ) pdf.info.title = "Certificate of Achievement" draw_border(pdf, 10, 12, 16, 18) draw_text(pdf, achievement) return pdf end def draw_border(pdf, *px_pos) px_pos.each do |px| pdf.rectangle(px, px, pdf.page_width - (px * 2), pdf.page_height - (px * 2)).stroke end end def draw_text(pdf, achievement) pdf.select_font "Times-Roman" pdf.text(" ", :font_size => 52) pdf.text("Certificate of Achievement ", :justification => :center) pdf.text(" ", :font_size => 18) pdf.text("hereby granted to ", :justification => :center) pdf.text(" ", :font_size => 64) pdf.text("in recognition of achieving the status of", :font_size => 18, :justification => :center) pdf.text(achievement, :font_size => 64, :justification => :center) end end
Now we can create a certificate and award it to many different people:
certificate = Certificate.new(Ruby Hacker); false [Tricia Ball, Marty Wise, Dung Nguyen].each do |name| certificate.award_to(name).save_as("#{name}.pdf") end
Figure 12-8 shows what tricia Ball.pdf looks like.
This recipe only scratches the surface of what you can do with the PDF::Writer library. Fortunately, theres an excellent manual and RDoc documentation. Although the library provides a lot of classes, most of the methods you want will be in PDF::Writer and the mixin PDF::Writer::Graphics.
Strings
Numbers
Date and Time
Arrays
Hashes
Files and Directories
Code Blocks and Iteration
Objects and Classes8
Modules and Namespaces
Reflection and Metaprogramming
XML and HTML
Graphics and Other File Formats
Databases and Persistence
Internet Services
Web Development Ruby on Rails
Web Services and Distributed Programming
Testing, Debugging, Optimizing, and Documenting
Packaging and Distributing Software
Automating Tasks with Rake
Multitasking and Multithreading
User Interface
Extending Ruby with Other Languages
System Administration