Digital photos and me, Part V

In this fifth and—I think—last post in the series, I’m going to describe the program I use to prepare sheets of photos for printing at my local Costco. You could use the same procedure for printing at, say, Wal-Mart or any of the various online photo printing shops.

It’s not really necessary for you to read the first four posts in the series (one, two, three, and four), but since the program uses the RMagick library for Ruby, reading the fourth post would probably be a good idea if you’re using a Macintosh, as it explains a fairly simple way to get RMagick installed. The program I’ll describe isn’t Mac-specific, so if you can get RMagick installed on your Windows or Linux machine, it should work fine. (The should in that last sentence is the tipoff that I haven’t tested it on Windows or Linux. If you do, let me know how it works.)

The idea behind the program is to create a JPEG file that:

An example would be something like this, which you can click on to see a larger version.

Back in 2000, I wrote a similar program in Python. I used it first to organize family photos, but then adapted it to handle the digital photos I was taking at work. I chose Python because of Tkinter, Python’s built-in interface to the Tk library of user interface widgets. I wanted the program to have a GUI that worked in Windows so my wife and coworkers could use it. The program worked pretty well, and I wrote a post about it some time ago. But I began to dislike it because:

The result is the following Ruby program. I’m not really a Ruby programmer—not yet, anyway—and it shows. The program is very much a procedural program, not object-oriented in any way, so I’m sure it would be considered poor Ruby style. No matter. For now, the important thing is that it works; the logic and data contained in it can be rewritten into a more Rubyish form as I learn the language and its styles and idioms.

I call the program photosheets because that’s what it makes. Here’s the listing.

#!/usr/bin/env ruby

require 'optparse'
require 'RMagick'
include Magick

Geometry = {
  4 => {
    'name' => 'Quad',
    'rows' => 2,
    'cols' => 2,
    'image_size' => [1280, 960],
    'dpi' => 300,
    'height' => 2400,
    'width' => 3000,
    'left_margin' => 145,
    'right_margin' => 2855,
    'top_margin' => 180,
    'title_size' => 16*4,
    'subtitle_size' => 10*4,
    'title_clearance' => 45,
    'label_size' => 9*4,
    'label_clearance' => 4,
    'horizontal_clearance' => 150,
    'vertical_clearance' => 100,
    'bfont' => 'Helvetica-Bold',
    'font' => 'Helvetica'
  },
  9 => {
    'name' => 'Nine',
    'rows' => 3,
    'cols' => 3,
    'image_size' => [800, 600],
    'dpi' => 300,
    'height' => 2400,
    'width' => 3000,
    'left_margin' => 150,
    'right_margin' => 2850,
    'top_margin' => 180,
    'title_size' => 16*4,
    'subtitle_size' => 10*4,
    'title_clearance' => 45,
    'label_size' => 9*4,
    'label_clearance' => 4,
    'horizontal_clearance' => 150,
    'vertical_clearance' => 100,
    'bfont' => 'Helvetica-Bold',
    'font' => 'Helvetica'
  }
}

def place_photo(photo, pos, per_page, lo)
  pos = (pos - 1) % per_page
  $img.read(photo)

  # Strip out the photo's directory path and extension.
  base = File::basename(photo, ".*")

  # Rotate if necesary and scale
  if  ($img.rows > $img.columns)
    $img.rotate!(270)
  end
  scale_x = lo['image_size'][0]*1.0/$img.columns
  scale_y = lo['image_size'][1]*1.0/$img.rows
  scale = [scale_x, scale_y].min
  h = (scale*$img.columns).round
  v = (scale*$img.rows).round
  $img.resize!(h,v, HammingFilter)

  # Determine placement on page.
  row = pos/lo['cols']            # zero-indexed
  col = pos - row*lo['cols']      # zero-indexed
  x = lo['left_margin'] +
          col * (lo['image_size'][0] + lo['horizontal_clearance']) +
          lo['image_size'][0] - h

  y = lo['top_margin'] + lo['title_size'] + lo['title_clearance'] +
          row * (lo['image_size'][1] + lo['vertical_clearance']) +
          lo['image_size'][1] - v

  lx = x + h
  ly = y + v + lo['label_clearance'] + lo['label_size']

  # Place the photo.
  # $sheet.composite!($thumb, x, y, OverCompositeOp)
  $sheet.composite!($img, x, y, OverCompositeOp)

  # Place the label.
  label = Draw.new
  label.font(lo['font'])
  label.pointsize(lo['label_size'])
  label.text_align(RightAlign)
  label.text(lx, ly, base)
  label.draw($sheet)

  $img.clear
  STDERR.print "#{pos+1}..." if $verbose
end

def make_sheet(sheet_count, photo_list, per_page, lo)
  # Determine the range of photos for this sheet.
  first_photo = (sheet_count - 1) * per_page
  last_photo = first_photo + per_page - 1
  if (last_photo >= photo_list.size)
    last_photo = photo_list.size - 1
  end
  sublist = photo_list[first_photo..last_photo]

  # Initialize the sheet.
  # sheet = Image.new(lo['width'], lo['height'])
  $sheet.erase!
  # $sheet = $sheet.texture_floodfill(100,100, $granite)

  # Draw the title block.
  tx = lo['width']/2
  ty = lo['top_margin'] + lo['title_size']
  title = Draw.new
  title.font(lo['bfont'])
  title.pointsize(lo['title_size'])
  title.text_align(CenterAlign)
  title.text(tx, ty, $ctitle) unless $ctitle == ''
  title.pointsize(lo['subtitle_size'])
  title.font(lo['font'])
  title.font_weight(NormalWeight)
  tx = lo['left_margin']
  title.text_align(LeftAlign)
  title.text(tx, ty, $ltitle) unless $ltitle == ''
  tx = lo['right_margin']
  title.text_align(RightAlign)
  title.text(tx, ty, $rtitle) unless $rtitle == ''
  title.draw($sheet)

  # Place the photos.
  pos = 0
  sublist.each do |photo|
    pos += 1
    place_photo(photo, pos, per_page, lo)
  end

  # Save the sheet.
  name = "Sheet-%02d.jpg" % sheet_count
  $sheet.write(name)
  STDERR.puts "#{name} done!" if $verbose
end


# Start of main program.

# Defaults.
per_page = 9
$ctitle = ''
$ltitle = ''
$rtitle = ''
$verbose = FALSE

# Process the options.
opts = OptionParser.new
opts.banner = "Usage: photosheets [options] files"
opts.separator("Create one or more 8x10 sheets of photos.\nOptions:")
opts.on("-n N", Integer, "photos per page (4 or 9; default: 9)") {
  |num| per_page = num }
opts.on("-t TITLE", String, "main title (centered)") {
  |str| $ctitle = str }
opts.on("-l SUBTITLE", String, "left subtitle (project)") {
  |str| $ltitle = str }
opts.on("-r SUBTITLE", String, "right subtitle (date)") {
  |str| $rtitle = str }
opts.on("-v", "verbose output (default: off)") {
  |str| $verbose = TRUE }
opts.on("-h", "--help", "print this message") {
  |str| puts opts.help; exit }
photo_list = opts.parse(ARGV)   #everything after the options

photo_list = photo_list.delete_if { |f| f =~ /^Sheet-/ }

layout = Geometry[per_page]

# Get the title and subtitles interactively if no main title is given.
if ($ctitle == '')
  STDERR.print "Main title:              "
  $ctitle = STDIN.gets
  STDERR.print "Left subtitle (project): "
  $ltitle = STDIN.gets
  STDERR.print "Right subtitle (date):   "
  $rtitle = STDIN.gets
end

# Initialize the counts and the images.
num_photos = photo_list.size;
num_sheets = num_photos/per_page
num_sheets =num_sheets + 1 if (num_photos % per_page > 0)
$granite = Magick::Image.read('granite:').first
# $granite = $granite.level(0, 1.50)
$sheet = Image.new(layout['width'], layout['height'])#{
#   self.background_color = '#DDD'
#  }
$img = ImageList.new

# Make the sheets.
1.upto(num_sheets) do |i|
  make_sheet(i, photo_list, per_page, layout)
end

As you can see, it’s about 200 lines long, with the first 50 or so lines taken up with defining the geometry of the layout.

Thanks to the optparse library, you can get a good idea of how the program works by typing photosheets --help at the command line. The response will be

Usage: photosheets [options] files
Create one or more 8x10 sheets of photos.

Options:
    -n N                             photos per page (4 or 9; default: 9)
    -t TITLE                         main title (centered)
    -l SUBTITLE                      left subtitle (project)
    -r SUBTITLE                      right subtitle (date)
    -v                               verbose output (default: off)
    -h, --help                       print this message

which tells you that you can use the -n option to specify either 4 or 9 photos per sheet and you can give the main title with the -t option and the two subtitles with the -l and -r options. Since at work I usually use the left subtitle for the project name and the right subtitle for the date or date range of the photos, those conventions are mentioned parenthetically in the help. Nine photos per sheet is the default because that’s what I use the most. You can change any of this stuff by fiddling with the parts of the program labeled # Defaults and # Process the options.

The program creates a series of JPEG files in the directory you are in when you call it. The files are named Sheet-01.jpg, Sheet-02.jpg`, etc. If you already have files with those names in the current directory, they’ll be overwritten (a ruthlessness that is a legacy of my Linux days).

Normally, the program doesn’t give any feedback as it runs—you just wait until the output files are created. But if you call the program with the -v option, it will give you a running account of its progress, like this

1...2...3...4...5...6...7...8...9...Sheet-01.jpg done!
1...2...3...4...5...6...7...8...9...Sheet-02.jpg done!
1...2...3...4...5...6...7...8...9...Sheet-03.jpg done!
1...2...3...4...5...6...7...8...9...Sheet-04.jpg done!
1...2...3...Sheet-05.jpg done!

An important feature not mentioned in the help is interactive input. If you call the program without the -t option, it assumes you want to provide the title and subtitles interactively and begins prompting you for them. Here’s an example of the prompts and the answers I gave to produce the sheet shown above.

Main title:              Main Title
Left subtitle (project): Project Name         
Right subtitle (date):   October 8, 2006

This behavior is set up to make my life easier and require the least amount of typing and thinking. My most common use of the program is to open the Terminal, change to a directory of photos, and call photosheets *.jpg. I then type in the titles (any of which can be blank) when prompted and wait for the multi-photo sheets to be created. These get uploaded to Costco and printed on 8×10 paper.

If you’re wondering why I bother printing out my photos when I can just view them on my computer, you need to get more experience with that big analog thing we older folks call the “real world.” Riffling through sheets of photos, or laying them all out on a big table, is much more efficient than looking at just a few at a time on your screen. No matter how big your screen is, I’ll be able to find a table that’s bigger.

So that’s it. These five posts, taken together, explain how I deal with digital photos on screen and off. No iPhoto necessary, and in fact, no iPhoto wanted because iPhoto’s method of organizing photos isn’t convenient for the way I work. Perhaps I should say “iPhoto’s old method of organizing photos,” as this way of working was developed when iPhoto ’05 was the current thing. I’ve read somewhere that the current iPhoto doesn’t force you to organize your photos its way. If that’s the case, I might be willing to revisit it. But right now I don’t feel any compelling need to.

Tags: