ruport 1.7.1 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
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