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 +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
|