pdf-wrapper 0.0.2 → 0.0.3

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.
data/CHANGELOG CHANGED
@@ -1,3 +1,14 @@
1
+ v0.0.3 (xxx)
2
+ - added support for repeating elements (like page numbers) via repeating_element
3
+ - Ensured consistent behaviour WRT functions moving the internal cursor
4
+ - functions that require positioning info (cell, shapes, etc) will not move the cursor at all
5
+ - functions where positioning info in optional (text, image, etc), the cursor will be moved to the bottom left
6
+ corner of the object
7
+ - Ensure no unrecognised options are provided to various functions
8
+ - Add support for padding between a cell border and its text
9
+ - added support for scaling images proportionally
10
+ - expanded spec coverage
11
+
1
12
  v0.0.2 (11th January 2008)
2
13
  - Added support for a range of extra image formats (jpg, pdf, gif, etc)
3
14
  - Various documentation improvements
data/README CHANGED
@@ -8,12 +8,12 @@ use for making PDFs. The idea is to lever the low level tools in those libraries
8
8
  higher level tools - tables, text boxes, borders, lists, repeating elements
9
9
  (headers/footers), etc.
10
10
 
11
- At this stage the API is *roughly* following that of PDF::Writer, but i've made
12
- tweaks in some places and added some new methods. This is a work in progress so
13
- many features of PDF::Writer aren't available yet.
11
+ The API started off *roughly* following that of PDF::Writer, but i've made
12
+ tweaks and changes along the way and it's diverging. This is a work in progress
13
+ so many features of PDF::Writer aren't available yet.
14
14
 
15
15
  A key motivation for writing this library is cairo's support for Unicode in PDFs.
16
- All text functions in this library support UTF8 input, although as a native
16
+ All text functions in this library support UTF-8 input, although as a native
17
17
  English speaker I've only tested this a little, so any feedback is welcome.
18
18
 
19
19
  There also seems to be a lack of English documentation available for the ruby
@@ -51,7 +51,7 @@ James Healy <jimmy@deefa.com>
51
51
  * ruby/pango[http://ruby-gnome2.sourceforge.jp/] (optional, required to add text)
52
52
  * ruby/rsvg2[http://ruby-gnome2.sourceforge.jp/] (optional, required for SVG support)
53
53
  * ruby/gdkpixbuf[http://ruby-gnome2.sourceforge.jp/] (optional, required for GIF/JPG support)
54
- * ruby/poppler[http://ruby-gnome2.sourceforge.jp/] (optional, required embedding PDF images)
54
+ * ruby/poppler[http://ruby-gnome2.sourceforge.jp/] (optional, required for embedding PDF images)
55
55
 
56
56
  These are all ruby bindings to C libraries. On Debian/Ubuntu based systems
57
57
  (which I develop on) you can get them by running:
@@ -69,7 +69,5 @@ JRuby users, you're probably out of luck.
69
69
 
70
70
  Rubinius users, I have no idea.
71
71
 
72
- Ruby1.9 users, the current release of ruby/cairo (1.5.0) doesn't work with 1.9,
73
- but the version in SVN does. Hopefully it will be released soon. The version in
74
- Debian has been patched to work with 1.9 already. PDF::Wrapper itself is 1.9
75
- compatible.
72
+ Ruby1.9 users, the current release of ruby/cairo (1.5.1) added support for 1.9. PDF::Wrapper
73
+ itself is 1.9 compatible.
data/Rakefile CHANGED
@@ -6,7 +6,7 @@ require 'rake/testtask'
6
6
  require "rake/gempackagetask"
7
7
  require 'spec/rake/spectask'
8
8
 
9
- PKG_VERSION = "0.0.2"
9
+ PKG_VERSION = "0.0.3"
10
10
  PKG_NAME = "pdf-wrapper"
11
11
  PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
12
12
 
@@ -46,7 +46,7 @@ Rake::RDocTask.new("doc") do |rdoc|
46
46
  rdoc.rdoc_dir = (ENV['CC_BUILD_ARTIFACTS'] || 'doc') + '/rdoc'
47
47
  rdoc.rdoc_files.include('README')
48
48
  rdoc.rdoc_files.include('CHANGELOG')
49
- rdoc.rdoc_files.include('DESIGN')
49
+ rdoc.rdoc_files.include('TODO')
50
50
  rdoc.rdoc_files.include('lib/**/*.rb')
51
51
  rdoc.options << "--inline-source"
52
52
  end
@@ -66,13 +66,13 @@ spec = Gem::Specification.new do |spec|
66
66
  spec.files = Dir.glob("{examples,lib,specs}/**/**/*") + ["Rakefile"]
67
67
  spec.require_path = "lib"
68
68
  spec.has_rdoc = true
69
- spec.extra_rdoc_files = %w{README DESIGN CHANGELOG}
69
+ spec.extra_rdoc_files = %w{README CHANGELOG TODO}
70
70
  spec.rdoc_options << '--title' << 'PDF::Wrapper Documentation' << '--main' << 'README' << '-q'
71
71
  spec.author = "James Healy"
72
72
  spec.homepage = "http://pdf-wrapper.rubyforge.org/"
73
73
  spec.email = "jimmy@deefa.com"
74
74
  spec.rubyforge_project = "pdf-wrapper"
75
- spec.description = "A unicode aware PDF writing library that uses the ruby bindings to various c libraries ( like, cairo, pango, poppler and rsvg ) to do the heavy lifting."
75
+ spec.description = "A unicode aware PDF writing library that uses the ruby bindings to various c libraries ( like cairo, pango, poppler and rsvg ) to do the heavy lifting."
76
76
  end
77
77
 
78
78
  # package the library into a gem
data/TODO ADDED
@@ -0,0 +1 @@
1
+ All TODO's are currently marked inline in the source code
data/examples/cell.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # coding: utf-8
2
3
 
3
4
  $:.unshift(File.dirname(__FILE__) + "/../lib")
4
5
 
data/examples/image.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # coding: utf-8
2
3
 
3
4
  $:.unshift(File.dirname(__FILE__) + "/../lib")
4
5
 
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+ # coding: utf-8
3
+
4
+ $:.unshift(File.dirname(__FILE__) + "/../lib")
5
+
6
+ require 'pdf/wrapper'
7
+
8
+ pdf = PDF::Wrapper.new(:paper => :A4)
9
+
10
+ pdf.repeating_element(:all) do
11
+ pdf.text("Page #{pdf.page}!", :left => pdf.margin_left, :top => pdf.margin_top, :font_size => 18, :alignment => :center)
12
+ end
13
+
14
+ pdf.repeating_element(:even) do
15
+ pdf.circle(pdf.absolute_x_middle, pdf.absolute_y_middle, 100)
16
+ end
17
+
18
+ pdf.repeating_element(:odd) do
19
+ pdf.rectangle(pdf.absolute_x_middle, pdf.absolute_y_middle, 100, 100)
20
+ end
21
+
22
+ pdf.repeating_element([1,2]) do
23
+ pdf.rounded_rectangle(100, 100, 100, 100, 10)
24
+ end
25
+
26
+ pdf.repeating_element(3) do
27
+ pdf.line(pdf.absolute_x_middle, pdf.absolute_y_middle, 100, 100)
28
+ end
29
+
30
+ pdf.repeating_element((3..4)) do
31
+ pdf.circle(400, 400, 100, :color => :red)
32
+ end
33
+
34
+ pdf.start_new_page
35
+ pdf.start_new_page
36
+ pdf.start_new_page
37
+
38
+ pdf.render_to_file("repeating.pdf")
data/examples/shapes.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # coding: utf-8
2
3
 
3
4
  $:.unshift(File.dirname(__FILE__) + "/../lib")
4
5
 
data/examples/table.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # coding: utf-8
2
3
 
3
4
  $:.unshift(File.dirname(__FILE__) + "/../lib")
4
5
 
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # coding: utf-8
2
3
 
3
4
  $:.unshift(File.dirname(__FILE__) + "/../lib")
4
5
 
data/examples/utf8.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # coding: utf-8
2
3
 
3
4
  $:.unshift(File.dirname(__FILE__) + "/../lib")
4
5
 
data/lib/pdf/core.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # coding: utf-8
2
+
1
3
  class Hash
2
4
  # raise an error if this hash has any keys that aren't in the supplied list
3
5
  # - borrowed from activesupport
data/lib/pdf/wrapper.rb CHANGED
@@ -1,9 +1,9 @@
1
- # -* coding: UTF-8 -*-
1
+ # coding: utf-8
2
2
 
3
3
  require 'stringio'
4
4
  require 'pdf/core'
5
5
 
6
- # try to load cairo from the standard places, but don't worry if it fails,
6
+ # try to load cairo from the standard places, but don't worry if it fails,
7
7
  # we'll try to find it via rubygems
8
8
  begin
9
9
  require 'cairo'
@@ -23,7 +23,7 @@ module PDF
23
23
  # Create PDF files by using the cairo and pango libraries.
24
24
  #
25
25
  # Rendering to a file:
26
- #
26
+ #
27
27
  # require 'pdf/wrapper'
28
28
  # pdf = PDF::Wrapper.new(:paper => :A4)
29
29
  # pdf.text "Hello World"
@@ -47,7 +47,7 @@ module PDF
47
47
  class Wrapper
48
48
 
49
49
  attr_reader :margin_left, :margin_right, :margin_top, :margin_bottom
50
- attr_reader :page_width, :page_height
50
+ attr_reader :page_width, :page_height, :page
51
51
 
52
52
  # borrowed from PDF::Writer
53
53
  PAGE_SIZES = { # :value {...}:
@@ -82,17 +82,18 @@ module PDF
82
82
  # Options:
83
83
  # <tt>:paper</tt>:: The paper size to use (default :A4)
84
84
  # <tt>:orientation</tt>:: :portrait (default) or :landscape
85
- # <tt>:background_colour</tt>:: The background colour to use (default :white)
85
+ # <tt>:background_color</tt>:: The background colour to use (default :white)
86
86
  def initialize(opts={})
87
87
  options = {:paper => :A4,
88
88
  :orientation => :portrait,
89
- :background_colour => :white
89
+ :background_color => :white
90
90
  }
91
91
  options.merge!(opts)
92
-
92
+
93
93
  # test for invalid options
94
+ options.assert_valid_keys(:paper, :orientation, :background_color)
94
95
  raise ArgumentError, "Invalid paper option" unless PAGE_SIZES.include?(options[:paper])
95
-
96
+
96
97
  # set page dimensions
97
98
  if options[:orientation].eql?(:portrait)
98
99
  @page_width = PAGE_SIZES[options[:paper]][0]
@@ -117,14 +118,18 @@ module PDF
117
118
  @context = Cairo::Context.new(@surface)
118
119
 
119
120
  # set the background colour
120
- set_color(options[:background_colour])
121
+ set_color(options[:background_color])
121
122
  @context.paint
122
123
 
123
124
  # set a default drawing colour and font style
124
125
  default_color(:black)
125
126
  default_font("Sans Serif")
126
127
  default_font_size(16)
127
-
128
+
129
+ # maintain a count of pages and array of repeating elements to add to each page
130
+ @page = 1
131
+ @repeating = []
132
+
128
133
  # move the cursor to the top left of the usable canvas
129
134
  reset_cursor
130
135
  end
@@ -132,13 +137,13 @@ module PDF
132
137
  #####################################################
133
138
  # Functions relating to calculating various page dimensions
134
139
  #####################################################
135
-
140
+
136
141
  # Returns the x value of the left margin
137
142
  # The top left corner of the page is (0,0)
138
143
  def absolute_left_margin
139
144
  @margin_left
140
145
  end
141
-
146
+
142
147
  # Returns the x value of the right margin
143
148
  # The top left corner of the page is (0,0)
144
149
  def absolute_right_margin
@@ -206,7 +211,7 @@ module PDF
206
211
  #####################################################
207
212
  # Functions relating to working with text
208
213
  #####################################################
209
-
214
+
210
215
  # change the default font size
211
216
  def default_font_size(size)
212
217
  #@context.set_font_size(size.to_i)
@@ -229,12 +234,12 @@ module PDF
229
234
  #
230
235
  # Parameters:
231
236
  # <tt>c</tt>:: either a colour symbol recognised by rcairo (:red, :blue, :black, etc) or
232
- # an array with 3-4 integer elements. The first 3 numbers are red, green and
237
+ # an array with 3-4 integer elements. The first 3 numbers are red, green and
233
238
  # blue (0-255). The optional 4th number is the alpha channel and should be
234
239
  # between 0 and 1. See the API docs at http://cairo.rubyforge.org/ for a list
235
240
  # of predefined colours
236
241
  def default_color(c)
237
- c = translate_color(c)
242
+ c = translate_color(c)
238
243
  validate_color(c)
239
244
  @default_color = c
240
245
  end
@@ -248,43 +253,54 @@ module PDF
248
253
  # the following options:
249
254
  #
250
255
  # <tt>:border</tt>:: Which sides of the cell should have a border? A string with any combination the letters tblr (top, bottom, left, right). Nil for no border, defaults to all sides.
251
- # <tt>:border_width</tt>:: How wide should the border be?
256
+ # <tt>:border_width</tt>:: How wide should the border be?
252
257
  # <tt>:border_color</tt>:: What color should the border be?
253
258
  # <tt>:bgcolor</tt>:: A background color for the cell. Defaults to none.
259
+ # <tt>:padding</tt>:: The number of points to leave between the inside of the border and text. Defaults to 3.
254
260
  def cell(str, x, y, w, h, opts={})
255
261
  # TODO: add support for pango markup (see http://ruby-gnome2.sourceforge.jp/hiki.cgi?pango-markup)
256
262
  # TODO: add a wrap option so wrapping can be disabled
257
- # TODO: raise an error if any unrecognised options were supplied
258
- # TODO: add padding between border and text
259
- # TODO: how do we handle a single word that is too long for the width?
263
+ # TODO: handle a single word that is too long for the width
264
+ # TODO: add an option to draw a border with rounded corners
260
265
 
261
266
  options = default_text_options
262
- options.merge!({:border => "tblr", :border_width => 1, :border_color => :black, :bgcolor => nil})
267
+ options.merge!({:border => "tblr", :border_width => 1, :border_color => :black, :bgcolor => nil, :padding => 3})
263
268
  options.merge!(opts)
269
+ options.assert_valid_keys(default_text_options.keys + [:width, :border, :border_width, :border_color, :bgcolor, :padding])
270
+
271
+ # apply padding
272
+ textw = x - (options[:padding] * 2)
273
+ texth = h - (options[:padding] * 2)
274
+ textx = x + options[:padding]
275
+ texty = y + options[:padding]
264
276
 
265
- options[:width] = w
266
277
  options[:border] = "" unless options[:border]
267
278
  options[:border].downcase!
268
279
 
280
+ # save the cursor position so we can restore it at the end
281
+ origx, origy = current_point
282
+
269
283
  # TODO: raise an exception if the box coords or dimensions will place it off the canvas
270
284
  rectangle(x,y,w,h, :color => options[:bgcolor], :fill_color => options[:bgcolor]) if options[:bgcolor]
271
285
 
272
- layout = build_pango_layout(str.to_s, options)
286
+ layout = build_pango_layout(str.to_s, textw, options)
273
287
 
274
288
  set_color(options[:color])
275
289
 
276
290
  # draw the context on our cairo layout
277
- render_layout(layout, x, y, h, :auto_new_page => false)
291
+ render_layout(layout, textx, texty, texth, :auto_new_page => false)
278
292
 
279
293
  # draw a border around the cell
280
294
  # TODO: obey options[:border_width]
281
- # TODO: obey options[:border_color]
282
- line(x,y,x+w,y) if options[:border].include?("t")
283
- line(x,y+h,x+w,y+h) if options[:border].include?("b")
284
- line(x,y,x,y+h) if options[:border].include?("l")
285
- line(x+w,y,x+w,y+h) if options[:border].include?("r")
295
+ line(x,y,x+w,y, :color => options[:border_color]) if options[:border].include?("t")
296
+ line(x,y+h,x+w,y+h, :color => options[:border_color]) if options[:border].include?("b")
297
+ line(x,y,x,y+h, :color => options[:border_color]) if options[:border].include?("l")
298
+ line(x+w,y,x+w,y+h, :color => options[:border_color]) if options[:border].include?("r")
299
+
300
+ # restore the cursor position
301
+ move_to(origx, origy)
286
302
  end
287
-
303
+
288
304
  # draws a basic table onto the page
289
305
  # data - a 2d array with the data for the columns. The first row will be treated as the headings
290
306
  #
@@ -298,23 +314,23 @@ module PDF
298
314
  # TODO: instead of accepting the data, use a XHTML table string?
299
315
  # TODO: handle overflowing to a new page
300
316
  # TODO: add a way to display borders
301
- # TODO: raise an error if any unrecognised options were supplied
302
317
 
303
318
  x, y = current_point
304
319
  options = default_text_options.merge!({:left => x,
305
320
  :top => y
306
321
  })
307
322
  options.merge!(opts)
323
+ options.assert_valid_keys(default_text_options.keys + default_positioning_options.keys)
308
324
  options[:width] = body_width - options[:left] unless options[:width]
309
325
 
310
- # move to the start of our table (the top left)
326
+ # move to the start of our table (the top left)
311
327
  x = options[:left]
312
328
  y = options[:top]
313
329
  move_to(x,y)
314
330
 
315
331
  # all columns will have the same width at this stage
316
332
  cell_width = options[:width] / data.first.size
317
-
333
+
318
334
  # draw the header cells
319
335
  y = draw_table_row(data.shift, cell_width, options)
320
336
  x = options[:left]
@@ -334,7 +350,7 @@ module PDF
334
350
  # the default font styling set by default_font(), default_font_size, etc
335
351
  #
336
352
  # There is no way to place a bottom bound (or height) onto the text. Text will wrap as
337
- # necessary and take all the room it needs. For finer grained control of text boxes, see the
353
+ # necessary and take all the room it needs. For finer grained control of text boxes, see the
338
354
  # cell method.
339
355
  #
340
356
  # To override all these defaults, use the options hash
@@ -354,11 +370,11 @@ module PDF
354
370
  # <tt>:spacing</tt>:: Space between lines in PDF points
355
371
  def text(str, opts={})
356
372
  # TODO: add support for pango markup (see http://ruby-gnome2.sourceforge.jp/hiki.cgi?pango-markup)
373
+ # TODO: add converters from various markup languages to pango markup. (bluecloth, redcloth, markdown, textile, etc)
357
374
  # TODO: add a wrap option so wrapping can be disabled
358
- # TODO: raise an error if any unrecognised options were supplied
359
375
  #
360
376
  # the non pango way to add text to the cairo context, not particularly useful for
361
- # PDF generation as it doesn't support wrapping text or other advanced layout features
377
+ # PDF generation as it doesn't support wrapping text or other advanced layout features
362
378
  # and I really don't feel like re-implementing all that
363
379
  # @context.show_text(str)
364
380
 
@@ -366,12 +382,13 @@ module PDF
366
382
  x, y = current_point
367
383
  options = default_text_options.merge!({:left => x, :top => y})
368
384
  options.merge!(opts)
385
+ options.assert_valid_keys(default_text_options.keys + default_positioning_options.keys)
369
386
 
370
387
  # if the user hasn't specified a width, make the text wrap on the right margin
371
388
  options[:width] = absolute_right_margin - options[:left] if options[:width].nil?
372
389
 
373
- layout = build_pango_layout(str.to_s, options)
374
-
390
+ layout = build_pango_layout(str.to_s, options[:width], options)
391
+
375
392
  set_color(options[:color])
376
393
 
377
394
  # draw the context on our cairo layout
@@ -382,12 +399,12 @@ module PDF
382
399
 
383
400
  # Returns the amount of vertical space needed to display the supplied text at the requested width
384
401
  # opts is an options hash that specifies various attributes of the text. See the text function for more information.
385
- # TODO: raise an error if any unrecognised options were supplied
386
402
  def text_height(str, width, opts = {})
387
403
  options = default_text_options.merge!(opts)
388
404
  options[:width] = width || body_width
405
+ options.assert_valid_keys(default_text_options.keys + default_positioning_options.keys)
389
406
 
390
- layout = build_pango_layout(str.to_s, options)
407
+ layout = build_pango_layout(str.to_s, options[:width], options)
391
408
  width, height = layout.size
392
409
 
393
410
  return height / Pango::SCALE
@@ -407,23 +424,28 @@ module PDF
407
424
  # <tt>:color</tt>:: The colour of the circle outline
408
425
  # <tt>:fill_color</tt>:: The colour to fill the circle with. Defaults to nil (no fill)
409
426
  def circle(x, y, r, opts = {})
410
- # TODO: raise an error if any unrecognised options were supplied
411
427
  options = {:color => @default_color,
412
428
  :fill_color => nil
413
429
  }
414
430
  options.merge!(opts)
431
+ options.assert_valid_keys(:color, :fill_color)
432
+
433
+ # save the cursor position so we can restore it at the end
434
+ origx, origy = current_point
415
435
 
416
436
  move_to(x + r, y)
417
437
 
418
- # if the rectangle should be filled in
438
+ # if the circle should be filled in
419
439
  if options[:fill_color]
420
440
  set_color(options[:fill_color])
421
441
  @context.circle(x, y, r).fill
422
442
  end
423
-
443
+
424
444
  set_color(options[:color])
425
445
  @context.circle(x, y, r).stroke
426
- move_to(x + r, y + r)
446
+
447
+ # restore the cursor position
448
+ move_to(origx, origy)
427
449
  end
428
450
 
429
451
  # draw a line from x1,y1 to x2,y2
@@ -431,15 +453,20 @@ module PDF
431
453
  # Options:
432
454
  # <tt>:color</tt>:: The colour of the line
433
455
  def line(x0, y0, x1, y1, opts = {})
434
- # TODO: raise an error if any unrecognised options were supplied
435
456
  options = {:color => @default_color }
436
457
  options.merge!(opts)
458
+ options.assert_valid_keys(:color)
459
+
460
+ # save the cursor position so we can restore it at the end
461
+ origx, origy = current_point
437
462
 
438
463
  set_color(options[:color])
439
464
 
440
465
  move_to(x0,y0)
441
466
  @context.line_to(x1,y1).stroke
442
- move_to(x1,y1)
467
+
468
+ # restore the cursor position
469
+ move_to(origx, origy)
443
470
  end
444
471
 
445
472
  # draw a rectangle starting at x,y with w,h dimensions.
@@ -453,22 +480,26 @@ module PDF
453
480
  # <tt>:color</tt>:: The colour of the rectangle outline
454
481
  # <tt>:fill_color</tt>:: The colour to fill the rectangle with. Defaults to nil (no fill)
455
482
  def rectangle(x, y, w, h, opts = {})
456
- # TODO: raise an error if any unrecognised options were supplied
457
483
  options = {:color => @default_color,
458
484
  :fill_color => nil
459
485
  }
460
486
  options.merge!(opts)
461
-
487
+ options.assert_valid_keys(:color, :fill_color)
488
+
489
+ # save the cursor position so we can restore it at the end
490
+ origx, origy = current_point
491
+
462
492
  # if the rectangle should be filled in
463
493
  if options[:fill_color]
464
494
  set_color(options[:fill_color])
465
- @context.rectangle(x, y, w, h).fill
495
+ @context.rectangle(x, y, w, h).fill
466
496
  end
467
-
497
+
468
498
  set_color(options[:color])
469
499
  @context.rectangle(x, y, w, h).stroke
470
-
471
- move_to(x+w, y+h)
500
+
501
+ # restore the cursor position
502
+ move_to(origx, origy)
472
503
  end
473
504
 
474
505
  # draw a rounded rectangle starting at x,y with w,h dimensions.
@@ -483,24 +514,28 @@ module PDF
483
514
  # <tt>:color</tt>:: The colour of the rectangle outline
484
515
  # <tt>:fill_color</tt>:: The colour to fill the rectangle with. Defaults to nil (no fill)
485
516
  def rounded_rectangle(x, y, w, h, r, opts = {})
486
- # TODO: raise an error if any unrecognised options were supplied
487
517
  options = {:color => @default_color,
488
518
  :fill_color => nil
489
519
  }
490
520
  options.merge!(opts)
491
-
521
+ options.assert_valid_keys(:color, :fill_color)
522
+
492
523
  raise ArgumentError, "Argument r must be less than both w and h arguments" if r >= w || r >= h
493
-
524
+
525
+ # save the cursor position so we can restore it at the end
526
+ origx, origy = current_point
527
+
494
528
  # if the rectangle should be filled in
495
529
  if options[:fill_color]
496
530
  set_color(options[:fill_color])
497
- @context.rounded_rectangle(x, y, w, h, r).fill
531
+ @context.rounded_rectangle(x, y, w, h, r).fill
498
532
  end
499
-
533
+
500
534
  set_color(options[:color])
501
535
  @context.rounded_rectangle(x, y, w, h, r).stroke
502
-
503
- move_to(x+w, y+h)
536
+
537
+ # restore the cursor position
538
+ move_to(origx, origy)
504
539
  end
505
540
 
506
541
  #####################################################
@@ -517,16 +552,18 @@ module PDF
517
552
  # <tt>:top</tt>:: The y co-ordinate of the top of the image.
518
553
  # <tt>:height</tt>:: The height of the image
519
554
  # <tt>:width</tt>:: The width of the image
555
+ # <tt>:proportional</tt>:: Boolean. Maintain image proportions when scaling. Defaults to false.
520
556
  #
521
557
  # left and top default to the current cursor location
522
558
  # width and height default to the size of the imported image
523
- #
524
- # if width or height are specified, the image will *not* be scaled proportionally
525
559
  def image(filename, opts = {})
526
- # TODO: add some options for things like justification, scaling and padding
527
- # TODO: raise an error if any unrecognised options were supplied
528
- # TODO: add support for pdf/eps/ps images
560
+ # TODO: add some options for justification and padding
561
+ # TODO: add a seperate method for adding arbitary pages from a PDF file to this one. Good for
562
+ # templating, etc. Save a letterhead as a PDF file, then open it and add it to the page
563
+ # as a starting point. Until we call start_new_page, we can add stuff over the top of the
564
+ # imported content
529
565
  raise ArgumentError, "file #{filename} not found" unless File.file?(filename)
566
+ opts.assert_valid_keys(default_positioning_options.keys + [:proportional])
530
567
 
531
568
  case detect_image_type(filename)
532
569
  when :pdf then draw_pdf filename, opts
@@ -560,9 +597,9 @@ module PDF
560
597
  @context.show_page
561
598
  @context.target.finish
562
599
 
563
- # write each line from the StringIO object it was rendered to into the
600
+ # write each line from the StringIO object it was rendered to into the
564
601
  # requested file
565
- File.open(filename, "w") do |of|
602
+ File.open(filename, "w") do |of|
566
603
  @output.rewind
567
604
  @output.each_line { |line| of.write(line) }
568
605
  end
@@ -579,54 +616,85 @@ module PDF
579
616
  @context.move_to(x,y)
580
617
  end
581
618
 
582
- # reset the cursor by moving it to the top left of the useable section of the page
619
+ # reset the cursor by moving it to the top left of the useable section of the page
583
620
  def reset_cursor
584
621
  @context.move_to(margin_left,margin_top)
585
622
  end
586
623
 
624
+ # add the same elements to multiple pages. Useful for adding items like headers, footers and
625
+ # watermarks.
626
+ #
627
+ # arguments:
628
+ # <tt>spec</tt>:: Which pages to add the items to. :all, :odd, :even, a range, an Array of numbers or an number
629
+ #
630
+ # To add text to every page that mentions the page number
631
+ # pdf.repeating_element(:all) do
632
+ # pdf.text("Page #{pdf.page}!", :left => pdf.margin_left, :top => pdf.margin_top, :font_size => 18)
633
+ # end
634
+ #
635
+ # To add a circle to the middle of every page
636
+ # pdf.repeating_element(:all) do
637
+ # pdf.circle(pdf.absolute_x_middle, pdf.absolute_y_middle, 100)
638
+ # end
639
+ def repeating_element(spec = :all, &block)
640
+ call_repeating_element(spec, block)
641
+
642
+ # store it so we can add it to future pages
643
+ @repeating << {:spec => spec, :block => block}
644
+ end
645
+
587
646
  # move to the next page
588
- def start_new_page
647
+ #
648
+ # arguments:
649
+ # <tt>pageno</tt>:: If specified, the current page number will be set to that. By default, the page number will just increment.
650
+ def start_new_page(pageno = nil)
589
651
  @context.show_page
652
+
653
+ # reset or increment the page counter
654
+ if pageno
655
+ @page = pageno.to_i
656
+ else
657
+ @page += 1
658
+ end
659
+
660
+ # move the cursor to the top left of our page body
590
661
  reset_cursor
662
+
663
+ # apply the appropriate repeating elements to the new page
664
+ @repeating.each do |repeat|
665
+ call_repeating_element(repeat[:spec], repeat[:block])
666
+ end
591
667
  end
592
668
 
593
669
  private
594
670
 
595
- def build_pango_layout(str, opts = {})
596
- options = {:left => @margin_left,
597
- :top => @margin_top,
598
- :font => @default_font,
599
- :font_size => @default_font_size,
600
- :color => @default_color,
601
- :alignment => :left,
602
- :justify => false,
603
- :spacing => 0
604
- }
605
- options.merge!(opts)
671
+ def build_pango_layout(str, w, opts = {})
672
+ options = default_text_options.merge!(opts)
606
673
 
607
- # if the user hasn't specified a width, make the text wrap on the right margin
608
- options[:width] = absolute_right_margin - options[:left] if options[:width].nil?
674
+ # if the user hasn't specified a width, make the layout as wide as the page body
675
+ w = body_width if w.nil?
609
676
 
610
677
  # even though this is a private function, raise this error to force calling functions
611
678
  # to decide how they want to handle converting non-strings into strings for rendering
612
679
  raise ArgumentError, 'build_pango_layout must be passed a string' unless str.kind_of?(String)
613
680
 
614
- # if we're running under a M17n aware VM, ensure the string provided is UTF-8
681
+ # if we're running under a M17n aware VM, ensure the string provided is UTF-8 or can be
682
+ # converted to UTF-8
615
683
  if RUBY_VERSION >= "1.9"
616
684
  begin
617
685
  str = str.encode("UTF-8")
618
686
  rescue
619
687
  raise ArgumentError, 'Strings must be supplied with a UTF-8 encoding, or an encoding that can be converted to UTF-8'
620
- end
688
+ end
621
689
  end
622
-
690
+
623
691
  # The pango way:
624
692
  load_libpango
625
693
 
626
694
  # create a new Pango layout that our text will be added to
627
695
  layout = @context.create_pango_layout
628
696
  layout.text = str.to_s
629
- layout.width = options[:width] * Pango::SCALE
697
+ layout.width = w * Pango::SCALE
630
698
  layout.spacing = options[:spacing] * Pango::SCALE
631
699
 
632
700
  # set the alignment of the text in the layout
@@ -651,6 +719,34 @@ module PDF
651
719
  return layout
652
720
  end
653
721
 
722
+ # runs the code in block, passing it a hash of options that might be
723
+ # required
724
+ def call_repeating_element(spec, block)
725
+ # TODO: disallow start_new_page when adding a repeating element
726
+ if spec == :all ||
727
+ (spec == :even && (page % 2) == 0) ||
728
+ (spec == :odd && (page % 2) == 1) ||
729
+ (spec.class == Range && spec.include?(page)) ||
730
+ (spec.class == Array && spec.include?(page)) ||
731
+ (spec.respond_to?(:to_i) && spec.to_i == page)
732
+
733
+ @context.save do
734
+ # add it to the current page
735
+ block.call
736
+ end
737
+ end
738
+ end
739
+
740
+ def default_positioning_options
741
+ # TODO: use these defaults in appropriate places
742
+ x, y = current_point
743
+ { :left => x,
744
+ :top => y,
745
+ :width => points_to_right_margin(x),
746
+ :height => points_to_bottom_margin(y)
747
+ }
748
+ end
749
+
654
750
  def default_text_options
655
751
  { :font => @default_font,
656
752
  :font_size => @default_font_size,
@@ -675,26 +771,45 @@ module PDF
675
771
  return :pdf
676
772
  elsif bytes.include?("<svg")
677
773
  return :svg
678
- elsif bytes.include?("Exif")
774
+ elsif bytes.include?("Exif") || bytes.include?("JFIF")
679
775
  return :jpg
680
776
  else
681
777
  return nil
682
778
  end
683
779
  end
684
780
 
781
+ def calc_image_dimensions(desired_w, desired_h, actual_w, actual_h, scale = false)
782
+ if scale
783
+ wp = desired_w / actual_w.to_f
784
+ hp = desired_h / actual_h.to_f
785
+
786
+ if wp < hp
787
+ width = actual_w * wp
788
+ height = actual_h * wp
789
+ else
790
+ width = actual_w * hp
791
+ height = actual_h * hp
792
+ end
793
+ else
794
+ width = desired_w || actual_w
795
+ height = desired_h || actual_h
796
+ end
797
+ return width.to_f, height.to_f
798
+ end
799
+
685
800
  def draw_pdf(filename, opts = {})
686
801
  # based on a similar function in rabbit. Thanks Kou.
687
802
  load_libpoppler
688
803
  x, y = current_point
689
804
  page = Poppler::Document.new(filename).get_page(1)
690
805
  w, h = page.size
691
- width = (opts[:width] || w).to_f
692
- height = (opts[:height] || h).to_f
806
+ width, height = calc_image_dimensions(opts[:width], opts[:height], w, h, opts[:proportional])
693
807
  @context.save do
694
808
  @context.translate(opts[:left] || x, opts[:top] || y)
695
809
  @context.scale(width / w, height / h)
696
810
  @context.render_poppler_page(page)
697
811
  end
812
+ move_to(x, y + height)
698
813
  end
699
814
 
700
815
  def draw_pixbuf(filename, opts = {})
@@ -702,28 +817,28 @@ module PDF
702
817
  load_libpixbuf
703
818
  x, y = current_point
704
819
  pixbuf = Gdk::Pixbuf.new(filename)
705
- width = (opts[:width] || pixbuf.width).to_f
706
- height = (opts[:height] || pixbuf.height).to_f
820
+ width, height = calc_image_dimensions(opts[:width], opts[:height], pixbuf.width, pixbuf.height, opts[:proportional])
707
821
  @context.save do
708
822
  @context.translate(opts[:left] || x, opts[:top] || y)
709
823
  @context.scale(width / pixbuf.width, height / pixbuf.height)
710
824
  @context.set_source_pixbuf(pixbuf, 0, 0)
711
825
  @context.paint
712
826
  end
827
+ move_to(x, y + height)
713
828
  end
714
829
 
715
830
  def draw_png(filename, opts = {})
716
831
  # based on a similar function in rabbit. Thanks Kou.
717
832
  x, y = current_point
718
833
  img_surface = Cairo::ImageSurface.from_png(filename)
719
- width = (opts[:width] || img_surface.width).to_f
720
- height = (opts[:height] || img_surface.height).to_f
834
+ width, height = calc_image_dimensions(opts[:width], opts[:height], img_surface.width, img_surface.height, opts[:proportional])
721
835
  @context.save do
722
836
  @context.translate(opts[:left] || x, opts[:top] || y)
723
837
  @context.scale(width / img_surface.width, height / img_surface.height)
724
838
  @context.set_source(img_surface, 0, 0)
725
839
  @context.paint
726
840
  end
841
+ move_to(x, y + height)
727
842
  end
728
843
 
729
844
  def draw_svg(filename, opts = {})
@@ -731,14 +846,14 @@ module PDF
731
846
  load_librsvg
732
847
  x, y = current_point
733
848
  handle = RSVG::Handle.new_from_file(filename)
734
- width = (opts[:width] || handle.width).to_f
735
- height = (opts[:height] || handle.height).to_f
849
+ width, height = calc_image_dimensions(opts[:width], opts[:height], handle.width, handle.height, opts[:proportional])
736
850
  @context.save do
737
851
  @context.translate(opts[:left] || x, opts[:top] || y)
738
852
  @context.scale(width / handle.width, height / handle.height)
739
853
  @context.render_rsvg_handle(handle)
740
854
  #@context.paint
741
855
  end
856
+ move_to(x, y + height)
742
857
  end
743
858
 
744
859
  # adds a single table row to the canvas. Top left of the row will be at the current x,y
@@ -776,13 +891,13 @@ module PDF
776
891
  else
777
892
  # start a new page if necesary
778
893
  if row_height > (absolute_bottom_margin - y)
779
- start_new_page
894
+ start_new_page
780
895
  y = margin_top
781
896
  end
782
897
 
783
898
  # add our cell, then advance x to the left edge of the next cell
784
899
  self.cell(head, x, y, column_widths, row_height, opts)
785
- x += column_widths
900
+ x += column_widths
786
901
  end
787
902
 
788
903
  end
@@ -839,7 +954,7 @@ module PDF
839
954
  end
840
955
 
841
956
  # renders a pango layout onto our main context
842
- # based on a function of the same name found in the text2.rb sample file
957
+ # based on a function of the same name found in the text2.rb sample file
843
958
  # distributed with rcairo - it's still black magic to me and has a few edge
844
959
  # cases where it doesn't work too well. Needs to be improved.
845
960
  def render_layout(layout, x, y, h, opts = {})
@@ -880,7 +995,7 @@ module PDF
880
995
  end
881
996
 
882
997
  # move to the start of the next line
883
- move_to(x, y)
998
+ move_to(x, y)
884
999
  end
885
1000
 
886
1001
  # return the y co-ord we finished on
@@ -893,9 +1008,10 @@ module PDF
893
1008
  # Cairo and Poppler are both loaded, it breaks.
894
1009
  Cairo::Color.parse(c).to_rgb.to_a
895
1010
  end
1011
+
896
1012
  # set the current drawing colour
897
1013
  #
898
- # for info on what is valid, see the comments for default_color
1014
+ # for info on what is valid, see the comments for default_color
899
1015
  def set_color(c)
900
1016
  c = translate_color(c)
901
1017
  validate_color(c)
@@ -904,13 +1020,13 @@ module PDF
904
1020
 
905
1021
  # test to see if the specified colour is a a valid cairo color
906
1022
  #
907
- # for info on what is valid, see the comments for default_color
1023
+ # for info on what is valid, see the comments for default_color
908
1024
  def validate_color(c)
909
1025
  c = translate_color(c)
910
1026
  @context.save
911
1027
  # catch and reraise an exception to keep stack traces readable and clear
912
1028
  begin
913
- raise ArgumentError unless c.kind_of?(Array)
1029
+ raise ArgumentError unless c.kind_of?(Array)
914
1030
  raise ArgumentError if c.size != 3 && c.size != 4
915
1031
  @context.set_source_rgba(c)
916
1032
  rescue ArgumentError