ruport 1.7.1 → 1.8.0

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 (43) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +38 -0
  3. data/HACKING +1 -17
  4. data/{README.rdoc → README.md} +30 -38
  5. data/Rakefile +0 -10
  6. data/examples/row_renderer.rb +1 -1
  7. data/examples/simple_pdf_lines.rb +1 -1
  8. data/examples/trac_ticket_status.rb +1 -1
  9. data/lib/ruport/controller.rb +1 -1
  10. data/lib/ruport/data/grouping.rb +7 -7
  11. data/lib/ruport/data/record.rb +4 -4
  12. data/lib/ruport/data/table.rb +9 -9
  13. data/lib/ruport/formatter/csv.rb +1 -1
  14. data/lib/ruport/formatter/markdown.rb +105 -0
  15. data/lib/ruport/formatter/prawn_pdf.rb +96 -9
  16. data/lib/ruport/formatter/text.rb +1 -1
  17. data/lib/ruport/formatter.rb +1 -2
  18. data/lib/ruport/version.rb +1 -1
  19. data/lib/ruport.rb +7 -11
  20. data/test/controller_test.rb +107 -109
  21. data/test/csv_formatter_test.rb +21 -21
  22. data/test/data_feeder_test.rb +39 -39
  23. data/test/expected_outputs/prawn_pdf_formatter/pdf_basic.pdf.test +265 -0
  24. data/test/grouping_test.rb +74 -74
  25. data/test/helpers.rb +16 -5
  26. data/test/html_formatter_test.rb +22 -22
  27. data/test/markdown_formatter_test.rb +142 -0
  28. data/test/prawn_pdf_formatter_test.rb +108 -0
  29. data/test/record_test.rb +82 -82
  30. data/test/table_pivot_test.rb +9 -2
  31. data/test/table_test.rb +33 -40
  32. data/test/template_test.rb +12 -12
  33. data/test/text_formatter_test.rb +34 -34
  34. data/util/bench/data/table/bench_column_manip.rb +0 -1
  35. data/util/bench/data/table/bench_dup.rb +0 -1
  36. data/util/bench/data/table/bench_init.rb +0 -1
  37. data/util/bench/data/table/bench_manip.rb +0 -1
  38. data/util/bench/formatter/bench_csv.rb +0 -1
  39. data/util/bench/formatter/bench_html.rb +0 -1
  40. data/util/bench/formatter/bench_pdf.rb +0 -1
  41. data/util/bench/formatter/bench_text.rb +0 -1
  42. metadata +30 -29
  43. data/lib/ruport/formatter/pdf.rb +0 -589
@@ -1,589 +0,0 @@
1
- # Ruport : Extensible Reporting System
2
- #
3
- # formatter/pdf.rb provides text formatting for Ruport.
4
- #
5
- # Created by Gregory Brown, February 2006
6
- # Extended by James Healy, Fall 2006
7
- # Copyright (C) 2006-2007 Gregory Brown / James Healy, All Rights Reserved.
8
- #
9
- # Initially inspired by some ideas and code from Simon Claret,
10
- # with many improvements from James Healy and Michael Milner over time.
11
- #
12
- # This is free software distributed under the same terms as Ruby 1.8
13
- # See LICENSE and COPYING for details.
14
- #
15
- module Ruport
16
-
17
- # This class provides PDF output for Ruport's Table, Group, and Grouping
18
- # controllers. It wraps Austin Ziegler's PDF::Writer to provide a higher
19
- # level interface and provides a number of helpers designed to make
20
- # generating PDF reports much easier. You will typically want to build
21
- # subclasses of this formatter to customize it as needed.
22
- #
23
- # Many methods forward options to PDF::Writer, so you may wish to consult
24
- # its API docs.
25
- #
26
- # === Rendering Options
27
- # General:
28
- # * paper_size #=> "LETTER"
29
- # * paper_orientation #=> :portrait
30
- #
31
- # Text:
32
- # * text_format (sets options to be passed to add_text by default)
33
- #
34
- # Table:
35
- # * table_format (a hash that can take any of the options available
36
- # to PDF::SimpleTable)
37
- # * table_format[:maximum_width] #=> 500
38
- #
39
- # Grouping:
40
- # * style (:inline,:justified,:separated,:offset)
41
- #
42
- class Formatter::PDF < Formatter
43
-
44
- module PDFWriterProxy #:nodoc:
45
- def method_missing(id,*args)
46
- super(id,*args)
47
- rescue
48
- pdf_writer.send(id,*args)
49
- end
50
- end
51
-
52
- renders :pdf, :for => [ Controller::Row, Controller::Table,
53
- Controller::Group, Controller::Grouping ]
54
- attr_writer :pdf_writer
55
-
56
- # If you use this macro in your formatter, Ruport will automatically forward
57
- # calls to the underlying PDF::Writer, for any methods that are not wrapped
58
- # or redefined.
59
- def self.proxy_to_pdf_writer
60
- include PDFWriterProxy
61
- end
62
-
63
- save_as_binary_file
64
-
65
- def initialize
66
- Ruport.quiet do
67
- require "pdf/writer"
68
- require "pdf/simpletable"
69
- end
70
- end
71
-
72
- # Hook for setting available options using a template. See the template
73
- # documentation for the available options and their format.
74
- def apply_template
75
- apply_page_format_template(template.page)
76
- apply_text_format_template(template.text)
77
- apply_table_format_template(template.table)
78
- apply_column_format_template(template.column)
79
- apply_heading_format_template(template.heading)
80
- apply_grouping_format_template(template.grouping)
81
- end
82
-
83
- # Returns the current PDF::Writer object or creates a new one if it has not
84
- # been set yet.
85
- #
86
- def pdf_writer
87
- @pdf_writer ||= options.formatter ||
88
- ::PDF::Writer.new( :paper => options.paper_size || "LETTER",
89
- :orientation => options.paper_orientation || :portrait)
90
- end
91
-
92
- # Calls the draw_table method.
93
- #
94
- def build_table_body
95
- draw_table(data)
96
- end
97
-
98
- # Appends the results of PDF::Writer#render to output for your
99
- # <tt>pdf_writer</tt> object.
100
- #
101
- def finalize_table
102
- render_pdf unless options.skip_finalize_table
103
- end
104
-
105
- # Generates a header with the group name for Controller::Group.
106
- def build_group_header
107
- pad(10) { add_text data.name.to_s, :justification => :center }
108
- end
109
-
110
- # Renders the group as a table for Controller::Group.
111
- def build_group_body
112
- render_table data, options.to_hash.merge(:formatter => pdf_writer)
113
- end
114
-
115
- # Determines which style to use and renders the main body for
116
- # Controller::Grouping.
117
- def build_grouping_body
118
- case options.style
119
- when :inline
120
- render_inline_grouping(options.to_hash.merge(:formatter => pdf_writer,
121
- :skip_finalize_table => true))
122
- when :justified, :separated
123
- render_justified_or_separated_grouping
124
- when :offset
125
- render_offset_grouping
126
- else
127
- raise NotImplementedError, "Unknown style"
128
- end
129
- end
130
-
131
- # Calls <tt>render_pdf</tt>.
132
- def finalize_grouping
133
- render_pdf
134
- end
135
-
136
- # Call PDF::Writer#text with the given arguments, using
137
- # <tt>text_format</tt> defaults, if they are defined.
138
- #
139
- # Example:
140
- #
141
- # options.text_format { :font_size => 14 }
142
- #
143
- # add_text("Hello Joe") #renders at 14pt
144
- # add_text("Hello Mike",:font_size => 16) # renders at 16pt
145
- def add_text(text, format_opts={})
146
- format_opts = options.text_format.merge(format_opts) if options.text_format
147
- pdf_writer.text(text, format_opts)
148
- end
149
-
150
- # Calls PDF::Writer#render and appends to <tt>output</tt>.
151
- def render_pdf
152
- output << pdf_writer.render
153
- end
154
-
155
- # - If the image is bigger than the box, it will be scaled down until
156
- # it fits.
157
- # - If the image is smaller than the box, it won't be resized.
158
- #
159
- # options:
160
- # - :x: left bound of box
161
- # - :y: bottom bound of box
162
- # - :width: width of box
163
- # - :height: height of box
164
- #
165
- def center_image_in_box(path, image_opts={})
166
- x = image_opts[:x]
167
- y = image_opts[:y]
168
- width = image_opts[:width]
169
- height = image_opts[:height]
170
- info = ::PDF::Writer::Graphics::ImageInfo.new(File.open(path, "rb"))
171
-
172
- # reduce the size of the image until it fits into the requested box
173
- img_width, img_height =
174
- fit_image_in_box(info.width,width,info.height,height)
175
-
176
- # if the image is smaller than the box, calculate the white space buffer
177
- x, y = add_white_space(x,y,img_width,width,img_height,height)
178
-
179
- pdf_writer.add_image_from_file(path, x, y, img_width, img_height)
180
- end
181
-
182
- # Draws some text on the canvas, surrounded by a box with rounded corners.
183
- #
184
- # Yields an OpenStruct which options can be defined on.
185
- #
186
- # Example:
187
- #
188
- # rounded_text_box(options.text) do |o|
189
- # o.radius = 5
190
- # o.width = options.width || 400
191
- # o.height = options.height || 130
192
- # o.font_size = options.font_size || 12
193
- # o.heading = options.heading
194
- #
195
- # o.x = pdf_writer.absolute_x_middle - o.width/2
196
- # o.y = 300
197
- # end
198
- #
199
- def rounded_text_box(text)
200
- opts = OpenStruct.new
201
- yield(opts)
202
-
203
- resize_text_to_box(text, opts)
204
-
205
- pdf_writer.save_state
206
- draw_box(opts.x, opts.y, opts.width, opts.height, opts.radius,
207
- opts.fill_color, opts.stroke_color)
208
- add_text_with_bottom_border(opts.heading, opts.x, opts.y,
209
- opts.width, opts.font_size) if opts.heading
210
- pdf_writer.restore_state
211
-
212
- start_position = opts.heading ? opts.y - 20 : opts.y
213
- draw_text(text, :y => start_position,
214
- :left => opts.x,
215
- :right => opts.x + opts.width,
216
- :justification => opts.justification || :center,
217
- :font_size => opts.font_size)
218
- move_cursor_to(opts.y - opts.height)
219
- end
220
-
221
- # Adds n to pdf_writer.y, moving the vertical drawing position in the
222
- # document.
223
- def move_cursor(n)
224
- pdf_writer.y += n
225
- end
226
-
227
- # Moves the cursor to a specific y coordinate in the document.
228
- def move_cursor_to(n)
229
- pdf_writer.y = n
230
- end
231
-
232
- # Moves the vertical drawing position in the document upwards by n.
233
- def move_up(n)
234
- pdf_writer.y += n
235
- end
236
-
237
- def move_down(n)
238
- pdf_writer.y -= n
239
- end
240
-
241
- # Adds a specified amount of whitespace above and below the code
242
- # in your block. For example, if you want to surround the top and
243
- # bottom of a line of text with 5 pixels of whitespace:
244
- #
245
- # pad(5) { add_text "This will be padded top and bottom" }
246
- def pad(y,&block)
247
- move_cursor(-y)
248
- block.call
249
- move_cursor(-y)
250
- end
251
-
252
- # Adds a specified amount of whitespace above the code in your block.
253
- # For example, if you want to add a 10 pixel buffer to the top of a
254
- # line of text:
255
- #
256
- # pad_top(10) { add_text "This will be padded on top" }
257
- def pad_top(y,&block)
258
- move_cursor(-y)
259
- block.call
260
- end
261
-
262
- # Adds a specified amount of whitespace below the code in your block.
263
- # For example, if you want to add a 10 pixel buffer to the bottom of a
264
- # line of text:
265
- #
266
- # pad_bottom(10) { add_text "This will be padded on bottom" }
267
- def pad_bottom(y,&block)
268
- block.call
269
- move_cursor(-y)
270
- end
271
-
272
- # Draws a PDF::SimpleTable using the given data (usually a Data::Table).
273
- # Takes all the options you can set on a PDF::SimpleTable object,
274
- # see the PDF::Writer API docs for details, or check our quick reference
275
- # at:
276
- #
277
- # http://stonecode.svnrepository.com/ruport/trac.cgi/wiki/PdfWriterQuickRef
278
- def draw_table(table_data, format_opts={})
279
- m = "PDF Formatter requires column_names to be defined"
280
- raise FormatterError, m if table_data.column_names.empty?
281
-
282
- table_data.rename_columns { |c| c.to_s }
283
-
284
- if options.table_format
285
- format_opts =
286
- Marshal.load(Marshal.dump(options.table_format.merge(format_opts)))
287
- end
288
-
289
- old = pdf_writer.font_size
290
-
291
- ::PDF::SimpleTable.new do |table|
292
- table.maximum_width = 500
293
- table.column_order = table_data.column_names
294
- table.data = table_data
295
- table.data = [{}] if table.data.empty?
296
- apply_pdf_table_column_opts(table,table_data,format_opts)
297
-
298
- format_opts.each {|k,v| table.send("#{k}=", v) }
299
- table.render_on(pdf_writer)
300
- end
301
-
302
- pdf_writer.font_size = old
303
- end
304
-
305
- # This module provides tools to simplify some common drawing operations.
306
- # It is included by default in the PDF formatter.
307
- #
308
- module DrawingHelpers
309
-
310
- # Draws a horizontal line from x1 to x2
311
- def horizontal_line(x1,x2)
312
- pdf_writer.line(x1,cursor,x2,cursor)
313
- pdf_writer.stroke
314
- end
315
-
316
- # Draws a horizontal line from left_boundary to right_boundary
317
- def horizontal_rule
318
- horizontal_line(left_boundary,right_boundary)
319
- end
320
-
321
- alias_method :hr, :horizontal_rule
322
-
323
- # Draws a vertical line at x from y1 to y2
324
- def vertical_line_at(x,y1,y2)
325
- pdf_writer.line(x,y1,x,y2)
326
- pdf_writer.stroke
327
- end
328
-
329
- # Alias for PDF::Writer#absolute_left_margin
330
- def left_boundary
331
- pdf_writer.absolute_left_margin
332
- end
333
-
334
- # Alias for PDF::Writer#absolute_right_margin
335
- def right_boundary
336
- pdf_writer.absolute_right_margin
337
- end
338
-
339
- # Alias for PDF::Writer#absolute_top_margin
340
- def top_boundary
341
- pdf_writer.absolute_top_margin
342
- end
343
-
344
- # Alias for PDF::Writer#absolute_bottom_margin
345
- def bottom_boundary
346
- pdf_writer.absolute_bottom_margin
347
- end
348
-
349
- # Alias for PDF::Writer#y
350
- def cursor
351
- pdf_writer.y
352
- end
353
-
354
- # Draws text at an absolute location, defined by
355
- # :y, :x1|:left, :x2|:right
356
- #
357
- # All options to add_text are also supported.
358
- def draw_text(text,text_opts)
359
- ypos = cursor
360
- move_cursor_to(text_opts[:y]) if text_opts[:y]
361
- add_text(text,
362
- text_opts.merge(:absolute_left => text_opts[:x1] || text_opts[:left],
363
- :absolute_right => text_opts[:x2] || text_opts[:right]))
364
- move_cursor_to(ypos)
365
- end
366
-
367
- # Draws text at an absolute location, defined by
368
- # :y, :x1|:left
369
- #
370
- # The x position defaults to the left margin and the
371
- # y position defaults to the current cursor location.
372
- #
373
- # Uses PDF::Writer#add_text, so it will ignore any options not supported
374
- # by that method.
375
- def draw_text!(text,text_opts)
376
- ypos = cursor
377
- pdf_writer.add_text(text_opts[:x1] || text_opts[:left] || left_boundary,
378
- text_opts[:y] || ypos,
379
- text,
380
- text_opts[:font_size],
381
- text_opts[:angle] || 0)
382
- move_cursor_to(ypos)
383
- end
384
-
385
- def finalize
386
- render_pdf
387
- end
388
- end
389
-
390
- include DrawingHelpers
391
-
392
- private
393
-
394
- def apply_pdf_table_column_opts(table,table_data,format_opts)
395
- column_opts = format_opts.delete(:column_options)
396
-
397
- if column_opts
398
- heading_opts = column_opts.delete(:heading)
399
- if column_opts[:justification]
400
- heading_opts ||= {}
401
- heading_opts = {
402
- :justification => column_opts[:justification]
403
- }.merge(heading_opts)
404
- end
405
- specific = get_specific_column_options(table_data.column_names,
406
- column_opts)
407
- columns = table_data.column_names.inject({}) { |s,c|
408
- s.merge( c => ::PDF::SimpleTable::Column.new(c) { |col|
409
- col.heading = create_heading(heading_opts)
410
- column_opts.each { |k,v| col.send("#{k}=",v) }
411
- # use the specific column names now
412
- specific[c].each { |k,v| col.send("#{k}=",v) }
413
- })
414
- }
415
- table.columns = columns
416
- end
417
- end
418
-
419
- def get_specific_column_options(column_names,column_opts)
420
- column_names.inject({}) do |s,c|
421
- opts = column_opts.delete(c) || {}
422
- if opts[:heading]
423
- opts = opts.merge(:heading => create_heading(opts[:heading]))
424
- end
425
- s.merge(c => opts)
426
- end
427
- end
428
-
429
- def create_heading(heading_opts)
430
- heading_opts ||= {}
431
- ::PDF::SimpleTable::Column::Heading.new {|head|
432
- heading_opts.each {|k,v| head.send("#{k}=",v) }
433
- }
434
- end
435
-
436
- def grouping_columns
437
- data.data.to_a[0][1].column_names.dup.unshift(data.grouped_by)
438
- end
439
-
440
- def table_with_grouped_by_column
441
- Ruport::Data::Table.new(:column_names => grouping_columns)
442
- end
443
-
444
- def render_justified_or_separated_grouping
445
- table = table_with_grouped_by_column
446
- data.each do |name,group|
447
- group.each_with_index do |r,i|
448
- if i == 0
449
- table << { data.grouped_by => "<b>#{name}</b>" }.merge(r.to_hash)
450
- else
451
- table << r
452
- end
453
- end
454
- table << [" "] if options.style == :separated
455
- end
456
- render_table table, options.to_hash.merge(:formatter => pdf_writer)
457
- end
458
-
459
- def render_offset_grouping
460
- table = table_with_grouped_by_column
461
- data.each do |name,group|
462
- table << ["<b>#{name}</b>"]
463
- group.each {|r| table << r }
464
- end
465
- render_table table, options.to_hash.merge(:formatter => pdf_writer)
466
- end
467
-
468
- def image_fits_in_box?(img_width,box_width,img_height,box_height)
469
- !(img_width > box_width || img_height > box_height)
470
- end
471
-
472
- def fit_image_in_box(img_width,box_width,img_height,box_height)
473
- img_ratio = img_height.to_f / img_width.to_f
474
- until image_fits_in_box?(img_width,box_width,img_height,box_height)
475
- img_width -= 1
476
- img_height = img_width * img_ratio
477
- end
478
- return img_width, img_height
479
- end
480
-
481
- def add_white_space(x,y,img_width,box_width,img_height,box_height)
482
- if img_width < box_width
483
- white_space = box_width - img_width
484
- x = x + (white_space / 2)
485
- end
486
- if img_height < box_height
487
- white_space = box_height - img_height
488
- y = y + (white_space / 2)
489
- end
490
- return x, y
491
- end
492
-
493
- def resize_text_to_box(text,opts)
494
- loop do
495
- sz = pdf_writer.text_width(text, opts.font_size)
496
- opts.x + sz > opts.x + opts.width or break
497
- opts.font_size -= 1
498
- end
499
- end
500
-
501
- def draw_box(x,y,width,height,radius,fill_color=nil,stroke_color=nil)
502
- pdf_writer.fill_color(fill_color || Color::RGB::White)
503
- pdf_writer.stroke_color(stroke_color || Color::RGB::Black)
504
- pdf_writer.rounded_rectangle(x, y, width, height, radius).fill_stroke
505
- end
506
-
507
- def add_text_with_bottom_border(text,x,y,width,font_size)
508
- pdf_writer.line( x, y - 20,
509
- x + width, y - 20).stroke
510
- pdf_writer.fill_color(Color::RGB::Black)
511
- move_cursor_to(y - 3)
512
- add_text("<b>#{text}</b>",
513
- :absolute_left => x, :absolute_right => x + width,
514
- :justification => :center, :font_size => font_size)
515
- end
516
-
517
- def apply_page_format_template(t)
518
- t = (t || {}).merge(options.page_format || {})
519
- options.paper_size ||= t[:size]
520
- options.paper_orientation ||= t[:layout]
521
- end
522
-
523
- def apply_text_format_template(t)
524
- t = (t || {}).merge(options.text_format || {})
525
- options.text_format = t unless t.empty?
526
- end
527
-
528
- def apply_table_format_template(t)
529
- t = (t || {}).merge(options.table_format || {})
530
- options.table_format = t unless t.empty?
531
- end
532
-
533
- def apply_column_format_template(t)
534
- t = (t || {}).merge(options.column_format || {})
535
- column_opts = {}
536
- column_opts.merge!(:justification => t[:alignment]) if t[:alignment]
537
- column_opts.merge!(:width => t[:width]) if t[:width]
538
- unless column_opts.empty?
539
- if options.table_format
540
- if options.table_format[:column_options]
541
- options.table_format[:column_options] =
542
- column_opts.merge(options.table_format[:column_options])
543
- else
544
- options.table_format.merge!(:column_options => column_opts)
545
- end
546
- else
547
- options.table_format = { :column_options => column_opts }
548
- end
549
- end
550
- end
551
-
552
- def apply_heading_format_template(t)
553
- t = (t || {}).merge(options.heading_format || {})
554
- heading_opts = {}
555
- heading_opts.merge!(:justification => t[:alignment]) if t[:alignment]
556
- heading_opts.merge!(:bold => t[:bold]) unless t[:bold].nil?
557
- heading_opts.merge!(:title => t[:title]) if t[:title]
558
- unless heading_opts.empty?
559
- if options.table_format
560
- if options.table_format[:column_options]
561
- if options.table_format[:column_options][:heading]
562
- options.table_format[:column_options][:heading] =
563
- heading_opts.merge(
564
- options.table_format[:column_options][:heading]
565
- )
566
- else
567
- options.table_format[:column_options].merge!(
568
- :heading => heading_opts
569
- )
570
- end
571
- else
572
- options.table_format.merge!(
573
- :column_options => { :heading => heading_opts }
574
- )
575
- end
576
- else
577
- options.table_format = {
578
- :column_options => { :heading => heading_opts }
579
- }
580
- end
581
- end
582
- end
583
-
584
- def apply_grouping_format_template(t)
585
- t = (t || {}).merge(options.grouping_format || {})
586
- options.style ||= t[:style]
587
- end
588
- end
589
- end