prawn-core 0.7.2 → 0.8.4

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