pdf-wrapper 0.0.2 → 0.0.3

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