Easy PDF Photo-Calendars in Ruby
Looking at the sample calendars above you would think that humanity has failed - each day is represented by a lead image from the BBC News and seems to showcase nothing but suffering, destruction, and pain. On a more positive note, generating these PDF calendars was nothing but joy thanks to the nifty PDF::Writer library by Austin Zigler. With a few extensions and some Ruby-foo magic, my final calendar featured news-clustering, multi-month views, and a number of other goodies. Here, we'll cover the basics of generating the PDF calendar grid and populating it with photos. Let's get to it!
Creating the PDF canvas
In my project, I had a number of different print-out styles (Calendar, Treemap, etc.) and output mediums (PDF, SVG, etc.). Hence, I created a Canvas class and abstracted the PDF code inside to decouple some of the functions:
require 'rubygems'
require 'pdf/writer'
require 'RMagick'
module Canvas
class Canvas
attr_accessor :name
def initialize(name)
@name, @doc = name, Pdf.new(name)
@maxWidth, @maxHeight = @doc.pdf.page_width, @doc.pdf.page_height
@x, @y = 0, @doc.pdf.y - 75 # offset the y-pointer
end
def save() @doc.save; end
end
private
class Pdf
attr_reader :name, :pdf
def initialize(name)
@name = name
@pdf = PDF::Writer.new(:orientation => :landscape)
# Set document meta-data
@pdf.info.title = @name
@pdf.info.author = "Ilya Grigorik"
@pdf.info.subject = "Calendar"
@pdf.margins_pt(0, 0, 0, 0) # top, left, bottom, right
end
def save() @pdf.save_as(@name); end
def addImage(*attrs) @pdf.add_image_from_file(*attrs); end
end
end
As you can see, the Canvas object will encapsulate the PDF class in our example. This is an unnecessary step if you're only working with PDF output, but we'll keep it as it is, it shouldn't complicate our code too much.
Creating the calendar view
The CalendarView class will be responsible for creating the grid and handling the calendar logic. First, the CalendarView constructor will initialize the pdf output file (super). Then, once the canvas is ready, it will go ahead and draw the calendar grid. Now, depending on the year/month view of the calendar, the first of every month will fall on a different day of we week, and we need to figure out which! Hence, we also provide a date object to the constructor (first day of the month, ex: '2007-02-01'):
module Canvas
class CalendarView < Canvas
def initialize(name, date)
super(name)
@topRow = 50
@rowH = (@doc.pdf.page_height.to_i - @topRow) / 6
@columnW = (@doc.pdf.page_width.to_i) / 7
@doc.pdf.stroke_color! Color::RGB.new(140,140,140)
# Draw calendar grid
1.upto(6) do |n|
@doc.pdf.line(@doc.pdf.absolute_left_margin, n*@rowH, @doc.pdf.absolute_right_margin, n*@rowH).stroke
end
1.upto(6) do |n|
@doc.pdf.line(n*@columnW, @doc.pdf.absolute_bottom_margin+6*@rowH, n*@columnW, @doc.pdf.absolute_bottom_margin).stroke
end
# Set first day of the week to Monday & figure out the start-position for current month
@order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
@startSquare = @order.index(date.strftime("%A"))-1
end
def addImage(file, date)
squareNum = @startSquare + Time.parse(date).day.to_i
row = squareNum / 7
col = squareNum % 7
# Calculate the x, y offsets
x = @doc.pdf.absolute_left_margin + col * @columnW
y = (@doc.pdf.absolute_top_margin.to_i - @topRow - @rowH * (row+1)) - 3
# Read the source image file and it's corresponding dimensions
imageMagick = Magick::Image.read(file).first
imgWidth, imgHeight = imageMagick.columns, imageMagick.rows
imgRatio = imgWidth.to_f / imgHeight.to_f
# scale to fit in calendar, preserve aspect ratio
imgWidth, imgHeight = @columnW, (@columnW / imgRatio).to_i if ((imgWidth > imgHeight) and (imgWidth > @columnW))
imgHeight, imgWidth = @rowH, (@rowH * imgRatio).to_i if ((imgHeight > imgWidth) and (imgHeight > @rowH))
imgHeight, imgWidth = @rowH, @columnW if (imgHeight == imgWidth)
# center the photo in the calendar square
x = x + (@columnW-imgWidth)/2 if imgWidth < @columnW
y = y + (@rowH-imgHeight)/2 if imgHeight < @rowH
@doc.addImage(file, x, y, imgWidth, imgHeight)
end
end
end
By this point, we already have the calendar layout, and now we just have to populate it with photos. To do so, we simply call on the addImage method, and provide the filename and the date of the photo - this method will automatically figure out the correct square and resize, and center the image for us!
Spiffy, on-demand PDF calendars
We're almost there. All we have to do now is define the list of photos and dates. This could be a directory listing with 'created' timestamps, or it could also be your Flickr photostream, etc. For the sake of brevity, we will simply hard-code a few photo filenames with dates:
require 'canvas.rb'
require 'calendar.rb'
# First parameter: output filename, Second parameter: Date object for the month/year of the calendar view
pdfDoc = Canvas::CalendarView.new("my-calendar.pdf", '2007-02-01')
# Load an array of images, provide full path, and the date (read exif from photo?)
images = [['image1.jpg', '2007-02-25'],
['image2.jpg', '2007-03-26']]
# Insert the images into our calendar!
images.each { |image| pdfDoc.addImage(image[0], image[1]) }
pdfDoc.save
That's it, a dynamic PDF photo-calendar in ~100 lines of code! If you're curious, you can view the full calendar for BBC news: download it first, it's rather large! For more examples on using PDF::Writer, I would also recommend a great guide by Austin Ziegler himself: Creating Printable Documents with Ruby.