pdf-wrapper 0.0.7 → 0.1.0

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.
@@ -0,0 +1,337 @@
1
+ module PDF
2
+ class Wrapper
3
+
4
+ # change the default font size
5
+ def font_size(size)
6
+ @default_font_size = size.to_i unless size.nil?
7
+ end
8
+ alias font_size= font_size
9
+
10
+ # change the default font to write with
11
+ def font(fontname, style = nil, weight = nil)
12
+ @default_font = fontname
13
+ @default_font_style = style unless style.nil?
14
+ @default_font_weight = weight unless weight.nil?
15
+ end
16
+
17
+ # add text to the page, bounded by a box with dimensions HxW, with it's top left corner
18
+ # at x,y. Any text that doesn't fit it the box will be silently dropped.
19
+ #
20
+ # In addition to the standard text style options (see the documentation for text()), cell() supports
21
+ # the following options:
22
+ #
23
+ # <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.
24
+ # <tt>:border_width</tt>:: How wide should the border be?
25
+ # <tt>:border_color</tt>:: What color should the border be?
26
+ # <tt>:fill_color</tt>:: A background color for the cell. Defaults to none.
27
+ # <tt>:radius</tt>:: Give the border around the cell rounded corners. Implies :border => "tblr"
28
+ def cell(str, x, y, w, h, opts={})
29
+ # TODO: add a wrap option so wrapping can be disabled
30
+ # TODO: add an option for vertical alignment
31
+
32
+ options = default_text_options
33
+ options.merge!({:border => "tblr", :border_width => @default_line_width, :border_color => :black, :fill_color => nil, :padding => device_to_user_dist(3,0).first, :radius => nil})
34
+ options.merge!(opts)
35
+ options.assert_valid_keys(default_text_options.keys + [:width, :border, :border_width, :border_color, :fill_color, :padding, :radius])
36
+
37
+ # apply padding
38
+ textw = w - (options[:padding] * 2)
39
+ texth = h - (options[:padding] * 2)
40
+
41
+ # if the user wants a rounded rectangle, we'll draw the border with a rectangle instead
42
+ # of 4 lines
43
+ options[:border] = nil if options[:radius]
44
+
45
+ # normalise the border
46
+ options[:border] = "" unless options[:border]
47
+ options[:border].downcase!
48
+
49
+ save_coords do
50
+ translate(x, y) do
51
+ # draw a border around the cell
52
+ if options[:radius]
53
+ rectangle(0,0,w,h, :radius => options[:radius], :color => options[:border_color], :fill_color => options[:fill_color], :line_width => options[:border_width])
54
+ else
55
+ rectangle(0,0,w,h, :color => options[:fill_color], :fill_color => options[:fill_color]) if options[:fill_color]
56
+ line(0,0,w,0, :color => options[:border_color], :line_width => options[:border_width]) if options[:border].include?("t")
57
+ line(0,h,w,h, :color => options[:border_color], :line_width => options[:border_width]) if options[:border].include?("b")
58
+ line(0,0,0,h, :color => options[:border_color], :line_width => options[:border_width]) if options[:border].include?("l")
59
+ line(w,0,w,h, :color => options[:border_color], :line_width => options[:border_width]) if options[:border].include?("r")
60
+ end
61
+
62
+ layout = build_pango_layout(str.to_s, textw, options)
63
+
64
+ color(options[:color]) if options[:color]
65
+
66
+ # draw the context on our cairo layout
67
+ render_layout(layout, options[:padding], options[:padding], texth, :auto_new_page => false)
68
+ end
69
+
70
+ end
71
+ end
72
+
73
+ # Write text to the page
74
+ #
75
+ # By default the text will be rendered using all the space within the margins and using
76
+ # the default font styling set by default_font(), default_font_size, etc
77
+ #
78
+ # There is no way to place a bottom bound (or height) onto the text. Text will wrap as
79
+ # necessary and take all the room it needs. For finer grained control of text boxes, see the
80
+ # cell method.
81
+ #
82
+ # To override all these defaults, use the options hash
83
+ #
84
+ # Positioning Options:
85
+ #
86
+ # <tt>:left</tt>:: The x co-ordinate of the left-hand side of the text.
87
+ # <tt>:top</tt>:: The y co-ordinate of the top of the text.
88
+ # <tt>:width</tt>:: The width of the text to wrap at
89
+ #
90
+ # Text Style Options:
91
+ #
92
+ # <tt>:font</tt>:: The font family to use as a string
93
+ # <tt>:font_size</tt>:: The size of the font in points
94
+ # <tt>:alignment</tt>:: Align the text along the left, right or centre. Use :left, :right, :center
95
+ # <tt>:wrap</tt>:: The wrapping technique to use if required. Use :word, :char or :wordchar. Default is :wordchar
96
+ # <tt>:justify</tt>:: Justify the text so it exapnds to fill the entire width of each line. Note that this only works in pango >= 1.17
97
+ # <tt>:spacing</tt>:: Space between lines in PDF points
98
+ # <tt>:markup</tt>:: Interpret the text as a markup language. Default is nil (none).
99
+ #
100
+ # = Markup
101
+ #
102
+ # If the markup option is specified, the text can be modified in various ways. At this stage
103
+ # the only markup syntax implemented is :pango.
104
+ #
105
+ # == Pango Markup
106
+ #
107
+ # Full details on the Pango markup language are avaialble at http://ruby-gnome2.sourceforge.jp/hiki.cgi?pango-markup
108
+ #
109
+ # The format is vaguely XML-like.
110
+ #
111
+ # Bold: "Some of this text is <b>bold</b>."
112
+ # Italics: "Some of this text is in <b>italics</b>."
113
+ # Strikethrough: "My name is <s>Bob</s>James."
114
+ # Monospace Font: "Code:\n<tt>puts 1</tt>."
115
+ #
116
+ # For more advanced control, use span tags
117
+ #
118
+ # Big and Bold: Some of this text is <span weight="bold" font_desc="20">bold</span>.
119
+ # Stretched: Some of this text is <span stretch="extraexpanded">funny looking</span>.
120
+ def text(str, opts={})
121
+ # TODO: add converters from various markup languages to pango markup. (markdown, textile, etc)
122
+ # TODO: add a wrap option so wrapping can be disabled
123
+ #
124
+ # the non pango way to add text to the cairo context, not particularly useful for
125
+ # PDF generation as it doesn't support wrapping text or other advanced layout features
126
+ # and I really don't feel like re-implementing all that
127
+ # @context.show_text(str)
128
+
129
+ # the "pango way"
130
+ x, y = current_point
131
+ options = default_text_options.merge!({:left => x, :top => y})
132
+ options.merge!(opts)
133
+ options.assert_valid_keys(default_text_options.keys + default_positioning_options.keys)
134
+
135
+ # if the user hasn't specified a width, make the text wrap on the right margin
136
+ options[:width] = absolute_right_margin - options[:left] if options[:width].nil?
137
+
138
+ layout = build_pango_layout(str.to_s, options[:width], options)
139
+
140
+ color(options[:color]) if options[:color]
141
+
142
+ # draw the context on our cairo layout
143
+ y = render_layout(layout, options[:left], options[:top], points_to_bottom_margin(options[:top]), :auto_new_page => true)
144
+
145
+ move_to(options[:left], y + device_y_to_user_y(5))
146
+ end
147
+
148
+ # Returns the amount of vertical space needed to display the supplied text at the requested width
149
+ # opts is an options hash that specifies various attributes of the text. See the text function for more information.
150
+ def text_height(str, width, opts = {})
151
+ options = default_text_options.merge(opts)
152
+ options.assert_valid_keys(default_text_options.keys)
153
+ options[:width] = width || body_width
154
+
155
+ layout = build_pango_layout(str.to_s, options[:width], options)
156
+ width, height = layout.size
157
+
158
+ return height / Pango::SCALE
159
+ end
160
+
161
+ # Returns the amount of horizontal space needed to display the supplied text with the requested options
162
+ # opts is an options hash that specifies various attributes of the text. See the text function for more information.
163
+ # The text is assumed to not wrap.
164
+ def text_width(str, opts = {})
165
+ options = default_text_options.merge(opts)
166
+ options.assert_valid_keys(default_text_options.keys)
167
+
168
+ layout = build_pango_layout(str.to_s, -1, options)
169
+ width, height = layout.size
170
+
171
+ return width / Pango::SCALE
172
+ end
173
+
174
+ private
175
+
176
+ # takes a string and a range of options and creates a pango layout for us. Pango
177
+ # does all the hard work of calculating text layout, wrapping, fonts, sizes,
178
+ # direction and more. Thank $diety.
179
+ #
180
+ # The string should be encoded using utf-8. If you get unexpected characters in the
181
+ # rendered output, check the string encoding. Under Ruby 1.9 compatible VMs, any
182
+ # non utf-8 strings will be automatically converted if possible.
183
+ #
184
+ # The layout will be constrained to the requested width, but has no maximum height. It
185
+ # is up to some other part of the code to decide how much of the layout should actually
186
+ # be rendered to the document, when page breaks should be inserted, etc. To specify no
187
+ # wrapping, set width to nil. This will result in a single line layout that is as wide
188
+ # as it needs to be to fit the entire string.
189
+ #
190
+ # options:
191
+ # <tt>:markup</tt>:: The markup language of the string. See Wrapper#text for more information
192
+ # <tt>:spacing</tt>:: The spacing between lines. See Wrapper#text for more information
193
+ # <tt>:alignment</tt>:: The alignment of the text. See Wrapper#text for more information
194
+ # <tt>:justify</tt>:: Should spacing between words be tweaked so each edge of the line touches
195
+ # the edge of the layout. See Wrapper#text for more information
196
+ # <tt>:font</tt>:: The font to use. See Wrapper#text for more information
197
+ # <tt>:font_size</tt>:: The font size to use. See Wrapper#text for more information
198
+ # <tt>:wrap</tt>:: The wrap technique to use. See Wrapper#text for more information
199
+ def build_pango_layout(str, w, opts = {})
200
+ options = default_text_options.merge!(opts)
201
+
202
+ # if the user hasn't specified a width, make the layout as wide as the page body
203
+ w = body_width if w.nil?
204
+
205
+ # even though this is a private function, raise this error to force calling functions
206
+ # to decide how they want to handle converting non-strings into strings for rendering
207
+ raise ArgumentError, 'build_pango_layout must be passed a string' unless str.kind_of?(String)
208
+
209
+ # if we're running under a M17n aware VM, ensure the string provided is UTF-8 or can be
210
+ # converted to UTF-8
211
+ if RUBY_VERSION >= "1.9"
212
+ begin
213
+ str = str.encode("UTF-8")
214
+ rescue
215
+ raise ArgumentError, 'Strings must be supplied with a UTF-8 encoding, or an encoding that can be converted to UTF-8'
216
+ end
217
+ end
218
+
219
+ # The pango way:
220
+ load_libpango
221
+
222
+ # create a new Pango layout that our text will be added to
223
+ layout = @context.create_pango_layout
224
+ if options[:markup] == :pango
225
+ layout.markup = str.to_s
226
+ else
227
+ layout.text = str.to_s
228
+ end
229
+ if w.nil? || w < 0
230
+ layout.width = -1
231
+ else
232
+ # width is specified in user points
233
+ layout.width = w * Pango::SCALE
234
+ end
235
+ # spacing is specified in user points
236
+ layout.spacing = device_y_to_user_y(options[:spacing] * Pango::SCALE)
237
+
238
+ # set the alignment of the text in the layout
239
+ if options[:alignment].eql?(:left)
240
+ layout.alignment = Pango::Layout::ALIGN_LEFT
241
+ elsif options[:alignment].eql?(:right)
242
+ layout.alignment = Pango::Layout::ALIGN_RIGHT
243
+ elsif options[:alignment].eql?(:center) || options[:alignment].eql?(:centre)
244
+ layout.alignment = Pango::Layout::ALIGN_CENTER
245
+ else
246
+ raise ArgumentError, "Invalid alignment requested"
247
+ end
248
+
249
+ # set the wrapping technique text of the layout
250
+ if options[:wrap].eql?(:word)
251
+ layout.wrap = Pango::Layout::WRAP_WORD
252
+ elsif options[:wrap].eql?(:char)
253
+ layout.wrap = Pango::Layout::WRAP_CHAR
254
+ elsif options[:wrap].eql?(:wordchar)
255
+ layout.wrap = Pango::Layout::WRAP_WORD_CHAR
256
+ else
257
+ raise ArgumentError, "Invalid wrap technique requested"
258
+ end
259
+
260
+ # justify the text if need be - only works in pango >= 1.17
261
+ layout.justify = true if options[:justify]
262
+
263
+ # setup the font that will be used to render the text
264
+ fdesc = Pango::FontDescription.new(options[:font])
265
+ # font size should be specified in device points for simplicity's sake.
266
+ fdesc.size = device_y_to_user_y(options[:font_size] * Pango::SCALE)
267
+ layout.font_description = fdesc
268
+ @context.update_pango_layout(layout)
269
+
270
+ return layout
271
+ end
272
+
273
+ def default_text_options
274
+ { :font => @default_font,
275
+ :font_size => @default_font_size,
276
+ :alignment => :left,
277
+ :wrap => :wordchar,
278
+ :justify => false,
279
+ :spacing => 0,
280
+ :color => nil,
281
+ :markup => nil
282
+ }
283
+ end
284
+
285
+ # renders a pango layout onto our main context
286
+ # based on a function of the same name found in the text2.rb sample file
287
+ # distributed with rcairo - it's still black magic to me and has a few edge
288
+ # cases where it doesn't work too well. Needs to be improved.
289
+ def render_layout(layout, x, y, h, opts = {})
290
+ # we can't use context.show_pango_layout, as that won't start
291
+ # a new page if the layout hits the bottom margin. Instead,
292
+ # we iterate over each line of text in the layout and add it to
293
+ # the canvas, page breaking as necessary
294
+ options = {:auto_new_page => true }
295
+ options.merge!(opts)
296
+
297
+ offset = 0
298
+ baseline = 0
299
+
300
+ iter = layout.iter
301
+ loop do
302
+ line = iter.line
303
+ ink_rect, logical_rect = iter.line_extents
304
+
305
+ # calculate the relative starting co-ords of this line
306
+ baseline = iter.baseline / Pango::SCALE
307
+ linex = logical_rect.x / Pango::SCALE
308
+
309
+ if baseline - offset >= h
310
+ # our text is using the maximum amount of vertical space we want it to
311
+ if options[:auto_new_page]
312
+ # create a new page and we can continue adding text
313
+ offset = baseline
314
+ start_new_page
315
+ else
316
+ # the user doesn't want us to continue on the next page, so
317
+ # stop adding lines to the canvas
318
+ break
319
+ end
320
+ end
321
+
322
+ # move to the start of this line
323
+ @context.move_to(x + linex, y + baseline - offset)
324
+
325
+ # draw the line on the canvas
326
+ @context.show_pango_layout_line(line)
327
+
328
+ break unless iter.next_line!
329
+ end
330
+
331
+ # return the y co-ord we finished on
332
+ return device_y_to_user_y(y + baseline - offset)
333
+ end
334
+
335
+
336
+ end
337
+ end
Binary file
@@ -101,7 +101,7 @@ context "The PDF::Wrapper class" do
101
101
  w = h = 200
102
102
  r = 5
103
103
  pdf = PDF::Wrapper.new
104
- pdf.rounded_rectangle(x,y,w,h,r)
104
+ pdf.rectangle(x,y,w,h,:radius => r)
105
105
 
106
106
  receiver = PDF::Reader::RegisterReceiver.new
107
107
  reader = PDF::Reader.string(pdf.render, receiver)
@@ -118,7 +118,7 @@ context "The PDF::Wrapper class" do
118
118
  r = 5
119
119
  w = 5
120
120
  pdf = PDF::Wrapper.new
121
- pdf.rounded_rectangle(x,y,w,h,r, :line_width => w)
121
+ pdf.rounded_rectangle(x,y,w,h, :radius => r, :line_width => w)
122
122
 
123
123
  receiver = PDF::Reader::RegisterReceiver.new
124
124
  reader = PDF::Reader.string(pdf.render, receiver)
@@ -134,7 +134,7 @@ context "The PDF::Wrapper class" do
134
134
  w = h = 200
135
135
  r = 5
136
136
  pdf = PDF::Wrapper.new
137
- pdf.rounded_rectangle(x,y,w,h,r, :fill_color => :red)
137
+ pdf.rounded_rectangle(x,y,w,h, :radius => r, :fill_color => :red)
138
138
 
139
139
  receiver = PDF::Reader::RegisterReceiver.new
140
140
  reader = PDF::Reader.string(pdf.render, receiver)
data/specs/spec_helper.rb CHANGED
@@ -25,6 +25,11 @@ class PDF::Wrapper
25
25
  public :load_libpixbuf
26
26
  public :load_libpango
27
27
  public :load_libpoppler
28
+ public :user_x_to_device_x
29
+ public :user_y_to_device_y
30
+ public :user_to_device_dist
31
+ public :device_x_to_user_x
32
+ public :device_y_to_user_y
28
33
  public :validate_color
29
34
  end
30
35
 
@@ -52,7 +57,7 @@ class PageSizeReceiver
52
57
 
53
58
  # Called when page parsing ends
54
59
  def begin_page(args)
55
- pages << args["MediaBox"] || args[:MediaBox]
60
+ pages << (args["MediaBox"] || args[:MediaBox])
56
61
  end
57
62
  end
58
63
 
@@ -0,0 +1,111 @@
1
+ # coding: utf-8
2
+
3
+ require File.dirname(__FILE__) + '/spec_helper'
4
+
5
+ context "The PDF::Wrapper class" do
6
+ specify "should be able to draw a table on the canvas using an array of data" do
7
+ pdf = PDF::Wrapper.new
8
+ data = [%w{data1 data2}, %w{data3 data4}]
9
+ pdf.table(data)
10
+
11
+ receiver = PageTextReceiver.new
12
+ reader = PDF::Reader.string(pdf.render, receiver)
13
+
14
+ receiver.content.first.include?("data1").should be_true
15
+ receiver.content.first.include?("data2").should be_true
16
+ receiver.content.first.include?("data3").should be_true
17
+ receiver.content.first.include?("data4").should be_true
18
+ end
19
+
20
+ specify "should be able to draw a table on the canvas using a PDF::Wrapper::Table object" do
21
+ pdf = PDF::Wrapper.new
22
+ table = PDF::Wrapper::Table.new do |t|
23
+ t.data = [%w{data1 data2}, %w{data3 data4}]
24
+ end
25
+
26
+ pdf.table(table)
27
+
28
+ receiver = PageTextReceiver.new
29
+ reader = PDF::Reader.string(pdf.render, receiver)
30
+
31
+ receiver.content.first.include?("data1").should be_true
32
+ receiver.content.first.include?("data2").should be_true
33
+ receiver.content.first.include?("data3").should be_true
34
+ receiver.content.first.include?("data4").should be_true
35
+ end
36
+
37
+ specify "should be able to draw a table on the canvas with no headings" do
38
+ pdf = PDF::Wrapper.new
39
+
40
+ table = PDF::Wrapper::Table.new do |t|
41
+ t.data = (1..50).collect { [1,2] }
42
+ t.headers = ["col1", "col2"]
43
+ t.show_headers = nil
44
+ end
45
+
46
+ pdf.table(table)
47
+
48
+ receiver = PageTextReceiver.new
49
+ reader = PDF::Reader.string(pdf.render, receiver)
50
+
51
+ receiver.content.first.include?("col1").should be_false
52
+ receiver.content.first.include?("col2").should be_false
53
+ end
54
+
55
+ specify "should be able to draw a table on the canvas with headers on the first page only" do
56
+ pdf = PDF::Wrapper.new
57
+
58
+ table = PDF::Wrapper::Table.new do |t|
59
+ t.data = (1..50).collect { [1,2] }
60
+ t.headers = ["col1", "col2"]
61
+ t.show_headers = :once
62
+ end
63
+
64
+ pdf.table(table)
65
+
66
+ receiver = PageTextReceiver.new
67
+ reader = PDF::Reader.string(pdf.render, receiver)
68
+
69
+ receiver.content[0].include?("col1").should be_true
70
+ receiver.content[0].include?("col2").should be_true
71
+ receiver.content[1].include?("col1").should be_false
72
+ receiver.content[1].include?("col2").should be_false
73
+ end
74
+
75
+ specify "should be able to draw a table on the canvas with headers on all pages" do
76
+ pdf = PDF::Wrapper.new
77
+
78
+ table = PDF::Wrapper::Table.new do |t|
79
+ t.data = (1..50).collect { [1,2] }
80
+ t.headers = ["col1", "col2"]
81
+ t.show_headers = :page
82
+ end
83
+
84
+ pdf.table(table)
85
+
86
+ receiver = PageTextReceiver.new
87
+ reader = PDF::Reader.string(pdf.render, receiver)
88
+
89
+ receiver.content[0].include?("col1").should be_true
90
+ receiver.content[0].include?("col2").should be_true
91
+ receiver.content[1].include?("col1").should be_true
92
+ receiver.content[1].include?("col2").should be_true
93
+ end
94
+
95
+ specify "should leave the cursor in the bottom left when adding a table" do
96
+ pdf = PDF::Wrapper.new
97
+ data = [%w{head1 head2},%w{data1 data2}]
98
+ pdf.table(data, :left => pdf.margin_left)
99
+ x,y = pdf.current_point
100
+ x.to_i.should eql(pdf.margin_left)
101
+ end
102
+
103
+ specify "should default to using as much available space when adding a table that isn't left aligned with the left margin" do
104
+ pdf = PDF::Wrapper.new
105
+ data = [%w{head1 head2},%w{data1 data2}]
106
+ pdf.table(data, :left => 100)
107
+ x,y = pdf.current_point
108
+ x.to_i.should eql(100)
109
+ end
110
+
111
+ end