prawn-core 0.7.2 → 0.8.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/Rakefile +1 -1
  2. data/examples/general/background.rb +1 -1
  3. data/examples/general/measurement_units.rb +2 -2
  4. data/examples/general/outlines.rb +50 -0
  5. data/examples/general/repeaters.rb +11 -7
  6. data/examples/general/stamp.rb +6 -6
  7. data/examples/graphics/basic_images.rb +1 -1
  8. data/examples/graphics/curves.rb +1 -1
  9. data/examples/graphics/rounded_polygons.rb +19 -0
  10. data/examples/graphics/rounded_rectangle.rb +20 -0
  11. data/examples/graphics/transformations.rb +52 -0
  12. data/examples/m17n/win_ansi_charset.rb +1 -1
  13. data/examples/text/font_calculations.rb +3 -3
  14. data/examples/text/indent_paragraphs.rb +18 -0
  15. data/examples/text/kerning.rb +4 -4
  16. data/examples/text/rotated.rb +98 -0
  17. data/examples/text/simple_text.rb +3 -3
  18. data/examples/text/simple_text_ttf.rb +1 -1
  19. data/lib/prawn/byte_string.rb +1 -0
  20. data/lib/prawn/core.rb +12 -5
  21. data/lib/prawn/core/object_store.rb +99 -0
  22. data/lib/prawn/core/page.rb +96 -0
  23. data/lib/prawn/core/text.rb +75 -0
  24. data/lib/prawn/document.rb +71 -78
  25. data/lib/prawn/document/annotations.rb +2 -2
  26. data/lib/prawn/document/bounding_box.rb +19 -9
  27. data/lib/prawn/document/column_box.rb +13 -12
  28. data/lib/prawn/document/graphics_state.rb +49 -0
  29. data/lib/prawn/document/internals.rb +5 -40
  30. data/lib/prawn/document/page_geometry.rb +1 -18
  31. data/lib/prawn/document/snapshot.rb +12 -7
  32. data/lib/prawn/errors.rb +18 -0
  33. data/lib/prawn/font.rb +4 -2
  34. data/lib/prawn/font/afm.rb +8 -0
  35. data/lib/prawn/font/dfont.rb +12 -4
  36. data/lib/prawn/font/ttf.rb +9 -0
  37. data/lib/prawn/graphics.rb +66 -9
  38. data/lib/prawn/graphics/color.rb +1 -1
  39. data/lib/prawn/graphics/transformation.rb +156 -0
  40. data/lib/prawn/graphics/transparency.rb +3 -7
  41. data/lib/prawn/images.rb +4 -3
  42. data/lib/prawn/images/png.rb +2 -2
  43. data/lib/prawn/outline.rb +278 -0
  44. data/lib/prawn/pdf_object.rb +5 -3
  45. data/lib/prawn/repeater.rb +25 -13
  46. data/lib/prawn/stamp.rb +6 -29
  47. data/lib/prawn/text.rb +139 -121
  48. data/lib/prawn/text/box.rb +168 -102
  49. data/spec/bounding_box_spec.rb +7 -2
  50. data/spec/document_spec.rb +7 -5
  51. data/spec/font_spec.rb +9 -1
  52. data/spec/graphics_spec.rb +229 -0
  53. data/spec/object_store_spec.rb +5 -5
  54. data/spec/outline_spec.rb +229 -0
  55. data/spec/repeater_spec.rb +18 -1
  56. data/spec/snapshot_spec.rb +7 -7
  57. data/spec/span_spec.rb +6 -2
  58. data/spec/spec_helper.rb +7 -3
  59. data/spec/stamp_spec.rb +13 -0
  60. data/spec/text_at_spec.rb +119 -0
  61. data/spec/text_box_spec.rb +257 -4
  62. data/spec/text_spec.rb +278 -180
  63. data/vendor/pdf-inspector/lib/pdf/inspector/graphics.rb +12 -0
  64. metadata +16 -3
  65. data/lib/prawn/object_store.rb +0 -92
@@ -1,6 +1,6 @@
1
1
  # encoding: utf-8
2
2
 
3
- # text/rectangle.rb : Implements text boxes
3
+ # text/box.rb : Implements text boxes
4
4
  #
5
5
  # Copyright November 2009, Daniel Nelson. All Rights Reserved.
6
6
  #
@@ -46,7 +46,15 @@ module Prawn
46
46
  # Alignment within the bounding box [:left]
47
47
  # <tt>:valign</tt>:: <tt>:top</tt>, <tt>:center</tt>, or <tt>:bottom</tt>.
48
48
  # Vertical alignment within the bounding box [:top]
49
+ # <tt>:rotate</tt>:: <tt>number</tt>. The angle to rotate the text
50
+ # <tt>:rotate_around</tt>:: <tt>:center</tt>, <tt>:upper_left</tt>,
51
+ # <tt>:upper_right</tt>, <tt>:lower_right</tt>,
52
+ # or <tt>:lower_left</tt>. The point around which
53
+ # to rotate the text [:upper_left]
49
54
  # <tt>:leading</tt>:: <tt>number</tt>. Additional space between lines [0]
55
+ # <tt>:single_line</tt>:: <tt>boolean</tt>. If true, then only the first
56
+ # line will be drawn [false]
57
+ # <tt>:skip_encoding</tt>:: <tt>boolean</tt> [false]
50
58
  # <tt>:overflow</tt>:: <tt>:truncate</tt>, <tt>:shrink_to_fit</tt>,
51
59
  # <tt>:expand</tt>, or <tt>:ellipses</tt>. This
52
60
  # controls the behavior when
@@ -57,24 +65,33 @@ module Prawn
57
65
  # the font size will not be
58
66
  # reduced to less than this value, even if it
59
67
  # means that some text will be cut off). [5]
60
- # <tt>:wrap_block</tt>:: <tt>proc</tt>. A proc used for custom line
61
- # wrapping. The proc must accept a single
62
- # <tt>line</tt> of text and an <tt>options</tt> hash
63
- # and return the string from that single line that
64
- # can fit on the line under the conditions defined by
65
- # <tt>options</tt>. If omitted, the default wrapping
66
- # proc is used. The options hash passed into the
67
- # wrap_block proc includes the following options:
68
+ # <tt>:line_wrap</tt>:: <tt>object</tt>. An object used for custom line
69
+ # wrapping on a case by case basis. Note that if you
70
+ # want to change wrapping document-wide, do
71
+ # pdf.default_line_wrap = MyLineWrap.new. Your custom
72
+ # object must have a wrap_line method that accept a
73
+ # single <tt>line</tt> of text and an
74
+ # <tt>options</tt> hash and returns the string from
75
+ # that single line that can fit on the line under
76
+ # the conditions defined by <tt>options</tt>. If
77
+ # omitted, the line wrap object is used.
78
+ # The options hash passed into the wrap_object proc
79
+ # includes the following options:
68
80
  # <tt>:width</tt>:: the width available for the
69
81
  # current line of text
70
82
  # <tt>:document</tt>:: the pdf object
71
83
  # <tt>:kerning</tt>:: boolean
72
84
  # <tt>:size</tt>:: the font size
73
85
  #
74
- # Returns any text that did not print under the current settings
86
+ # Returns any text that did not print under the current settings.
75
87
  #
76
- def text_box(text, options)
77
- Text::Box.new(text, options.merge(:document => self)).render
88
+ # NOTE: if an AFM font is used, then the returned text is encoded in
89
+ # WinAnsi. Subsequent calls to text_box that pass this returned text back
90
+ # into text box must include a :skip_encoding => true option. This is
91
+ # unnecessary when using TTF fonts because those operate on UTF-8 encoding.
92
+ #
93
+ def text_box(string, options)
94
+ Text::Box.new(string, options.merge(:document => self)).render
78
95
  end
79
96
 
80
97
  # Generally, one would use the text_box convenience method. However, using
@@ -83,6 +100,14 @@ module Prawn
83
100
  # vertical space was consumed by the printed text
84
101
  #
85
102
  class Box
103
+
104
+ VALID_OPTIONS = Prawn::Core::Text::VALID_OPTIONS +
105
+ [:at, :height, :width, :align, :valign,
106
+ :overflow, :min_font_size, :line_wrap,
107
+ :leading, :document, :rotate, :rotate_around,
108
+ :single_line, :skip_encoding]
109
+
110
+
86
111
 
87
112
  # The text that was successfully printed (or, if <tt>dry_run</tt> was
88
113
  # used, the test that would have been successfully printed)
@@ -100,27 +125,28 @@ module Prawn
100
125
 
101
126
  # See Prawn::Text#text_box for valid options
102
127
  #
103
- def initialize(text, options={})
128
+ def initialize(string, options={})
104
129
  @inked = false
105
- Prawn.verify_options(valid_options, options)
106
- options = options.dup
107
- @overflow = options[:overflow] || :truncate
108
- # we'll be messing with the strings encoding, don't change the user's
109
- # original string
110
- @text_to_print = text.dup
111
- @text = nil
130
+ Prawn.verify_options(VALID_OPTIONS, options)
131
+ options = options.dup
132
+ @overflow = options[:overflow] || :truncate
133
+ @original_string = string
134
+ @text = nil
112
135
 
113
- @document = options[:document]
114
- @at = options[:at] ||
115
- [@document.bounds.left, @document.bounds.top]
116
- @width = options[:width] ||
117
- @document.bounds.right - @at[0]
118
- @height = options[:height] ||
119
- @at[1] - @document.bounds.bottom
120
- @center = [@at[0] + @width * 0.5, @at[1] + @height * 0.5]
121
- @align = options[:align] || :left
122
- @vertical_align = options[:valign] || :top
123
- @leading = options[:leading] || 0
136
+ @document = options[:document]
137
+ @at = options[:at] ||
138
+ [@document.bounds.left, @document.bounds.top]
139
+ @width = options[:width] ||
140
+ @document.bounds.right - @at[0]
141
+ @height = options[:height] ||
142
+ @at[1] - @document.bounds.bottom
143
+ @align = options[:align] || :left
144
+ @vertical_align = options[:valign] || :top
145
+ @leading = options[:leading] || 0
146
+ @rotate = options[:rotate] || 0
147
+ @rotate_around = options[:rotate_around] || :upper_left
148
+ @single_line = options[:single_line]
149
+ @skip_encoding = options[:skip_encoding] || @document.skip_encoding
124
150
 
125
151
  if @overflow == :expand
126
152
  # if set to expand, then we simply set the bottom
@@ -130,7 +156,7 @@ module Prawn
130
156
  @overflow = :truncate
131
157
  end
132
158
  @min_font_size = options[:min_font_size] || 5
133
- @wrap_block = options [:wrap_block] || default_wrap_block
159
+ @line_wrap = options [:line_wrap] || @document.default_line_wrap
134
160
  @options = @document.text_options.merge(:kerning => options[:kerning],
135
161
  :size => options[:size],
136
162
  :style => options[:style])
@@ -147,19 +173,25 @@ module Prawn
147
173
  # Returns any text that did not print under the current settings
148
174
  #
149
175
  def render(flags={})
176
+ # dup because normalize_encoding changes the string
177
+ string = @original_string.dup
150
178
  unprinted_text = ''
151
179
  @document.save_font do
152
180
  process_options
153
181
 
154
- unless @document.skip_encoding
155
- @document.font.normalize_encoding!(@text_to_print)
182
+ unless @skip_encoding
183
+ @document.font.normalize_encoding!(string)
156
184
  end
157
185
 
158
186
  @document.font_size(@font_size) do
159
- shrink_to_fit if @overflow == :shrink_to_fit
160
- process_vertical_alignment
187
+ shrink_to_fit(string) if @overflow == :shrink_to_fit
188
+ process_vertical_alignment(string)
161
189
  @inked = true unless flags[:dry_run]
162
- unprinted_text = _render(@text_to_print)
190
+ if @rotate != 0 && @inked
191
+ unprinted_text = render_rotated(string)
192
+ else
193
+ unprinted_text = _render(string)
194
+ end
163
195
  @inked = false
164
196
  end
165
197
  end
@@ -179,18 +211,9 @@ module Prawn
179
211
 
180
212
  private
181
213
 
182
- def valid_options
183
- Text::VALID_TEXT_OPTIONS.dup.concat([:at, :height, :width,
184
- :align, :valign,
185
- :overflow, :min_font_size,
186
- :wrap_block,
187
- :leading,
188
- :document])
189
- end
190
-
191
- def process_vertical_alignment
214
+ def process_vertical_alignment(string)
192
215
  return if @vertical_align == :top
193
- _render(@text_to_print)
216
+ _render(string)
194
217
  case @vertical_align
195
218
  when :center
196
219
  @at[1] = @at[1] - (@height - height) * 0.5
@@ -202,8 +225,8 @@ module Prawn
202
225
 
203
226
  # Decrease the font size until the text fits or the min font
204
227
  # size is reached
205
- def shrink_to_fit
206
- while (unprinted_text = _render(@text_to_print)).length > 0 &&
228
+ def shrink_to_fit(string)
229
+ while (unprinted_text = _render(string)).length > 0 &&
207
230
  @font_size > @min_font_size
208
231
  @font_size -= 0.5
209
232
  @document.font_size = @font_size
@@ -218,6 +241,33 @@ module Prawn
218
241
  @kerning = @options[:kerning]
219
242
  end
220
243
 
244
+ def render_rotated(string)
245
+ unprinted_text = ''
246
+
247
+ case @rotate_around
248
+ when :center
249
+ x = @at[0] + @width * 0.5
250
+ y = @at[1] - @height * 0.5
251
+ when :upper_right
252
+ x = @at[0] + @width
253
+ y = @at[1]
254
+ when :lower_right
255
+ x = @at[0] + @width
256
+ y = @at[1] - @height
257
+ when :lower_left
258
+ x = @at[0]
259
+ y = @at[1] - @height
260
+ else
261
+ x = @at[0]
262
+ y = @at[1]
263
+ end
264
+
265
+ @document.rotate(@rotate, :origin => [x, y]) do
266
+ unprinted_text = _render(string)
267
+ end
268
+ unprinted_text
269
+ end
270
+
221
271
  def _render(remaining_text)
222
272
  @line_height = @document.font.height
223
273
  @descender = @document.font.descender
@@ -229,11 +279,11 @@ module Prawn
229
279
  while remaining_text &&
230
280
  remaining_text.length > 0 &&
231
281
  @baseline_y.abs + @descender <= @height
232
- line_to_print = @wrap_block.call(remaining_text.first_line,
233
- :document => @document,
234
- :kerning => @kerning,
235
- :size => @font_size,
236
- :width => @width)
282
+ line_to_print = @line_wrap.wrap_line(remaining_text.first_line,
283
+ :document => @document,
284
+ :kerning => @kerning,
285
+ :size => @font_size,
286
+ :width => @width)
237
287
 
238
288
  if line_to_print.empty? && remaining_text.length > 0
239
289
  raise Errors::CannotFit
@@ -245,6 +295,7 @@ module Prawn
245
295
  remaining_text.length > 0)
246
296
  printed_text << print_line(line_to_print, print_ellipses)
247
297
  @baseline_y -= (@line_height + @leading)
298
+ break if @single_line
248
299
  end
249
300
 
250
301
  @text = printed_text.join("\n") if @inked
@@ -261,19 +312,19 @@ module Prawn
261
312
 
262
313
  case(@align)
263
314
  when :left
264
- x = @center[0] - @width * 0.5
315
+ x = @at[0]
265
316
  when :center
266
317
  line_width = @document.width_of(line_to_print, :kerning => @kerning)
267
- x = @center[0] - line_width * 0.5
318
+ x = @at[0] + @width * 0.5 - line_width * 0.5
268
319
  when :right
269
320
  line_width = @document.width_of(line_to_print, :kerning => @kerning)
270
- x = @center[0] + @width * 0.5 - line_width
321
+ x = @at[0] + @width - line_width
271
322
  end
272
323
 
273
324
  y = @at[1] + @baseline_y
274
325
 
275
326
  if @inked
276
- @document.text_at(line_to_print, :at => [x, y],
327
+ @document.draw_text!(line_to_print, :at => [x, y],
277
328
  :size => @font_size, :kerning => @kerning)
278
329
  end
279
330
 
@@ -292,53 +343,68 @@ module Prawn
292
343
  line_to_print[-3..-1] = "..." if line_to_print.length > 3
293
344
  end
294
345
  end
346
+ end
347
+
348
+ class LineWrap
349
+ def wrap_line(line, options)
350
+ @document = options[:document]
351
+ @size = options[:size]
352
+ @kerning = options[:kerning]
353
+ @width = options[:width]
354
+ @accumulated_width = 0
355
+ @output = ""
356
+
357
+ scan_pattern = @document.font.unicode? ? /\S+|\s+/ : /\S+|\s+/n
358
+ space_scan_pattern = @document.font.unicode? ? /\s/ : /\s/n
359
+
360
+ line.scan(scan_pattern).each do |segment|
361
+ # yes, this block could be split out into another method, but it is
362
+ # called on every word printed, so I'm keeping it here for speed
363
+
364
+ segment_width = @document.width_of(segment,
365
+ :size => @size,
366
+ :kerning => @kerning)
367
+
368
+ if @accumulated_width + segment_width <= @width
369
+ @accumulated_width += segment_width
370
+ @output << segment
371
+ else
372
+ # if the line contains white space, don't split the
373
+ # final word that doesn't fit, just return what fits nicely
374
+ break if @output =~ space_scan_pattern
375
+ wrap_by_char(segment)
376
+ break
377
+ end
378
+ end
379
+ @output
380
+ end
295
381
 
296
- def default_wrap_block
297
- lambda do |line, options|
298
- scan_pattern = options[:document].font.unicode? ? /\S+|\s+/ : /\S+|\s+/n
299
- space_scan_pattern = options[:document].font.unicode? ? /\s/ : /\s/n
300
- output = ""
301
- accumulated_width = 0
302
- line.scan(scan_pattern).each do |segment|
303
- segment_width = options[:document].width_of(segment,
304
- :size => options[:size],
305
- :kerning => options[:kerning])
306
-
307
- if accumulated_width + segment_width <= options[:width]
308
- accumulated_width += segment_width
309
- output << segment
310
- else
311
- # if the line contains white space, don't split the
312
- # final word that doesn't fit, just return what fits nicely
313
- break if output =~ space_scan_pattern
314
-
315
- # if there is no white space on the current line, then just
316
- # print whatever part of the last segment that will fit on the
317
- # line
318
- begin
319
- segment.unpack("U*").each do |char_int|
320
- char = [char_int].pack("U")
321
- accumulated_width += options[:document].width_of(char,
322
- :size => options[:size],
323
- :kerning => options[:kerning])
324
- break if accumulated_width >= options[:width]
325
- output << char
326
- end
327
- rescue
328
- # not valid unicode
329
- segment.each_char do |char|
330
- accumulated_width += options[:document].width_of(char,
331
- :size => options[:size],
332
- :kerning => options[:kerning])
333
- break if accumulated_width >= options[:width]
334
- output << char
335
- end
336
- end
337
- end
382
+ private
383
+
384
+ def wrap_by_char(segment)
385
+ if @document.font.unicode?
386
+ segment.unpack("U*").each do |char_int|
387
+ return unless append_char([char_int].pack("U"))
388
+ end
389
+ else
390
+ segment.each_char do |char|
391
+ return unless append_char(char)
338
392
  end
339
- output
393
+ end
394
+ end
395
+
396
+ def append_char(char)
397
+ @accumulated_width += @document.width_of(char,
398
+ :size => @size,
399
+ :kerning => @kerning)
400
+ if @accumulated_width >= @width
401
+ false
402
+ else
403
+ @output << char
404
+ true
340
405
  end
341
406
  end
342
407
  end
408
+
343
409
  end
344
410
  end
@@ -80,7 +80,12 @@ describe "A bounding box" do
80
80
  it "should have an absolute top-right of [x+width,y]" do
81
81
  @box.absolute_top_right.should == [@x + @width, @y]
82
82
  end
83
-
83
+
84
+ it "should require width to be set" do
85
+ assert_raises(ArgumentError) do
86
+ Prawn::Document::BoundingBox.new(nil, [100,100])
87
+ end
88
+ end
84
89
 
85
90
  end
86
91
 
@@ -91,7 +96,7 @@ describe "drawing bounding boxes" do
91
96
  it "should restore the margin box when bounding box exits" do
92
97
  margin_box = @pdf.bounds
93
98
 
94
- @pdf.bounding_box [100,500] do
99
+ @pdf.bounding_box [100,500], :width => 100 do
95
100
  #nothing
96
101
  end
97
102
 
@@ -180,8 +180,8 @@ describe "Document compression" do
180
180
  it "should not compress the page content stream if compression is disabled" do
181
181
 
182
182
  pdf = Prawn::Document.new(:compress => false)
183
- pdf.page_content.stubs(:compress_stream).returns(true)
184
- pdf.page_content.expects(:compress_stream).never
183
+ pdf.page.content.stubs(:compress_stream).returns(true)
184
+ pdf.page.content.expects(:compress_stream).never
185
185
 
186
186
  pdf.text "Hi There" * 20
187
187
  pdf.render
@@ -190,8 +190,8 @@ describe "Document compression" do
190
190
  it "should compress the page content stream if compression is enabled" do
191
191
 
192
192
  pdf = Prawn::Document.new(:compress => true)
193
- pdf.page_content.stubs(:compress_stream).returns(true)
194
- pdf.page_content.expects(:compress_stream).once
193
+ pdf.page.content.stubs(:compress_stream).returns(true)
194
+ pdf.page.content.expects(:compress_stream).once
195
195
 
196
196
  pdf.text "Hi There" * 20
197
197
  pdf.render
@@ -311,7 +311,7 @@ describe "The group() feature" do
311
311
  100.times { text "Too long" }
312
312
  end
313
313
  end.render
314
- }.should.raise(Prawn::Document::CannotGroup)
314
+ }.should.raise(Prawn::Errors::CannotGroup)
315
315
  end
316
316
 
317
317
  it "should group within individual column boxes" do
@@ -403,3 +403,5 @@ describe "PDF file versions" do
403
403
  str[0,8].should == "%PDF-1.4"
404
404
  end
405
405
  end
406
+
407
+