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.