# encoding: utf-8 # cell.rb: Table cell drawing. # # Copyright December 2009, Gregory Brown and Brad Ediger. All Rights Reserved. # # This is free software. Please see the LICENSE and COPYING files for details. module Prawn class Document # Instantiates and draws a cell on the document. # # cell(:content => "Hello world!", :at => [12, 34]) # # See Prawn::Table::Cell.make for full options. # def cell(options={}) cell = Table::Cell.make(self, options.delete(:content), options) cell.draw cell end # Set up, but do not draw, a cell. Useful for creating cells with # formatting options to be inserted into a Table. Call +draw+ on the # resulting Cell to ink it. # # See the documentation on Prawn::Cell for details on the arguments. # def make_cell(content, options={}) Prawn::Table::Cell.make(self, content, options) end end class Table # A Cell is a rectangular area of the page into which content is drawn. It # has a framework for sizing itself and adding padding and simple styling. # There are several standard Cell subclasses that handle things like text, # Tables, and (in the future) stamps, images, and arbitrary content. # # Cells are a basic building block for table support (see Prawn::Table). # # Please subclass me if you want new content types! I'm designed to be very # extensible. See the different standard Cell subclasses in # lib/prawn/table/cell/*.rb for a template. # class Cell # Amount of dead space (in PDF points) inside the borders but outside the # content. Padding defaults to 5pt. # attr_reader :padding # If provided, the minimum width that this cell will permit. # attr_reader :min_width # If provided, the maximum width that this cell can be drawn in. # attr_reader :max_width # Manually specify the cell's height. # attr_writer :height # Specifies which borders to enable. Must be an array of zero or more of: # [:left, :right, :top, :bottom]. # attr_accessor :borders # Specifies the width, in PDF points, of the cell's borders. # attr_accessor :border_width # Specifies the color of the cell borders. Given in HTML RGB format, e.g., # "ccffff". # attr_accessor :border_color # Specifies the content for the cell. Must be a "cellable" object. See the # "Data" section of the Prawn::Table documentation for details on cellable # objects. # attr_accessor :content # The background color, if any, for this cell. Specified in HTML RGB # format, e.g., "ccffff". The background is drawn under the whole cell, # including any padding. # attr_accessor :background_color # Instantiates a Cell based on the given options. The particular class of # cell returned depends on the :content argument. See the Prawn::Table # documentation under "Data" for allowable content types. # def self.make(pdf, content, options={}) at = options.delete(:at) || [0, pdf.cursor] content = "" if content.nil? options[:content] = content case content when Prawn::Table::Cell content when String Cell::Text.new(pdf, at, options) when Prawn::Table Cell::Subtable.new(pdf, at, options) when Array subtable = Prawn::Table.new(options[:content], pdf, {}) Cell::Subtable.new(pdf, at, options.merge(:content => subtable)) else # TODO: other types of content raise ArgumentError, "Content type not recognized: #{content.inspect}" end end # A small amount added to the bounding box width to cover over floating- # point errors when round-tripping from content_width to width and back. # This does not change cell positioning; it only slightly expands each # cell's bounding box width so that rounding error does not prevent a cell # from rendering. # FPTolerance = 1 # Sets up a cell on the document +pdf+, at the given x/y location +point+, # with the given +options+. Cell, like Table, follows the "options set # accessors" paradigm (see "Options" under the Table documentation), so # any cell accessor cell.foo = :bar can be set by providing the # option :foo => :bar here. # def initialize(pdf, point, options={}) @pdf = pdf @point = point # Set defaults; these can be changed by options @padding = [5, 5, 5, 5] @borders = [:top, :bottom, :left, :right] @border_width = 1 @border_color = '000000' options.each { |k, v| send("#{k}=", v) } # Sensible defaults for min / max. @min_width = padding_left + padding_right @max_width = @pdf.bounds.width end # Returns the cell's width in points, inclusive of padding. # def width # We can't ||= here because the FP error accumulates on the round-trip # from #content_width. @width || (content_width + padding_left + padding_right) end # Manually sets the cell's width, inclusive of padding. # def width=(w) @width = @min_width = @max_width = w end # Returns the width of the bare content in the cell, excluding padding. # def content_width if @width # manually set return @width - padding_left - padding_right end natural_content_width end # Returns the width this cell would naturally take on, absent other # constraints. Must be implemented in subclasses. # def natural_content_width raise NotImplementedError, "subclasses must implement natural_content_width" end # Returns the cell's height in points, inclusive of padding. # def height # We can't ||= here because the FP error accumulates on the round-trip # from #content_height. @height || (content_height + padding_top + padding_bottom) end # Returns the height of the bare content in the cell, excluding padding. # def content_height if @height # manually set return @height - padding_top - padding_bottom end natural_content_height end # Returns the height this cell would naturally take on, absent # constraints. Must be implemented in subclasses. # def natural_content_height raise NotImplementedError, "subclasses must implement natural_content_height" end # Draws the cell onto the document. Pass in a point [x,y] to override the # location at which the cell is drawn. # def draw(pt=[x, y]) draw_background(pt) @pdf.bounding_box([pt[0] + padding_left, pt[1] - padding_top], :width => content_width + FPTolerance, :height => content_height + FPTolerance) do draw_content end draw_borders(pt) end # x-position of the cell within the parent bounds. # def x @point[0] end # Set the x-position of the cell within the parent bounds. # def x=(val) @point[0] = val end # y-position of the cell within the parent bounds. # def y @point[1] end # Set the y-position of the cell within the parent bounds. # def y=(val) @point[1] = val end # Sets padding on this cell. The argument can be one of: # # * an integer (sets all padding) # * a two-element array [vertical, horizontal] # * a three-element array [top, horizontal, bottom] # * a four-element array [top, right, bottom, left] # def padding=(pad) @padding = case when pad.nil? [0, 0, 0, 0] when Numeric === pad # all padding [pad, pad, pad, pad] when pad.length == 2 # vert, horiz [pad[0], pad[1], pad[0], pad[1]] when pad.length == 3 # top, horiz, bottom [pad[0], pad[1], pad[2], pad[1]] when pad.length == 4 # top, right, bottom, left [pad[0], pad[1], pad[2], pad[3]] else raise ArgumentError, ":padding must be a number or an array [v,h] " + "or [t,r,b,l]" end end protected # Draws the cell's background color. # def draw_background(pt) x, y = pt margin = @border_width / 2 if @background_color @pdf.mask(:fill_color) do @pdf.fill_color @background_color h = @borders.include?(:bottom) ? height - (2*margin) : height + margin @pdf.fill_rectangle [x, y], width, h end end end # Draws borders around the cell. Borders are centered on the bounds of # the cell outside of any padding, so the caller is responsible for # setting appropriate padding to ensure the border does not overlap with # cell content. # def draw_borders(pt) x, y = pt return if @border_width <= 0 # Draw left / right borders one-half border width beyond the center of # the corner, so that the corners end up square. margin = @border_width / 2.0 @pdf.mask(:line_width, :stroke_color) do @pdf.line_width = @border_width @pdf.stroke_color = @border_color if @border_color @borders.each do |border| from, to = case border when :top [[x, y], [x+width, y]] when :bottom [[x, y-height], [x+width, y-height]] when :left [[x, y+margin], [x, y-height-margin]] when :right [[x+width, y+margin], [x+width, y-height-margin]] end @pdf.stroke_line(from, to) end end end # Draws cell content within the cell's bounding box. Must be implemented # in subclasses. # def draw_content raise NotImplementedError, "subclasses must implement draw_content" end def padding_top @padding[0] end def padding_top=(val) @padding[0] = val end def padding_right @padding[1] end def padding_right=(val) @padding[1] = val end def padding_bottom @padding[2] end def padding_bottom=(val) @padding[2] = val end def padding_left @padding[3] end def padding_left=(val) @padding[3] = val end end end end