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