PageRenderTime 79ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/prawn/table/cell.rb

https://github.com/seyfcom/prawn
Ruby | 395 lines | 202 code | 57 blank | 136 comment | 14 complexity | 9df7c84294fef9ee82a50ecdaa72b65e MD5 | raw file
  1. # encoding: utf-8
  2. # cell.rb: Table cell drawing.
  3. #
  4. # Copyright December 2009, Gregory Brown and Brad Ediger. All Rights Reserved.
  5. #
  6. # This is free software. Please see the LICENSE and COPYING files for details.
  7. module Prawn
  8. class Document
  9. # Instantiates and draws a cell on the document.
  10. #
  11. # cell(:content => "Hello world!", :at => [12, 34])
  12. #
  13. # See Prawn::Table::Cell.make for full options.
  14. #
  15. def cell(options={})
  16. cell = Table::Cell.make(self, options.delete(:content), options)
  17. cell.draw
  18. cell
  19. end
  20. # Set up, but do not draw, a cell. Useful for creating cells with
  21. # formatting options to be inserted into a Table. Call +draw+ on the
  22. # resulting Cell to ink it.
  23. #
  24. # See the documentation on Prawn::Cell for details on the arguments.
  25. #
  26. def make_cell(content, options={})
  27. Prawn::Table::Cell.make(self, content, options)
  28. end
  29. end
  30. class Table
  31. # A Cell is a rectangular area of the page into which content is drawn. It
  32. # has a framework for sizing itself and adding padding and simple styling.
  33. # There are several standard Cell subclasses that handle things like text,
  34. # Tables, and (in the future) stamps, images, and arbitrary content.
  35. #
  36. # Cells are a basic building block for table support (see Prawn::Table).
  37. #
  38. # Please subclass me if you want new content types! I'm designed to be very
  39. # extensible. See the different standard Cell subclasses in
  40. # lib/prawn/table/cell/*.rb for a template.
  41. #
  42. class Cell
  43. # Amount of dead space (in PDF points) inside the borders but outside the
  44. # content. Padding defaults to 5pt.
  45. #
  46. attr_reader :padding
  47. # If provided, the minimum width that this cell will permit.
  48. #
  49. def min_width
  50. set_width_constraints
  51. @min_width
  52. end
  53. # If provided, the maximum width that this cell can be drawn in.
  54. #
  55. def max_width
  56. set_width_constraints
  57. @max_width
  58. end
  59. # Manually specify the cell's height.
  60. #
  61. attr_writer :height
  62. # Specifies which borders to enable. Must be an array of zero or more of:
  63. # <tt>[:left, :right, :top, :bottom]</tt>.
  64. #
  65. attr_accessor :borders
  66. # Specifies the width, in PDF points, of the cell's borders.
  67. #
  68. attr_accessor :border_width
  69. # Specifies the color of the cell borders. Given in HTML RGB format, e.g.,
  70. # "ccffff".
  71. #
  72. attr_accessor :border_color
  73. # Specifies the content for the cell. Must be a "cellable" object. See the
  74. # "Data" section of the Prawn::Table documentation for details on cellable
  75. # objects.
  76. #
  77. attr_accessor :content
  78. # The background color, if any, for this cell. Specified in HTML RGB
  79. # format, e.g., "ccffff". The background is drawn under the whole cell,
  80. # including any padding.
  81. #
  82. attr_accessor :background_color
  83. # Instantiates a Cell based on the given options. The particular class of
  84. # cell returned depends on the :content argument. See the Prawn::Table
  85. # documentation under "Data" for allowable content types.
  86. #
  87. def self.make(pdf, content, options={})
  88. at = options.delete(:at) || [0, pdf.cursor]
  89. content = "" if content.nil?
  90. if content.is_a?(Hash)
  91. options.update(content)
  92. content = options[:content]
  93. else
  94. options[:content] = content
  95. end
  96. case content
  97. when Prawn::Table::Cell
  98. content
  99. when String
  100. Cell::Text.new(pdf, at, options)
  101. when Prawn::Table
  102. Cell::Subtable.new(pdf, at, options)
  103. when Array
  104. subtable = Prawn::Table.new(options[:content], pdf, {})
  105. Cell::Subtable.new(pdf, at, options.merge(:content => subtable))
  106. else
  107. # TODO: other types of content
  108. raise ArgumentError, "Content type not recognized: #{content.inspect}"
  109. end
  110. end
  111. # A small amount added to the bounding box width to cover over floating-
  112. # point errors when round-tripping from content_width to width and back.
  113. # This does not change cell positioning; it only slightly expands each
  114. # cell's bounding box width so that rounding error does not prevent a cell
  115. # from rendering.
  116. #
  117. FPTolerance = 1
  118. # Sets up a cell on the document +pdf+, at the given x/y location +point+,
  119. # with the given +options+. Cell, like Table, follows the "options set
  120. # accessors" paradigm (see "Options" under the Table documentation), so
  121. # any cell accessor <tt>cell.foo = :bar</tt> can be set by providing the
  122. # option <tt>:foo => :bar</tt> here.
  123. #
  124. def initialize(pdf, point, options={})
  125. @pdf = pdf
  126. @point = point
  127. # Set defaults; these can be changed by options
  128. @padding = [5, 5, 5, 5]
  129. @borders = [:top, :bottom, :left, :right]
  130. @border_width = 1
  131. @border_color = '000000'
  132. options.each { |k, v| send("#{k}=", v) }
  133. end
  134. # Supports setting multiple properties at once.
  135. #
  136. # cell.style(:padding => 0, :border_width => 2)
  137. #
  138. # is the same as:
  139. #
  140. # cell.padding = 0
  141. # cell.border_width = 2
  142. #
  143. def style(options={}, &block)
  144. options.each { |k, v| send("#{k}=", v) }
  145. # The block form supports running a single block for multiple cells, as
  146. # in Cells#style.
  147. block.call(self) if block
  148. end
  149. # Returns the cell's width in points, inclusive of padding.
  150. #
  151. def width
  152. # We can't ||= here because the FP error accumulates on the round-trip
  153. # from #content_width.
  154. @width || (content_width + padding_left + padding_right)
  155. end
  156. # Manually sets the cell's width, inclusive of padding.
  157. #
  158. def width=(w)
  159. @width = @min_width = @max_width = w
  160. end
  161. # Returns the width of the bare content in the cell, excluding padding.
  162. #
  163. def content_width
  164. if @width # manually set
  165. return @width - padding_left - padding_right
  166. end
  167. natural_content_width
  168. end
  169. # Returns the width this cell would naturally take on, absent other
  170. # constraints. Must be implemented in subclasses.
  171. #
  172. def natural_content_width
  173. raise NotImplementedError,
  174. "subclasses must implement natural_content_width"
  175. end
  176. # Returns the cell's height in points, inclusive of padding.
  177. #
  178. def height
  179. # We can't ||= here because the FP error accumulates on the round-trip
  180. # from #content_height.
  181. @height || (content_height + padding_top + padding_bottom)
  182. end
  183. # Returns the height of the bare content in the cell, excluding padding.
  184. #
  185. def content_height
  186. if @height # manually set
  187. return @height - padding_top - padding_bottom
  188. end
  189. natural_content_height
  190. end
  191. # Returns the height this cell would naturally take on, absent
  192. # constraints. Must be implemented in subclasses.
  193. #
  194. def natural_content_height
  195. raise NotImplementedError,
  196. "subclasses must implement natural_content_height"
  197. end
  198. # Draws the cell onto the document. Pass in a point [x,y] to override the
  199. # location at which the cell is drawn.
  200. #
  201. def draw(pt=[x, y])
  202. set_width_constraints
  203. draw_background(pt)
  204. draw_borders(pt)
  205. @pdf.bounding_box([pt[0] + padding_left, pt[1] - padding_top],
  206. :width => content_width + FPTolerance,
  207. :height => content_height + FPTolerance) do
  208. draw_content
  209. end
  210. end
  211. # x-position of the cell within the parent bounds.
  212. #
  213. def x
  214. @point[0]
  215. end
  216. # Set the x-position of the cell within the parent bounds.
  217. #
  218. def x=(val)
  219. @point[0] = val
  220. end
  221. # y-position of the cell within the parent bounds.
  222. #
  223. def y
  224. @point[1]
  225. end
  226. # Set the y-position of the cell within the parent bounds.
  227. #
  228. def y=(val)
  229. @point[1] = val
  230. end
  231. # Sets padding on this cell. The argument can be one of:
  232. #
  233. # * an integer (sets all padding)
  234. # * a two-element array [vertical, horizontal]
  235. # * a three-element array [top, horizontal, bottom]
  236. # * a four-element array [top, right, bottom, left]
  237. #
  238. def padding=(pad)
  239. @padding = case
  240. when pad.nil?
  241. [0, 0, 0, 0]
  242. when Numeric === pad # all padding
  243. [pad, pad, pad, pad]
  244. when pad.length == 2 # vert, horiz
  245. [pad[0], pad[1], pad[0], pad[1]]
  246. when pad.length == 3 # top, horiz, bottom
  247. [pad[0], pad[1], pad[2], pad[1]]
  248. when pad.length == 4 # top, right, bottom, left
  249. [pad[0], pad[1], pad[2], pad[3]]
  250. else
  251. raise ArgumentError, ":padding must be a number or an array [v,h] " +
  252. "or [t,r,b,l]"
  253. end
  254. end
  255. protected
  256. # Sets the cell's minimum and maximum width. Deferred until requested
  257. # because padding and size can change.
  258. #
  259. def set_width_constraints
  260. @min_width ||= padding_left + padding_right
  261. @max_width ||= @pdf.bounds.width
  262. end
  263. # Draws the cell's background color.
  264. #
  265. def draw_background(pt)
  266. x, y = pt
  267. margin = @border_width / 2
  268. if @background_color
  269. @pdf.mask(:fill_color) do
  270. @pdf.fill_color @background_color
  271. h = @borders.include?(:bottom) ? height - (2*margin) :
  272. height + margin
  273. @pdf.fill_rectangle [x, y], width, h
  274. end
  275. end
  276. end
  277. # Draws borders around the cell. Borders are centered on the bounds of
  278. # the cell outside of any padding, so the caller is responsible for
  279. # setting appropriate padding to ensure the border does not overlap with
  280. # cell content.
  281. #
  282. def draw_borders(pt)
  283. x, y = pt
  284. return if @border_width <= 0
  285. # Draw left / right borders one-half border width beyond the center of
  286. # the corner, so that the corners end up square.
  287. margin = @border_width / 2.0
  288. @pdf.mask(:line_width, :stroke_color) do
  289. @pdf.line_width = @border_width
  290. @pdf.stroke_color = @border_color if @border_color
  291. @borders.each do |border|
  292. from, to = case border
  293. when :top
  294. [[x, y], [x+width, y]]
  295. when :bottom
  296. [[x, y-height], [x+width, y-height]]
  297. when :left
  298. [[x, y+margin], [x, y-height-margin]]
  299. when :right
  300. [[x+width, y+margin], [x+width, y-height-margin]]
  301. end
  302. @pdf.stroke_line(from, to)
  303. end
  304. end
  305. end
  306. # Draws cell content within the cell's bounding box. Must be implemented
  307. # in subclasses.
  308. #
  309. def draw_content
  310. raise NotImplementedError, "subclasses must implement draw_content"
  311. end
  312. def padding_top
  313. @padding[0]
  314. end
  315. def padding_top=(val)
  316. @padding[0] = val
  317. end
  318. def padding_right
  319. @padding[1]
  320. end
  321. def padding_right=(val)
  322. @padding[1] = val
  323. end
  324. def padding_bottom
  325. @padding[2]
  326. end
  327. def padding_bottom=(val)
  328. @padding[2] = val
  329. end
  330. def padding_left
  331. @padding[3]
  332. end
  333. def padding_left=(val)
  334. @padding[3] = val
  335. end
  336. end
  337. end
  338. end