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 +11 -0
- data/README +7 -9
- data/Rakefile +4 -4
- data/TODO +1 -0
- data/examples/cell.rb +1 -0
- data/examples/image.rb +1 -0
- data/examples/repeating.rb +38 -0
- data/examples/shapes.rb +1 -0
- data/examples/table.rb +1 -0
- data/examples/utf8-long.rb +1 -0
- data/examples/utf8.rb +1 -0
- data/lib/pdf/core.rb +2 -0
- data/lib/pdf/wrapper.rb +216 -100
- data/specs/wrapper_spec.rb +134 -18
- metadata +6 -6
- data/DESIGN +0 -30
- data/specs/data/graph.svg +0 -138
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
|
-
|
12
|
-
tweaks
|
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
|
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.
|
73
|
-
|
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.
|
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('
|
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
|
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
|
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/examples/cell.rb
CHANGED
data/examples/image.rb
CHANGED
@@ -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
data/examples/table.rb
CHANGED
data/examples/utf8-long.rb
CHANGED
data/examples/utf8.rb
CHANGED
data/lib/pdf/core.rb
CHANGED
data/lib/pdf/wrapper.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
#
|
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>:
|
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
|
-
:
|
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[:
|
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:
|
258
|
-
# TODO: add
|
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,
|
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
|
-
|
282
|
-
line(x,y,x+w,y)
|
283
|
-
line(x,y
|
284
|
-
line(x,y,x,y+h)
|
285
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
527
|
-
# TODO:
|
528
|
-
#
|
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
|
-
|
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 =
|
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
|
608
|
-
|
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 =
|
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]
|
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]
|
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]
|
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]
|
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
|