Prawn: Turning Tables Part 2

Turning Tables Part 1

This is the second post in a series on my changes to Prawn to support rotated table text.

Prawn is a fast ruby gem for creating acrobat files. I use it to create reports and invoices. However, it does not have support yet for rotating table text. View the last post to see the details.

So what happens when you rotate table text? Let's take a look.

Find the Rotation

If we do a search for rotate in the source code, we find the render_rotated method inside Prawn::Text::Formatted::Box:

def render_rotated(text)  
  unprinted_text = ''

  case @rotate_around
  when :center
    x = @at[0] + @width * 0.5
    y = @at[1] - @height * 0.5
  when :upper_right
    x = @at[0] + @width
    y = @at[1]
  when :lower_right
    x = @at[0] + @width
    y = @at[1] - @height
  when :lower_left
    x = @at[0]
    y = @at[1] - @height
  else
    x = @at[0]
    y = @at[1]
  end

  @document.rotate(@rotate, :origin => [x, y]) do
    unprinted_text = wrap(text)
  end
  unprinted_text
end  

Based upon the @rotate_around setting, it configures the center of rotation. Then, after rotating the canvas, it wraps the text. This code will have to be updated to make the text stay inside the cell when it is rotated.

The wrap method on line 525 is inside Prawn::Text::Formatted::Wrap. (Wrap is a module included into Prawn::Text::Formatted::Box.) The wrap method refers to two key methods. One is available_width, which is inside Prawn::Text::Formatted::Box:

def available_width  
  @width
end  

This explains why the rotated text still thinks its width is the full width of the table cell. Regardless of the angle, it returns the full width. There are height functions also which will probably come into play.

The wrap method also calls print_line, which retrieves each fragment of that line and calls, eventually, draw_fragment in Prawn::Text::Formatted::Box:

def draw_fragment(fragment, accumulated_width=0, line_width=0, word_spacing=0) #:nodoc:  
  case(@align)
  when :left
    x = @at[0]
  when :center
    x = @at[0] + @width * 0.5 - line_width * 0.5
  when :right
    x = @at[0] + @width - line_width
  when :justify
    if @direction == :ltr
      x = @at[0]
    else
      x = @at[0] + @width - line_width
    end
  end

  x += accumulated_width

  y = @at[1] + @baseline_y

  y += fragment.y_offset

  fragment.left = x
  fragment.baseline = y

  if @inked
    draw_fragment_underlays(fragment)

    @document.word_spacing(word_spacing) {
      if @draw_text_callback
        @draw_text_callback.call(...)
      else
        @document.draw_text!(...)
      end
    }

    draw_fragment_overlays(fragment)
  end
end  

This method adjusts the position of each fragment taking alignment into account. We will have to adjust the x offset for each fragment left or right to properly fit it in the cell if the rotation angle is not divisible by 90 degrees.

Where's the Table?

Notice we haven't looked at Table specific code. This is all code for formatted text boxes, not table cells. So what gives?

Let's take it from the top. Given a Prawn document (pdf) and a two dimensional array of data, we typically create a table with:

pdf.table data, header: true, cell_style: {align: :left}  

This Prawn::Document#table call is in lib/prawn/table.rb:29:

def table(data, options={}, &block)  
  t = Table.new(data, self, options, &block)
  t.draw
  t
end  

The Prawn::Table::initialize method is a few lines down:

def initialize(data, document, options={}, &block)  
  @pdf = document
  @cells = make_cells(data)
  ...

Inside make_cells it iterates through the data array and calls Cell.make for each entry:

cell = Cell.make(@pdf, cell_data)  

Cell::make is, predictably, inside Prawn::Table::Cell, which creates the correct cell type depending on the content:

case content  
when Prawn::Table::Cell  
  content
when String  
  Cell::Text.new(pdf, at, options)
when Prawn::Table  
  Cell::Subtable.new(...)
when Array  
  subtable = Prawn::Table.new(...)
  Cell::Subtable.new(...)
else  
  raise Errors::UnrecognizedTableContent
end  

If we look at the Prawn::Table::Cell::Text::initialize method, we don't see anything significant. So the code is more implicit. What we are looking for doesn't happen when the cell is created, but when it is rendered. (Insert a lot of digging here.) Looking inside Prawn::Table::Cell we find a draw_content abstract method:

def draw_content  
  raise NotImplementedError, "subclasses must implement draw_content"
end  

A little searching shows that all cell types (image, subtable, and text) implement this method. In the Text#draw_content method, it calls text_box, which has what we are looking for:

def text_box(extra_options={})  
  if p = @text_options[:inline_format]
    ....
    ::Prawn::Text::Formatted::Box.new(...)
  else
    ::Prawn::Text::Box.new(...)
  end
end  

So the Table uses a default text box for a string cell, using a formatted text box if inline formatting is required. We need a text box specific to the needs of a table.

The Plan

So we need to override the following functions:

  • render_rotate
  • available_width
  • draw_fragment

All of these are in Prawn::Text::Formatted::Box. We will create a new Prawn::Table::Cell::Formatted::Box which will inherit from the text box class, overriding these methods.

It turns out that Prawn::Text::Box inherits from the formatted box. So we will create a corresponding Prawn::Table::Cell::Box class too.

In the next post we will start modifying the code to allow text at an angle inside a table cell.


Prawn: Turning Tables:

comments powered by Disqus