prawn-table-continued 1.0.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +2 -0
  3. data/GPLv2 +340 -0
  4. data/GPLv3 +674 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE +56 -0
  7. data/lib/prawn/table/cell/image.rb +69 -0
  8. data/lib/prawn/table/cell/in_table.rb +33 -0
  9. data/lib/prawn/table/cell/span_dummy.rb +93 -0
  10. data/lib/prawn/table/cell/subtable.rb +66 -0
  11. data/lib/prawn/table/cell/text.rb +155 -0
  12. data/lib/prawn/table/cell.rb +787 -0
  13. data/lib/prawn/table/cells.rb +261 -0
  14. data/lib/prawn/table/column_width_calculator.rb +182 -0
  15. data/lib/prawn/table/version.rb +5 -0
  16. data/lib/prawn/table.rb +711 -0
  17. data/manual/contents.rb +13 -0
  18. data/manual/example_helper.rb +8 -0
  19. data/manual/images/prawn.png +0 -0
  20. data/manual/images/stef.jpg +0 -0
  21. data/manual/table/basic_block.rb +53 -0
  22. data/manual/table/before_rendering_page.rb +26 -0
  23. data/manual/table/cell_border_lines.rb +24 -0
  24. data/manual/table/cell_borders_and_bg.rb +31 -0
  25. data/manual/table/cell_dimensions.rb +36 -0
  26. data/manual/table/cell_text.rb +38 -0
  27. data/manual/table/column_widths.rb +30 -0
  28. data/manual/table/content_and_subtables.rb +39 -0
  29. data/manual/table/creation.rb +27 -0
  30. data/manual/table/filtering.rb +36 -0
  31. data/manual/table/flow_and_header.rb +17 -0
  32. data/manual/table/image_cells.rb +33 -0
  33. data/manual/table/position.rb +29 -0
  34. data/manual/table/row_colors.rb +20 -0
  35. data/manual/table/span.rb +30 -0
  36. data/manual/table/style.rb +33 -0
  37. data/manual/table/table.rb +52 -0
  38. data/manual/table/width.rb +27 -0
  39. data/prawn-table.gemspec +36 -0
  40. data/spec/cell_spec.rb +652 -0
  41. data/spec/extensions/encoding_helpers.rb +11 -0
  42. data/spec/extensions/file_fixture_helper.rb +15 -0
  43. data/spec/fixtures/files/prawn.png +0 -0
  44. data/spec/spec_helper.rb +50 -0
  45. data/spec/table/span_dummy_spec.rb +26 -0
  46. data/spec/table_spec.rb +1626 -0
  47. metadata +234 -0
@@ -0,0 +1,787 @@
1
+ # encoding: utf-8
2
+
3
+ # cell.rb: Table cell drawing.
4
+ #
5
+ # Copyright December 2009, Gregory Brown and Brad Ediger. All Rights Reserved.
6
+ #
7
+ # This is free software. Please see the LICENSE and COPYING files for details.
8
+
9
+ require 'date'
10
+ module Prawn
11
+ class Document
12
+
13
+ # @group Experimental API
14
+
15
+ # Instantiates and draws a cell on the document.
16
+ #
17
+ # cell(:content => "Hello world!", :at => [12, 34])
18
+ #
19
+ # See Prawn::Table::Cell.make for full options.
20
+ #
21
+ def cell(options={})
22
+ cell = Table::Cell.make(self, options.delete(:content), options)
23
+ cell.draw
24
+ cell
25
+ end
26
+
27
+ # Set up, but do not draw, a cell. Useful for creating cells with
28
+ # formatting options to be inserted into a Table. Call +draw+ on the
29
+ # resulting Cell to ink it.
30
+ #
31
+ # See the documentation on Prawn::Cell for details on the arguments.
32
+ #
33
+ def make_cell(content, options={})
34
+ Prawn::Table::Cell.make(self, content, options)
35
+ end
36
+
37
+ end
38
+
39
+ class Table
40
+
41
+ # A Cell is a rectangular area of the page into which content is drawn. It
42
+ # has a framework for sizing itself and adding padding and simple styling.
43
+ # There are several standard Cell subclasses that handle things like text,
44
+ # Tables, and (in the future) stamps, images, and arbitrary content.
45
+ #
46
+ # Cells are a basic building block for table support (see Prawn::Table).
47
+ #
48
+ # Please subclass me if you want new content types! I'm designed to be very
49
+ # extensible. See the different standard Cell subclasses in
50
+ # lib/prawn/table/cell/*.rb for a template.
51
+ #
52
+ class Cell
53
+
54
+ # Amount of dead space (in PDF points) inside the borders but outside the
55
+ # content. Padding defaults to 5pt.
56
+ #
57
+ attr_reader :padding
58
+
59
+ # If provided, the minimum width that this cell in its column will permit.
60
+ #
61
+ def min_width_ignoring_span
62
+ set_width_constraints
63
+ @min_width
64
+ end
65
+
66
+ # Minimum width of the entire span group this cell controls.
67
+ #
68
+ def min_width
69
+ return min_width_ignoring_span if @colspan == 1
70
+
71
+ # Sum up the largest min-width from each column, including myself.
72
+ min_widths = Hash.new(0)
73
+ dummy_cells.each do |cell|
74
+ min_widths[cell.column] =
75
+ [min_widths[cell.column], cell.min_width].max
76
+ end
77
+ min_widths[column] = [min_widths[column], min_width_ignoring_span].max
78
+ min_widths.values.inject(0, &:+)
79
+ end
80
+
81
+ # Min-width of the span divided by the number of columns.
82
+ #
83
+ def avg_spanned_min_width
84
+ min_width.to_f / colspan
85
+ end
86
+
87
+ # If provided, the maximum width that this cell can be drawn in, within
88
+ # its column.
89
+ #
90
+ def max_width_ignoring_span
91
+ set_width_constraints
92
+ @max_width
93
+ end
94
+
95
+ # Maximum width of the entire span group this cell controls.
96
+ #
97
+ def max_width
98
+ return max_width_ignoring_span if @colspan == 1
99
+
100
+ # Sum the smallest max-width from each column in the group, including
101
+ # myself.
102
+ max_widths = Hash.new(0)
103
+ dummy_cells.each do |cell|
104
+ max_widths[cell.column] =
105
+ [max_widths[cell.column], cell.max_width].min
106
+ end
107
+ max_widths[column] = [max_widths[column], max_width_ignoring_span].min
108
+ max_widths.values.inject(0, &:+)
109
+ end
110
+
111
+ # Manually specify the cell's height.
112
+ #
113
+ attr_writer :height
114
+
115
+ # Specifies which borders to enable. Must be an array of zero or more of:
116
+ # <tt>[:left, :right, :top, :bottom]</tt>.
117
+ #
118
+ attr_accessor :borders
119
+
120
+ # Width, in PDF points, of the cell's borders: [top, right, bottom, left].
121
+ #
122
+ attr_reader :border_widths
123
+
124
+ # HTML RGB-format ("ccffff") border colors: [top, right, bottom, left].
125
+ #
126
+ attr_reader :border_colors
127
+
128
+ # Line style
129
+ #
130
+ attr_reader :border_lines
131
+
132
+ # Specifies the content for the cell. Must be a "cellable" object. See the
133
+ # "Data" section of the Prawn::Table documentation for details on cellable
134
+ # objects.
135
+ #
136
+ attr_accessor :content
137
+
138
+ # The background color, if any, for this cell. Specified in HTML RGB
139
+ # format, e.g., "ccffff". The background is drawn under the whole cell,
140
+ # including any padding.
141
+ #
142
+ attr_accessor :background_color
143
+
144
+ # Number of columns this cell spans. Defaults to 1.
145
+ #
146
+ attr_reader :colspan
147
+
148
+ # Number of rows this cell spans. Defaults to 1.
149
+ #
150
+ attr_reader :rowspan
151
+
152
+ # Array of SpanDummy cells (if any) that represent the other cells in
153
+ # this span group. They know their own width / height, but do not draw
154
+ # anything.
155
+ #
156
+ attr_reader :dummy_cells
157
+
158
+ # Instantiates a Cell based on the given options. The particular class of
159
+ # cell returned depends on the :content argument. See the Prawn::Table
160
+ # documentation under "Data" for allowable content types.
161
+ #
162
+ def self.make(pdf, content, options={})
163
+ at = options.delete(:at) || [0, pdf.cursor]
164
+
165
+ return Cell::Image.new(pdf, at, content) if content.is_a?(Hash) && content[:image]
166
+
167
+ if content.is_a?(Hash)
168
+ options.update(content)
169
+ content = options[:content]
170
+ end
171
+
172
+ content = content.to_s if stringify_content?(content)
173
+ options[:content] = content
174
+
175
+ case content
176
+ when Prawn::Table::Cell
177
+ content
178
+ when String
179
+ Cell::Text.new(pdf, at, options)
180
+ when Prawn::Table
181
+ Cell::Subtable.new(pdf, at, options)
182
+ when Array
183
+ subtable = Prawn::Table.new(options[:content], pdf, {})
184
+ Cell::Subtable.new(pdf, at, options.merge(:content => subtable))
185
+ else
186
+ raise Errors::UnrecognizedTableContent
187
+ end
188
+ end
189
+
190
+ def self.stringify_content?(content)
191
+ return true if content.nil?
192
+ return true if content.kind_of?(Numeric)
193
+ return true if content.kind_of?(Date)
194
+ return true if content.kind_of?(Time)
195
+
196
+ false
197
+ end
198
+
199
+ # A small amount added to the bounding box width to cover over floating-
200
+ # point errors when round-tripping from content_width to width and back.
201
+ # This does not change cell positioning; it only slightly expands each
202
+ # cell's bounding box width so that rounding error does not prevent a cell
203
+ # from rendering.
204
+ #
205
+ FPTolerance = 1
206
+
207
+ # Sets up a cell on the document +pdf+, at the given x/y location +point+,
208
+ # with the given +options+. Cell, like Table, follows the "options set
209
+ # accessors" paradigm (see "Options" under the Table documentation), so
210
+ # any cell accessor <tt>cell.foo = :bar</tt> can be set by providing the
211
+ # option <tt>:foo => :bar</tt> here.
212
+ #
213
+ def initialize(pdf, point, options={})
214
+ @pdf = pdf
215
+ @point = point
216
+
217
+ # Set defaults; these can be changed by options
218
+ @padding = [5, 5, 5, 5]
219
+ @borders = [:top, :bottom, :left, :right]
220
+ @border_widths = [1] * 4
221
+ @border_colors = ['000000'] * 4
222
+ @border_lines = [:solid] * 4
223
+ @colspan = 1
224
+ @rowspan = 1
225
+ @dummy_cells = []
226
+
227
+ style(options)
228
+
229
+ @initializer_run = true
230
+ end
231
+
232
+ # Supports setting multiple properties at once.
233
+ #
234
+ # cell.style(:padding => 0, :border_width => 2)
235
+ #
236
+ # is the same as:
237
+ #
238
+ # cell.padding = 0
239
+ # cell.border_width = 2
240
+ #
241
+ def style(options={}, &block)
242
+ options.each do |k, v|
243
+ send("#{k}=", v) if respond_to?("#{k}=")
244
+ end
245
+
246
+ # The block form supports running a single block for multiple cells, as
247
+ # in Cells#style.
248
+ block.call(self) if block
249
+ end
250
+
251
+ # Returns the width of the cell in its first column alone, ignoring any
252
+ # colspans.
253
+ #
254
+ def width_ignoring_span
255
+ # We can't ||= here because the FP error accumulates on the round-trip
256
+ # from #content_width.
257
+ defined?(@width) && @width || (content_width + padding_left + padding_right)
258
+ end
259
+
260
+ # Returns the cell's width in points, inclusive of padding. If the cell is
261
+ # the master cell of a colspan, returns the width of the entire span
262
+ # group.
263
+ #
264
+ def width
265
+ return width_ignoring_span if @colspan == 1 && @rowspan == 1
266
+
267
+ # We're in a span group; get the maximum width per column (including
268
+ # the master cell) and sum each column.
269
+ column_widths = Hash.new(0)
270
+ dummy_cells.each do |cell|
271
+ column_widths[cell.column] =
272
+ [column_widths[cell.column], cell.width].max
273
+ end
274
+ column_widths[column] = [column_widths[column], width_ignoring_span].max
275
+ column_widths.values.inject(0, &:+)
276
+ end
277
+
278
+ # Manually sets the cell's width, inclusive of padding.
279
+ #
280
+ def width=(w)
281
+ @width = @min_width = @max_width = w
282
+ end
283
+
284
+ # Returns the width of the bare content in the cell, excluding padding.
285
+ #
286
+ def content_width
287
+ if defined?(@width) && @width # manually set
288
+ return @width - padding_left - padding_right
289
+ end
290
+
291
+ natural_content_width
292
+ end
293
+
294
+ # Width of the entire span group.
295
+ #
296
+ def spanned_content_width
297
+ width - padding_left - padding_right
298
+ end
299
+
300
+ # Returns the width this cell would naturally take on, absent other
301
+ # constraints. Must be implemented in subclasses.
302
+ #
303
+ def natural_content_width
304
+ raise NotImplementedError,
305
+ "subclasses must implement natural_content_width"
306
+ end
307
+
308
+ # Returns the cell's height in points, inclusive of padding, in its first
309
+ # row only.
310
+ #
311
+ def height_ignoring_span
312
+ # We can't ||= here because the FP error accumulates on the round-trip
313
+ # from #content_height.
314
+ defined?(@height) && @height || (content_height + padding_top + padding_bottom)
315
+ end
316
+
317
+ # Returns the cell's height in points, inclusive of padding. If the cell
318
+ # is the master cell of a rowspan, returns the width of the entire span
319
+ # group.
320
+ #
321
+ def height
322
+ return height_ignoring_span if @colspan == 1 && @rowspan == 1
323
+
324
+ # We're in a span group; get the maximum height per row (including the
325
+ # master cell) and sum each row.
326
+ row_heights = Hash.new(0)
327
+ dummy_cells.each do |cell|
328
+ row_heights[cell.row] = [row_heights[cell.row], cell.height].max
329
+ end
330
+ row_heights[row] = [row_heights[row], height_ignoring_span].max
331
+ row_heights.values.inject(0, &:+)
332
+ end
333
+
334
+ # Returns the height of the bare content in the cell, excluding padding.
335
+ #
336
+ def content_height
337
+ if defined?(@height) && @height # manually set
338
+ return @height - padding_top - padding_bottom
339
+ end
340
+
341
+ natural_content_height
342
+ end
343
+
344
+ # Height of the entire span group.
345
+ #
346
+ def spanned_content_height
347
+ height - padding_top - padding_bottom
348
+ end
349
+
350
+ # Returns the height this cell would naturally take on, absent
351
+ # constraints. Must be implemented in subclasses.
352
+ #
353
+ def natural_content_height
354
+ raise NotImplementedError,
355
+ "subclasses must implement natural_content_height"
356
+ end
357
+
358
+ # Indicates the number of columns that this cell is to span. Defaults to
359
+ # 1.
360
+ #
361
+ # This must be provided as part of the table data, like so:
362
+ #
363
+ # pdf.table([["foo", {:content => "bar", :colspan => 2}]])
364
+ #
365
+ # Setting colspan from the initializer block is invalid because layout
366
+ # has already run. For example, this will NOT work:
367
+ #
368
+ # pdf.table([["foo", "bar"]]) { cells[0, 1].colspan = 2 }
369
+ #
370
+ def colspan=(span)
371
+ if defined?(@initializer_run) && @initializer_run
372
+ raise Prawn::Errors::InvalidTableSpan,
373
+ "colspan must be provided in the table's structure, never in the " +
374
+ "initialization block. See Prawn's documentation for details."
375
+ end
376
+
377
+ @colspan = span
378
+ end
379
+
380
+ # Indicates the number of rows that this cell is to span. Defaults to 1.
381
+ #
382
+ # This must be provided as part of the table data, like so:
383
+ #
384
+ # pdf.table([["foo", {:content => "bar", :rowspan => 2}], ["baz"]])
385
+ #
386
+ # Setting rowspan from the initializer block is invalid because layout
387
+ # has already run. For example, this will NOT work:
388
+ #
389
+ # pdf.table([["foo", "bar"], ["baz"]]) { cells[0, 1].rowspan = 2 }
390
+ #
391
+ def rowspan=(span)
392
+ if defined?(@initializer_run) && @initializer_run
393
+ raise Prawn::Errors::InvalidTableSpan,
394
+ "rowspan must be provided in the table's structure, never in the " +
395
+ "initialization block. See Prawn's documentation for details."
396
+ end
397
+
398
+ @rowspan = span
399
+ end
400
+
401
+ # Draws the cell onto the document. Pass in a point [x,y] to override the
402
+ # location at which the cell is drawn.
403
+ #
404
+ # If drawing a group of cells at known positions, look into
405
+ # Cell.draw_cells, which ensures that the backgrounds, borders, and
406
+ # content are all drawn in correct order so as not to overlap.
407
+ #
408
+ def draw(pt=[x, y])
409
+ Prawn::Table::Cell.draw_cells([[self, pt]])
410
+ end
411
+
412
+ # Given an array of pairs [cell, pt], draws each cell at its
413
+ # corresponding pt, making sure all backgrounds are behind all borders
414
+ # and content.
415
+ #
416
+ def self.draw_cells(cells)
417
+ cells.each do |cell, pt|
418
+ cell.set_width_constraints
419
+ cell.draw_background(pt)
420
+ end
421
+
422
+ cells.each do |cell, pt|
423
+ cell.draw_borders(pt)
424
+ cell.draw_bounded_content(pt)
425
+ end
426
+ end
427
+
428
+ # Draws the cell's content at the point provided.
429
+ #
430
+ def draw_bounded_content(pt)
431
+ @pdf.float do
432
+ @pdf.bounding_box([pt[0] + padding_left, pt[1] - padding_top],
433
+ :width => spanned_content_width + FPTolerance,
434
+ :height => spanned_content_height + FPTolerance) do
435
+ draw_content
436
+ end
437
+ end
438
+ end
439
+
440
+ # x-position of the cell within the parent bounds.
441
+ #
442
+ def x
443
+ @point[0]
444
+ end
445
+
446
+ # Set the x-position of the cell within the parent bounds.
447
+ #
448
+ def x=(val)
449
+ @point[0] = val
450
+ end
451
+
452
+ def relative_x
453
+ # Translate coordinates to the bounds we are in, since drawing is
454
+ # relative to the cursor, not ref_bounds.
455
+ x + @pdf.bounds.left_side - @pdf.bounds.absolute_left
456
+ end
457
+
458
+ # y-position of the cell within the parent bounds.
459
+ #
460
+ def y
461
+ @point[1]
462
+ end
463
+
464
+ # Set the y-position of the cell within the parent bounds.
465
+ #
466
+ def y=(val)
467
+ @point[1] = val
468
+ end
469
+
470
+ def relative_y(offset = 0)
471
+ y + offset - @pdf.bounds.absolute_bottom
472
+ end
473
+
474
+ # Sets padding on this cell. The argument can be one of:
475
+ #
476
+ # * an integer (sets all padding)
477
+ # * a two-element array [vertical, horizontal]
478
+ # * a three-element array [top, horizontal, bottom]
479
+ # * a four-element array [top, right, bottom, left]
480
+ #
481
+ def padding=(pad)
482
+ @padding = case
483
+ when pad.nil?
484
+ [0, 0, 0, 0]
485
+ when Numeric === pad # all padding
486
+ [pad, pad, pad, pad]
487
+ when pad.length == 2 # vert, horiz
488
+ [pad[0], pad[1], pad[0], pad[1]]
489
+ when pad.length == 3 # top, horiz, bottom
490
+ [pad[0], pad[1], pad[2], pad[1]]
491
+ when pad.length == 4 # top, right, bottom, left
492
+ [pad[0], pad[1], pad[2], pad[3]]
493
+ else
494
+ raise ArgumentError, ":padding must be a number or an array [v,h] " +
495
+ "or [t,r,b,l]"
496
+ end
497
+ end
498
+
499
+ def padding_top
500
+ @padding[0]
501
+ end
502
+
503
+ def padding_top=(val)
504
+ @padding[0] = val
505
+ end
506
+
507
+ def padding_right
508
+ @padding[1]
509
+ end
510
+
511
+ def padding_right=(val)
512
+ @padding[1] = val
513
+ end
514
+
515
+ def padding_bottom
516
+ @padding[2]
517
+ end
518
+
519
+ def padding_bottom=(val)
520
+ @padding[2] = val
521
+ end
522
+
523
+ def padding_left
524
+ @padding[3]
525
+ end
526
+
527
+ def padding_left=(val)
528
+ @padding[3] = val
529
+ end
530
+
531
+ # Sets border colors on this cell. The argument can be one of:
532
+ #
533
+ # * an integer (sets all colors)
534
+ # * a two-element array [vertical, horizontal]
535
+ # * a three-element array [top, horizontal, bottom]
536
+ # * a four-element array [top, right, bottom, left]
537
+ #
538
+ def border_color=(color)
539
+ @border_colors = case
540
+ when color.nil?
541
+ ["000000"] * 4
542
+ when String === color # all colors
543
+ [color, color, color, color]
544
+ when color.length == 2 # vert, horiz
545
+ [color[0], color[1], color[0], color[1]]
546
+ when color.length == 3 # top, horiz, bottom
547
+ [color[0], color[1], color[2], color[1]]
548
+ when color.length == 4 # top, right, bottom, left
549
+ [color[0], color[1], color[2], color[3]]
550
+ else
551
+ raise ArgumentError, ":border_color must be a string " +
552
+ "or an array [v,h] or [t,r,b,l]"
553
+ end
554
+ end
555
+ alias_method :border_colors=, :border_color=
556
+
557
+ def border_top_color
558
+ @border_colors[0]
559
+ end
560
+
561
+ def border_top_color=(val)
562
+ @border_colors[0] = val
563
+ end
564
+
565
+ def border_right_color
566
+ @border_colors[1]
567
+ end
568
+
569
+ def border_right_color=(val)
570
+ @border_colors[1] = val
571
+ end
572
+
573
+ def border_bottom_color
574
+ @border_colors[2]
575
+ end
576
+
577
+ def border_bottom_color=(val)
578
+ @border_colors[2] = val
579
+ end
580
+
581
+ def border_left_color
582
+ @border_colors[3]
583
+ end
584
+
585
+ def border_left_color=(val)
586
+ @border_colors[3] = val
587
+ end
588
+
589
+ # Sets border widths on this cell. The argument can be one of:
590
+ #
591
+ # * an integer (sets all widths)
592
+ # * a two-element array [vertical, horizontal]
593
+ # * a three-element array [top, horizontal, bottom]
594
+ # * a four-element array [top, right, bottom, left]
595
+ #
596
+ def border_width=(width)
597
+ @border_widths = case
598
+ when width.nil?
599
+ ["000000"] * 4
600
+ when Numeric === width # all widths
601
+ [width, width, width, width]
602
+ when width.length == 2 # vert, horiz
603
+ [width[0], width[1], width[0], width[1]]
604
+ when width.length == 3 # top, horiz, bottom
605
+ [width[0], width[1], width[2], width[1]]
606
+ when width.length == 4 # top, right, bottom, left
607
+ [width[0], width[1], width[2], width[3]]
608
+ else
609
+ raise ArgumentError, ":border_width must be a string " +
610
+ "or an array [v,h] or [t,r,b,l]"
611
+ end
612
+ end
613
+ alias_method :border_widths=, :border_width=
614
+
615
+ def border_top_width
616
+ @borders.include?(:top) ? @border_widths[0] : 0
617
+ end
618
+
619
+ def border_top_width=(val)
620
+ @border_widths[0] = val
621
+ end
622
+
623
+ def border_right_width
624
+ @borders.include?(:right) ? @border_widths[1] : 0
625
+ end
626
+
627
+ def border_right_width=(val)
628
+ @border_widths[1] = val
629
+ end
630
+
631
+ def border_bottom_width
632
+ @borders.include?(:bottom) ? @border_widths[2] : 0
633
+ end
634
+
635
+ def border_bottom_width=(val)
636
+ @border_widths[2] = val
637
+ end
638
+
639
+ def border_left_width
640
+ @borders.include?(:left) ? @border_widths[3] : 0
641
+ end
642
+
643
+ def border_left_width=(val)
644
+ @border_widths[3] = val
645
+ end
646
+
647
+ # Sets the cell's minimum and maximum width. Deferred until requested
648
+ # because padding and size can change.
649
+ #
650
+ def set_width_constraints
651
+ @min_width ||= padding_left + padding_right
652
+ @max_width ||= @pdf.bounds.width
653
+ end
654
+
655
+ # Sets border line style on this cell. The argument can be one of:
656
+ #
657
+ # Possible values are: :solid, :dashed, :dotted
658
+ #
659
+ # * one value (sets all lines)
660
+ # * a two-element array [vertical, horizontal]
661
+ # * a three-element array [top, horizontal, bottom]
662
+ # * a four-element array [top, right, bottom, left]
663
+ #
664
+ def border_line=(line)
665
+ @border_lines = case
666
+ when line.nil?
667
+ [:solid] * 4
668
+ when line.length == 1 # all lines
669
+ [line[0]] * 4
670
+ when line.length == 2
671
+ [line[0], line[1], line[0], line[1]]
672
+ when line.length == 3
673
+ [line[0], line[1], line[2], line[1]]
674
+ when line.length == 4
675
+ [line[0], line[1], line[2], line[3]]
676
+ else
677
+ raise ArgumentError, "border_line must be one of :solid, :dashed, "
678
+ ":dotted or an array [v,h] or [t,r,b,l]"
679
+ end
680
+ end
681
+ alias_method :border_lines=, :border_line=
682
+
683
+ def border_top_line
684
+ @borders.include?(:top) ? @border_lines[0] : 0
685
+ end
686
+
687
+ def border_top_line=(val)
688
+ @border_lines[0] = val
689
+ end
690
+
691
+ def border_right_line
692
+ @borders.include?(:right) ? @border_lines[1] : 0
693
+ end
694
+
695
+ def border_right_line=(val)
696
+ @border_lines[1] = val
697
+ end
698
+
699
+ def border_bottom_line
700
+ @borders.include?(:bottom) ? @border_lines[2] : 0
701
+ end
702
+
703
+ def border_bottom_line=(val)
704
+ @border_lines[2] = val
705
+ end
706
+
707
+ def border_left_line
708
+ @borders.include?(:left) ? @border_lines[3] : 0
709
+ end
710
+
711
+ def border_left_line=(val)
712
+ @border_lines[3] = val
713
+ end
714
+
715
+ # Draws the cell's background color.
716
+ #
717
+ def draw_background(pt)
718
+ return unless background_color
719
+
720
+ @pdf.mask(:fill_color) do
721
+ @pdf.fill_color background_color
722
+ @pdf.fill_rectangle pt, width, height
723
+ end
724
+ end
725
+
726
+ # Draws borders around the cell. Borders are centered on the bounds of
727
+ # the cell outside of any padding, so the caller is responsible for
728
+ # setting appropriate padding to ensure the border does not overlap with
729
+ # cell content.
730
+ #
731
+ def draw_borders(pt)
732
+ x, y = pt
733
+
734
+ @pdf.mask(:line_width, :stroke_color) do
735
+ @borders.each do |border|
736
+ idx = {:top => 0, :right => 1, :bottom => 2, :left => 3}[border]
737
+ border_color = @border_colors[idx]
738
+ border_width = @border_widths[idx]
739
+ border_line = @border_lines[idx]
740
+
741
+ next if border_width <= 0
742
+
743
+ # Left and right borders are drawn one-half border beyond the center
744
+ # of the corner, so that the corners end up square.
745
+ from, to = case border
746
+ when :top
747
+ [[x, y], [x+width, y]]
748
+ when :bottom
749
+ [[x, y-height], [x+width, y-height]]
750
+ when :left
751
+ [[x, y + (border_top_width / 2.0)],
752
+ [x, y - height - (border_bottom_width / 2.0)]]
753
+ when :right
754
+ [[x+width, y + (border_top_width / 2.0)],
755
+ [x+width, y - height - (border_bottom_width / 2.0)]]
756
+ end
757
+
758
+ case border_line
759
+ when :dashed
760
+ @pdf.dash border_width * 4
761
+ when :dotted
762
+ @pdf.dash border_width, :space => border_width * 2
763
+ when :solid
764
+ # normal line style
765
+ else
766
+ raise ArgumentError, "border_line must be :solid, :dotted or" +
767
+ " :dashed"
768
+ end
769
+
770
+ @pdf.line_width = border_width
771
+ @pdf.stroke_color = border_color
772
+ @pdf.stroke_line(from, to)
773
+ @pdf.undash
774
+ end
775
+ end
776
+ end
777
+
778
+ # Draws cell content within the cell's bounding box. Must be implemented
779
+ # in subclasses.
780
+ #
781
+ def draw_content
782
+ raise NotImplementedError, "subclasses must implement draw_content"
783
+ end
784
+
785
+ end
786
+ end
787
+ end