My solution so far.
I use 'BoxSorter' to keep all the boxes in a hash indexed by both
dimensions so its easy to find one that fits.  The 'TrunkSet'  keeps
track of the loaded boxes using arrays of characters.
The algorithm is kind of the opposite of Ilmari's: it iterates over
empty spaces in the trunk, looking for boxes that fit, instead of
iterating over the boxes.

-Adam
-----------------------------------------------------------------

require 'delegate'
class Box
  def initialize w,h
    @w,@h=w,h
  end
  def [] n
    n==0 ? @w :@h
  end
  def rotate!
    @w,@h = @h,@w
  end
  def other n
    n==@h ? @w :@h
  end
  def <=> box2   #compares the non-matching dimension
    if @w == box2[0] then @h <=> box2[1]
    elsif @w== box2[1] then @h <=> box2[0]
    elsif @h == box2[0] then @w <=> box2[1]
    else @w <=> box2[0]
    end
  end
end

class BoxSorter
  def initialize boxset
    @h = Hash.new
    boxset.each{|b|@h[b[0]]||=[];@h[b[0]]<<b;@h[b[1]]||=[];@h[b[1]]<<b; @h}
    @h.each {|k,v| (v.sort!||v).reverse!}
    # hash has key for each box side,
    #  containing array of boxes sorted by size of the other side
  end
  def size
    @h.size
  end
  def find_best_fit w,h
    while w>0
      set = @h[w]
      box = set.find{|b| b.other(w)<=h } if set
      if box
        self.remove box
        box.rotate! if box[0] != w
        return box
      end
      w-=1;
    end
  end
  def remove box
    @h.delete_if {|k,v| v.delete(box); v.empty? }
  end
end

class TrunkSet < DelegateClass(Array)
  def initialize w,h
    @width,@height=w,h
    super []
    grow
  end
  def grow
    @openrow=0
    self<< Array.new(@height){Array.new(@width){" "}}
  end

  def first_open_row
    loop do
      break if last[@openrow].find{|s| s==' '}
      grow if @height == (@openrow +=1)
    end
    last[@openrow]
  end
  def first_open_space
    gaps,lastchar = [],nil
    first_open_row.each_with_index do |c,i|
      if c==' '
        if c==lastchar then gaps[-1][0]+=1
        else                gaps << [1,i]; end
      end
      lastchar = c
    end
    gaps.max
  end
  def pad_out
    last[@openrow].map!{|c| if c==' ' then '+' else c end }
    first_open_row
  end

  def add_box box,  col
    size,height = box[0],box[1]
    (0..height).each do |row|
      fillchar = (row == height) ? ['+','-'] : ['|','#']
      if nil != (fillrow = last[@openrow+row])
        fillrow[col-1] = fillchar[0] if (col-1>=0 )
        size.times {|i| fillrow[col+i] = fillchar[1] }
        fillrow[col+size] = fillchar[0] if ( col+size < @width )
      end
    end
  end

  def rows_remaining
    @height-@openrow
  end
  def has_no_boxes?
    last.each{|r| return false if r.find{|c| c == '#'} }
    true
  end
end

class Packer
  def initialize size, boxes
    @loose_boxes = BoxSorter.new(boxes)
    @trunks = TrunkSet.new(*size)
  end

  def pack_a_box
    column,nextbox = nil,nil
    loop do
      space_available,column = @trunks.first_open_space
      nextbox = @loose_boxes.find_best_fit(space_available,
                                    @trunks.rows_remaining)
      break if nextbox
      @trunks.pad_out              #if no box fits, need to fill row with pads
    end
    @trunks.add_box(nextbox,column)
  end

  def pack
    until @loose_boxes.size == 0
      pack_a_box
    end
    (@trunks.rows_remaining).times { @trunks.pad_out }
  end

  def show
    @trunks.pop if @trunks.has_no_boxes?
    @trunks.each do |bin|
      bin.each { |row|puts row.join }
      puts ""
    end
    puts "#{@trunks.size} loads"
  end
end

class PackerParser
  attr_reader :binsize, :blocks
  def initialize file
    @binsize = file.readline.chomp
    @blocks = file.readline.chomp.split " "
  end
  def size
    @binsize.match(/(\d*)x(\d*)/)
    [$1.to_i,$2.to_i]
  end
  def boxes
    @blocks.map{|s| s.match(/(\d*)x(\d*)/);Box.new($1.to_i,$2.to_i)}
  end
end

if __FILE__ == $0
  pp = PackerParser.new(ARGF)
  puts pp.binsize
  puts pp.blocks.join(' ')

  pk = Packer.new(pp.size, pp.boxes)
  pk.pack
  pk.show
end