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.
- data/Rakefile +1 -1
- data/examples/general/background.rb +1 -1
- data/examples/general/measurement_units.rb +2 -2
- data/examples/general/outlines.rb +50 -0
- data/examples/general/repeaters.rb +11 -7
- data/examples/general/stamp.rb +6 -6
- data/examples/graphics/basic_images.rb +1 -1
- data/examples/graphics/curves.rb +1 -1
- data/examples/graphics/rounded_polygons.rb +19 -0
- data/examples/graphics/rounded_rectangle.rb +20 -0
- data/examples/graphics/transformations.rb +52 -0
- data/examples/m17n/win_ansi_charset.rb +1 -1
- data/examples/text/font_calculations.rb +3 -3
- data/examples/text/indent_paragraphs.rb +18 -0
- data/examples/text/kerning.rb +4 -4
- data/examples/text/rotated.rb +98 -0
- data/examples/text/simple_text.rb +3 -3
- data/examples/text/simple_text_ttf.rb +1 -1
- data/lib/prawn/byte_string.rb +1 -0
- data/lib/prawn/core.rb +12 -5
- data/lib/prawn/core/object_store.rb +99 -0
- data/lib/prawn/core/page.rb +96 -0
- data/lib/prawn/core/text.rb +75 -0
- data/lib/prawn/document.rb +71 -78
- data/lib/prawn/document/annotations.rb +2 -2
- data/lib/prawn/document/bounding_box.rb +19 -9
- data/lib/prawn/document/column_box.rb +13 -12
- data/lib/prawn/document/graphics_state.rb +49 -0
- data/lib/prawn/document/internals.rb +5 -40
- data/lib/prawn/document/page_geometry.rb +1 -18
- data/lib/prawn/document/snapshot.rb +12 -7
- data/lib/prawn/errors.rb +18 -0
- data/lib/prawn/font.rb +4 -2
- data/lib/prawn/font/afm.rb +8 -0
- data/lib/prawn/font/dfont.rb +12 -4
- data/lib/prawn/font/ttf.rb +9 -0
- data/lib/prawn/graphics.rb +66 -9
- data/lib/prawn/graphics/color.rb +1 -1
- data/lib/prawn/graphics/transformation.rb +156 -0
- data/lib/prawn/graphics/transparency.rb +3 -7
- data/lib/prawn/images.rb +4 -3
- data/lib/prawn/images/png.rb +2 -2
- data/lib/prawn/outline.rb +278 -0
- data/lib/prawn/pdf_object.rb +5 -3
- data/lib/prawn/repeater.rb +25 -13
- data/lib/prawn/stamp.rb +6 -29
- data/lib/prawn/text.rb +139 -121
- data/lib/prawn/text/box.rb +168 -102
- data/spec/bounding_box_spec.rb +7 -2
- data/spec/document_spec.rb +7 -5
- data/spec/font_spec.rb +9 -1
- data/spec/graphics_spec.rb +229 -0
- data/spec/object_store_spec.rb +5 -5
- data/spec/outline_spec.rb +229 -0
- data/spec/repeater_spec.rb +18 -1
- data/spec/snapshot_spec.rb +7 -7
- data/spec/span_spec.rb +6 -2
- data/spec/spec_helper.rb +7 -3
- data/spec/stamp_spec.rb +13 -0
- data/spec/text_at_spec.rb +119 -0
- data/spec/text_box_spec.rb +257 -4
- data/spec/text_spec.rb +278 -180
- data/vendor/pdf-inspector/lib/pdf/inspector/graphics.rb +12 -0
- metadata +16 -3
- data/lib/prawn/object_store.rb +0 -92
data/lib/prawn/text/box.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
-
# text/
|
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>:
|
61
|
-
# wrapping
|
62
|
-
#
|
63
|
-
#
|
64
|
-
#
|
65
|
-
# <tt>
|
66
|
-
#
|
67
|
-
#
|
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
|
-
|
77
|
-
|
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(
|
128
|
+
def initialize(string, options={})
|
104
129
|
@inked = false
|
105
|
-
Prawn.verify_options(
|
106
|
-
options
|
107
|
-
@overflow
|
108
|
-
|
109
|
-
|
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
|
114
|
-
@at
|
115
|
-
|
116
|
-
@width
|
117
|
-
|
118
|
-
@height
|
119
|
-
|
120
|
-
@
|
121
|
-
@
|
122
|
-
@
|
123
|
-
@
|
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
|
-
@
|
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 @
|
155
|
-
@document.font.normalize_encoding!(
|
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
|
-
|
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
|
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(
|
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(
|
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 = @
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
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 = @
|
315
|
+
x = @at[0]
|
265
316
|
when :center
|
266
317
|
line_width = @document.width_of(line_to_print, :kerning => @kerning)
|
267
|
-
x = @
|
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 = @
|
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.
|
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
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
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
|
-
|
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
|
data/spec/bounding_box_spec.rb
CHANGED
@@ -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
|
|
data/spec/document_spec.rb
CHANGED
@@ -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.
|
184
|
-
pdf.
|
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.
|
194
|
-
pdf.
|
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::
|
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
|
+
|