prawn-core 0.6.3 → 0.7.1

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 (43) hide show
  1. data/Rakefile +1 -1
  2. data/examples/general/context_sensitive_headers.rb +37 -0
  3. data/examples/general/float.rb +11 -0
  4. data/examples/general/repeaters.rb +43 -0
  5. data/examples/m17n/chinese_text_wrapping.rb +1 -3
  6. data/examples/text/font_calculations.rb +6 -6
  7. data/examples/text/text_box.rb +80 -17
  8. data/lib/prawn/core.rb +3 -1
  9. data/lib/prawn/document/bounding_box.rb +9 -0
  10. data/lib/prawn/document/column_box.rb +13 -2
  11. data/lib/prawn/document/internals.rb +21 -3
  12. data/lib/prawn/document/snapshot.rb +7 -2
  13. data/lib/prawn/document/span.rb +3 -3
  14. data/lib/prawn/document.rb +78 -19
  15. data/lib/prawn/font/afm.rb +10 -7
  16. data/lib/prawn/font/ttf.rb +6 -4
  17. data/lib/prawn/font.rb +34 -24
  18. data/lib/prawn/graphics/cap_style.rb +5 -2
  19. data/lib/prawn/graphics/color.rb +117 -57
  20. data/lib/prawn/graphics/dash.rb +4 -2
  21. data/lib/prawn/graphics/join_style.rb +6 -3
  22. data/lib/prawn/graphics/transparency.rb +65 -18
  23. data/lib/prawn/images/jpg.rb +1 -1
  24. data/lib/prawn/images/png.rb +1 -1
  25. data/lib/prawn/object_store.rb +30 -1
  26. data/lib/prawn/reference.rb +25 -3
  27. data/lib/prawn/repeater.rb +117 -0
  28. data/lib/prawn/stamp.rb +102 -40
  29. data/lib/prawn/text/box.rb +344 -0
  30. data/lib/prawn/text.rb +255 -0
  31. data/spec/document_spec.rb +125 -4
  32. data/spec/object_store_spec.rb +33 -0
  33. data/spec/repeater_spec.rb +79 -0
  34. data/spec/stamp_spec.rb +8 -0
  35. data/spec/text_box_spec.rb +282 -69
  36. data/spec/text_spec.rb +49 -29
  37. data/spec/transparency_spec.rb +14 -0
  38. data/vendor/pdf-inspector/lib/pdf/inspector/graphics.rb +2 -2
  39. metadata +158 -155
  40. data/examples/general/measurement_units.pdf +0 -4667
  41. data/lib/prawn/document/text/box.rb +0 -90
  42. data/lib/prawn/document/text/wrapping.rb +0 -62
  43. data/lib/prawn/document/text.rb +0 -184
@@ -11,9 +11,8 @@ require 'zlib'
11
11
  module Prawn
12
12
 
13
13
  class Reference #:nodoc:
14
-
15
- attr_accessor :gen, :data, :offset, :stream
16
- attr_reader :identifier
14
+
15
+ attr_accessor :gen, :data, :offset, :stream, :live, :identifier
17
16
 
18
17
  def initialize(id, data)
19
18
  @identifier = id
@@ -59,6 +58,29 @@ module Prawn
59
58
  @stream = other_ref.stream
60
59
  @compressed = other_ref.compressed?
61
60
  end
61
+
62
+ # Marks this and all referenced objects live, recursively.
63
+ def mark_live
64
+ return if @live
65
+ @live = true
66
+ referenced_objects.each { |o| o.mark_live }
67
+ end
68
+
69
+ private
70
+
71
+ # All objects referenced by this one. Used for GC.
72
+ def referenced_objects(obj=@data)
73
+ case obj
74
+ when Reference
75
+ []
76
+ when Hash
77
+ obj.values.map{|v| [v] + referenced_objects(v) }
78
+ when Array
79
+ obj.map{|v| [v] + referenced_objects(v) }
80
+ else []
81
+ end.flatten.grep(Reference)
82
+ end
83
+
62
84
  end
63
85
 
64
86
  module_function
@@ -0,0 +1,117 @@
1
+ # encoding: utf-8
2
+ #
3
+ # repeater.rb : Implements repeated page elements.
4
+ # Heavy inspired by repeating_element() in PDF::Wrapper
5
+ # http://pdf-wrapper.rubyforge.org/
6
+ #
7
+ # Copyright November 2009, Gregory Brown. All Rights Reserved.
8
+ #
9
+ # This is free software. Please see the LICENSE and COPYING files for details.
10
+
11
+ module Prawn
12
+
13
+ class Document
14
+
15
+ # A list of all repeaters in the document.
16
+ # See Document#repeat for details
17
+ #
18
+ def repeaters
19
+ @repeaters ||= []
20
+ end
21
+
22
+ # Provides a way to execute a block of code repeatedly based on a
23
+ # page_filter. Since Stamp is used under the hood, this method is very space
24
+ # efficient.
25
+ #
26
+ # Available page filters are:
27
+ # :all -- repeats on every page
28
+ # :odd -- repeats on odd pages
29
+ # :even -- repeats on even pages
30
+ # some_array -- repeats on every page listed in the array
31
+ # some_range -- repeats on every page included in the range
32
+ # some_lambda -- yields page number and repeats for true return values
33
+ #
34
+ # Example:
35
+ #
36
+ # Prawn::Document.generate("repeat.pdf", :skip_page_creation => true) do
37
+ #
38
+ # repeat :all do
39
+ # text "ALLLLLL", :at => bounds.top_left
40
+ # end
41
+ #
42
+ # repeat :odd do
43
+ # text "ODD", :at => [0,0]
44
+ # end
45
+ #
46
+ # repeat :even do
47
+ # text "EVEN", :at => [0,0]
48
+ # end
49
+ #
50
+ # repeat [1,2] do
51
+ # text "[1,2]", :at => [100,0]
52
+ # end
53
+ #
54
+ # repeat 2..4 do
55
+ # text "2..4", :at => [200,0]
56
+ # end
57
+ #
58
+ # repeat(lambda { |pg| pg % 3 == 0 }) do
59
+ # text "Every third", :at => [250, 20]
60
+ # end
61
+ #
62
+ # 10.times do
63
+ # start_new_page
64
+ # text "A wonderful page", :at => [400,400]
65
+ # end
66
+ #
67
+ # end
68
+ #
69
+ def repeat(page_filter, &block)
70
+ repeaters << Prawn::Repeater.new(self, page_filter, &block)
71
+ end
72
+ end
73
+
74
+ class Repeater #:nodoc:
75
+ class << self
76
+ attr_writer :count
77
+
78
+ def count
79
+ @count ||= 0
80
+ end
81
+ end
82
+
83
+ attr_reader :name
84
+
85
+ def initialize(document, page_filter, &block)
86
+ @document = document
87
+ @page_filter = page_filter
88
+ @stamp_name = "prawn_repeater(#{Repeater.count})"
89
+
90
+ @document.create_stamp(@stamp_name, &block)
91
+
92
+ Repeater.count += 1
93
+ end
94
+
95
+ def match?(page_number)
96
+ case @page_filter
97
+ when :all
98
+ true
99
+ when :odd
100
+ page_number % 2 == 1
101
+ when :even
102
+ page_number % 2 == 0
103
+ when Range, Array
104
+ @page_filter.include?(page_number)
105
+ when Proc
106
+ @page_filter.call(page_number)
107
+ end
108
+ end
109
+
110
+ def run(page_number)
111
+ @document.stamp(@stamp_name) if match?(page_number)
112
+ end
113
+
114
+ end
115
+ end
116
+
117
+
data/lib/prawn/stamp.rb CHANGED
@@ -8,25 +8,58 @@
8
8
  #
9
9
 
10
10
  module Prawn
11
- module Stamp
12
-
13
- def stamp(user_defined_name)
14
- raise Prawn::Errors::InvalidName if user_defined_name.empty?
15
- unless stamp_dictionary_registry[user_defined_name]
16
- raise Prawn::Errors::UndefinedObjectName
17
- end
18
-
19
- dict = stamp_dictionary_registry[user_defined_name]
20
11
 
21
- stamp_dictionary_name = dict[:stamp_dictionary_name]
22
- stamp_dictionary = dict[:stamp_dictionary]
12
+ # The Prawn::Stamp module is used to create content that will be
13
+ # included multiple times in a document. Using a stamp has three
14
+ # advantages over creating content anew each time it is placed on
15
+ # the page:
16
+ # i. faster document creation
17
+ # ii. smaller final document
18
+ # iii. faster display on subsequent displays of the repeated
19
+ # element because the viewer application can cache the rendered
20
+ # results
21
+ #
22
+ # Example:
23
+ # pdf.create_stamp("my_stamp") {
24
+ # pdf.fill_circle_at([10, 15], :radius => 5)
25
+ # pdf.text("hello world", :at => [20, 10])
26
+ # }
27
+ # pdf.stamp("my_stamp")
28
+ #
29
+ module Stamp
23
30
 
24
- add_content "/#{stamp_dictionary_name} Do"
25
-
26
- page_xobjects.merge!(stamp_dictionary_name => stamp_dictionary)
31
+ # Renders the stamp named <tt>name</tt> to the page
32
+ # raises <tt>Prawn::Errors::InvalidName</tt> if name.empty?
33
+ # raises <tt>Prawn::Errors::UndefinedObjectName</tt> if no stamp
34
+ # has been created with this name
35
+ #
36
+ # Example:
37
+ # pdf.create_stamp("my_stamp") {
38
+ # pdf.fill_circle_at([10, 15], :radius => 5)
39
+ # pdf.text("hello world", :at => [20, 10])
40
+ # }
41
+ # pdf.stamp("my_stamp")
42
+ #
43
+ def stamp(name)
44
+ dictionary_name, dictionary = stamp_dictionary(name)
45
+ add_content "/#{dictionary_name} Do"
46
+ page_xobjects.merge!(dictionary_name => dictionary)
27
47
  end
28
-
29
- def stamp_at(user_defined_name, point)
48
+
49
+ # Renders the stamp named <tt>name</tt> at a position offset from
50
+ # the initial coords at which the elements of the stamp was
51
+ # created
52
+ #
53
+ # Example:
54
+ # pdf.create_stamp("circle") do
55
+ # pdf.fill_circle_at([0, 0], :radius => 25)
56
+ # end
57
+ # # draws a circle at 100, 100
58
+ # pdf.stamp_at("circle", [100, 100])
59
+ #
60
+ # See stamp() for exceptions that might be raised
61
+ #
62
+ def stamp_at(name, point)
30
63
  # Save the graphics state
31
64
  add_content "q"
32
65
 
@@ -36,40 +69,36 @@ module Prawn
36
69
  add_content translate_position
37
70
 
38
71
  # Draw the stamp in the now translated user space
39
- stamp(user_defined_name)
72
+ stamp(name)
40
73
 
41
74
  # Restore the graphics state to remove the translation
42
75
  add_content "Q"
43
76
  end
44
-
45
- def create_stamp(user_defined_name="", &block)
46
- raise Prawn::Errors::InvalidName if user_defined_name.empty?
47
-
48
- if stamp_dictionary_registry[user_defined_name]
49
- raise Prawn::Errors::NameTaken
50
- end
51
77
 
52
- # BBox origin is the lower left margin of the page, so we need
53
- # it to be the full dimension of the page, or else things that
54
- # should appear near the top or right margin are invisible
55
- stamp_dictionary = ref!(:Type => :XObject,
56
- :Subtype => :Form,
57
- :BBox => [0, 0, page_dimensions[2], page_dimensions[3]])
78
+ # Creates a re-usable stamp named <tt>name</tt>
79
+ #
80
+ # raises <tt>Prawn::Errors::NameTaken</tt> if a stamp already
81
+ # exists in this document with this name
82
+ # raises <tt>Prawn::Errors::InvalidName</tt> if name.empty?
83
+ #
84
+ # Example:
85
+ # pdf.create_stamp("my_stamp") {
86
+ # pdf.fill_circle_at([10, 15], :radius => 5)
87
+ # pdf.text("hello world", :at => [20, 10])
88
+ # }
89
+ #
90
+ def create_stamp(name, &block)
91
+ dictionary = create_stamp_dictionary(name)
58
92
 
59
- stamp_dictionary_name = "Stamp#{next_stamp_dictionary_id}"
60
-
61
- stamp_dictionary_registry[user_defined_name] =
62
- { :stamp_dictionary_name => stamp_dictionary_name,
63
- :stamp_dictionary => stamp_dictionary}
64
-
65
-
66
93
  @active_stamp_stream = ""
67
- @active_stamp_dictionary = stamp_dictionary
94
+ @active_stamp_dictionary = dictionary
68
95
 
96
+ update_colors
69
97
  yield if block_given?
98
+ update_colors
70
99
 
71
- stamp_dictionary.data[:Length] = @active_stamp_stream.length + 1
72
- stamp_dictionary << @active_stamp_stream
100
+ dictionary.data[:Length] = @active_stamp_stream.length + 1
101
+ dictionary << @active_stamp_stream
73
102
 
74
103
  @active_stamp_stream = nil
75
104
  @active_stamp_dictionary = nil
@@ -85,5 +114,38 @@ module Prawn
85
114
  stamp_dictionary_registry.length + 1
86
115
  end
87
116
 
117
+ def stamp_dictionary(name)
118
+ raise Prawn::Errors::InvalidName if name.empty?
119
+ if stamp_dictionary_registry[name].nil?
120
+ raise Prawn::Errors::UndefinedObjectName
121
+ end
122
+
123
+ dict = stamp_dictionary_registry[name]
124
+
125
+ dictionary_name = dict[:stamp_dictionary_name]
126
+ dictionary = dict[:stamp_dictionary]
127
+ [dictionary_name, dictionary]
128
+ end
129
+
130
+ def create_stamp_dictionary(name)
131
+ raise Prawn::Errors::InvalidName if name.empty?
132
+ raise Prawn::Errors::NameTaken unless stamp_dictionary_registry[name].nil?
133
+ # BBox origin is the lower left margin of the page, so we need
134
+ # it to be the full dimension of the page, or else things that
135
+ # should appear near the top or right margin are invisible
136
+ dictionary = ref!(:Type => :XObject,
137
+ :Subtype => :Form,
138
+ :BBox => [0, 0,
139
+ page_dimensions[2], page_dimensions[3]])
140
+
141
+ dictionary_name = "Stamp#{next_stamp_dictionary_id}"
142
+
143
+ stamp_dictionary_registry[name] = {
144
+ :stamp_dictionary_name => dictionary_name,
145
+ :stamp_dictionary => dictionary
146
+ }
147
+ dictionary
148
+ end
149
+
88
150
  end
89
151
  end
@@ -0,0 +1,344 @@
1
+ # encoding: utf-8
2
+
3
+ # text/rectangle.rb : Implements text boxes
4
+ #
5
+ # Copyright November 2009, Daniel Nelson. All Rights Reserved.
6
+ #
7
+ # This is free software. Please see the LICENSE and COPYING files for details.
8
+
9
+ module Prawn
10
+ module Text
11
+
12
+ # Draws the requested text into a box. When the text overflows
13
+ # the rectangle, you can display ellipses, shrink to fit, or
14
+ # truncate the text. Text boxes are independent of the document
15
+ # y position.
16
+ #
17
+ # == Encoding
18
+ #
19
+ # Note that strings passed to this function should be encoded as UTF-8.
20
+ # If you get unexpected characters appearing in your rendered document,
21
+ # check this.
22
+ #
23
+ # If the current font is a built-in one, although the string must be
24
+ # encoded as UTF-8, only characters that are available in WinAnsi
25
+ # are allowed.
26
+ #
27
+ # If an empty box is rendered to your PDF instead of the character you
28
+ # wanted it usually means the current font doesn't include that character.
29
+ #
30
+ # == Options (default values marked in [])
31
+ #
32
+ # <tt>:kerning</tt>:: <tt>boolean</tt>. Whether or not to use kerning (if it
33
+ # is available with the current font) [true]
34
+ # <tt>:size</tt>:: <tt>number</tt>. The font size to use. [current font
35
+ # size]
36
+ # <tt>:style</tt>:: The style to use. The requested style must be part of
37
+ # the current font familly. [current style]
38
+ #
39
+ # <tt>:at</tt>:: <tt>[x, y]</tt>. The upper left corner of the box
40
+ # [@document.bounds.left, @document.bounds.top]
41
+ # <tt>:width</tt>:: <tt>number</tt>. The width of the box
42
+ # [@document.bounds.right - @at[0]]
43
+ # <tt>:height</tt>:: <tt>number</tt>. The height of the box [@at[1] -
44
+ # @document.bounds.bottom]
45
+ # <tt>:align</tt>:: <tt>:left</tt>, <tt>:center</tt>, or <tt>:right</tt>.
46
+ # Alignment within the bounding box [:left]
47
+ # <tt>:valign</tt>:: <tt>:top</tt>, <tt>:center</tt>, or <tt>:bottom</tt>.
48
+ # Vertical alignment within the bounding box [:top]
49
+ # <tt>:leading</tt>:: <tt>number</tt>. Additional space between lines [0]
50
+ # <tt>:overflow</tt>:: <tt>:truncate</tt>, <tt>:shrink_to_fit</tt>,
51
+ # <tt>:expand</tt>, or <tt>:ellipses</tt>. This
52
+ # controls the behavior when
53
+ # the amount of text exceeds the available space
54
+ # [:truncate]
55
+ # <tt>:min_font_size</tt>:: <tt>number</tt>. The minimum font size to use
56
+ # when :overflow is set to :shrink_to_fit (that is
57
+ # the font size will not be
58
+ # reduced to less than this value, even if it
59
+ # 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>:width</tt>:: the width available for the
69
+ # current line of text
70
+ # <tt>:document</tt>:: the pdf object
71
+ # <tt>:kerning</tt>:: boolean
72
+ # <tt>:size</tt>:: the font size
73
+ #
74
+ # Returns any text that did not print under the current settings
75
+ #
76
+ def text_box(text, options)
77
+ Text::Box.new(text, options.merge(:document => self)).render
78
+ end
79
+
80
+ # Generally, one would use the text_box convenience method. However, using
81
+ # Text::Box.new in conjunction with render() enables one to do look-ahead
82
+ # calculations prior to placing text on the page, or to determine how much
83
+ # vertical space was consumed by the printed text
84
+ #
85
+ class Box
86
+
87
+ # The text that was successfully printed (or, if <tt>dry_run</tt> was
88
+ # used, the test that would have been successfully printed)
89
+ attr_reader :text
90
+ # The upper left corner of the text box
91
+ attr_reader :at
92
+ # The line height of the last line printed
93
+ attr_reader :line_height
94
+ # The height of the ascender of the last line printed
95
+ attr_reader :ascender
96
+ # The height of the descender of the last line printed
97
+ attr_reader :descender
98
+ # The leading used during printing
99
+ attr_reader :leading
100
+
101
+ # See Prawn::Text#text_box for valid options
102
+ #
103
+ def initialize(text, options={})
104
+ @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
112
+
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
124
+
125
+ if @overflow == :expand
126
+ # if set to expand, then we simply set the bottom
127
+ # as the bottom of the document bounds, since that
128
+ # is the maximum we should expand to
129
+ @height = @at[1] - @document.bounds.bottom
130
+ @overflow = :truncate
131
+ end
132
+ @min_font_size = options[:min_font_size] || 5
133
+ @wrap_block = options [:wrap_block] || default_wrap_block
134
+ @options = @document.text_options.merge(:kerning => options[:kerning],
135
+ :size => options[:size],
136
+ :style => options[:style])
137
+ end
138
+
139
+ # Render text to the document based on the settings defined in initialize.
140
+ #
141
+ # In order to facilitate look-ahead calculations, <tt>render</tt> accepts
142
+ # a <tt>:dry_run => true</tt> option. If provided then everything is
143
+ # executed as if rendering, with the exception that nothing is drawn on
144
+ # the page. Useful for look-ahead computations of height, unprinted text,
145
+ # etc.
146
+ #
147
+ # Returns any text that did not print under the current settings
148
+ #
149
+ def render(flags={})
150
+ unprinted_text = ''
151
+ @document.save_font do
152
+ process_options
153
+
154
+ unless @document.skip_encoding
155
+ @document.font.normalize_encoding!(@text_to_print)
156
+ end
157
+
158
+ @document.font_size(@font_size) do
159
+ shrink_to_fit if @overflow == :shrink_to_fit
160
+ process_vertical_alignment
161
+ @inked = true unless flags[:dry_run]
162
+ unprinted_text = _render(@text_to_print)
163
+ @inked = false
164
+ end
165
+ end
166
+ unprinted_text
167
+ end
168
+
169
+ # The height actually used during the previous <tt>render</tt>
170
+ #
171
+ def height
172
+ return 0 if @baseline_y.nil? || @descender.nil?
173
+ # baseline is already pushed down one line below the current
174
+ # line, so we need to subtract line line_height and leading,
175
+ # but we need to add in the descender since baseline is
176
+ # above the descender
177
+ @baseline_y.abs + @descender - @line_height - @leading
178
+ end
179
+
180
+ private
181
+
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
192
+ return if @vertical_align == :top
193
+ _render(@text_to_print)
194
+ case @vertical_align
195
+ when :center
196
+ @at[1] = @at[1] - (@height - height) * 0.5
197
+ when :bottom
198
+ @at[1] = @at[1] - (@height - height)
199
+ end
200
+ @height = height
201
+ end
202
+
203
+ # Decrease the font size until the text fits or the min font
204
+ # size is reached
205
+ def shrink_to_fit
206
+ while (unprinted_text = _render(@text_to_print)).length > 0 &&
207
+ @font_size > @min_font_size
208
+ @font_size -= 0.5
209
+ @document.font_size = @font_size
210
+ end
211
+ end
212
+
213
+ def process_options
214
+ # must be performed within a save_font bock because
215
+ # document.process_text_options sets the font
216
+ @document.process_text_options(@options)
217
+ @font_size = @options[:size]
218
+ @kerning = @options[:kerning]
219
+ end
220
+
221
+ def _render(remaining_text)
222
+ @line_height = @document.font.height
223
+ @descender = @document.font.descender
224
+ @ascender = @document.font.ascender
225
+ @baseline_y = -@ascender
226
+
227
+ printed_text = []
228
+
229
+ while remaining_text &&
230
+ remaining_text.length > 0 &&
231
+ @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)
237
+ remaining_text = remaining_text.slice(line_to_print.length..
238
+ remaining_text.length)
239
+ print_ellipses = (@overflow == :ellipses && last_line? &&
240
+ remaining_text.length > 0)
241
+ printed_text << print_line(line_to_print, print_ellipses)
242
+ @baseline_y -= (@line_height + @leading)
243
+ end
244
+
245
+ @text = printed_text.join("\n") if @inked
246
+
247
+ remaining_text
248
+ end
249
+
250
+ def print_line(line_to_print, print_ellipses)
251
+ # strip so that trailing and preceding white space don't
252
+ # interfere with alignment
253
+ line_to_print.strip!
254
+
255
+ insert_ellipses(line_to_print) if print_ellipses
256
+
257
+ case(@align)
258
+ when :left
259
+ x = @center[0] - @width * 0.5
260
+ when :center
261
+ line_width = @document.width_of(line_to_print, :kerning => @kerning)
262
+ x = @center[0] - line_width * 0.5
263
+ when :right
264
+ line_width = @document.width_of(line_to_print, :kerning => @kerning)
265
+ x = @center[0] + @width * 0.5 - line_width
266
+ end
267
+
268
+ y = @at[1] + @baseline_y
269
+
270
+ if @inked
271
+ @document.text_at(line_to_print, :at => [x, y],
272
+ :size => @font_size, :kerning => @kerning)
273
+ end
274
+
275
+ line_to_print
276
+ end
277
+
278
+ def last_line?
279
+ @baseline_y.abs + @descender > @height - @line_height
280
+ end
281
+
282
+ def insert_ellipses(line_to_print)
283
+ if @document.width_of(line_to_print + "...",
284
+ :kerning => @kerning) < @width
285
+ line_to_print.insert(-1, "...")
286
+ else
287
+ line_to_print[-3..-1] = "..." if line_to_print.length > 3
288
+ end
289
+ end
290
+
291
+ def default_wrap_block
292
+ lambda do |line, options|
293
+ scan_pattern = /\S+|\s+/
294
+ output = ""
295
+ accumulated_width = 0
296
+ line.scan(scan_pattern).each do |segment|
297
+ segment_width = options[:document].width_of(segment,
298
+ :size => options[:size],
299
+ :kerning => options[:kerning])
300
+
301
+ if accumulated_width + segment_width <= options[:width]
302
+ accumulated_width += segment_width
303
+ output << segment
304
+ else
305
+ # if the line contains white space, don't split the
306
+ # final word that doesn't fit, just return what fits nicely
307
+ break if output =~ /\s/
308
+
309
+ # if there is no white space on the curren tline, then just
310
+ # print whatever part of the last segment that will fit on the
311
+ # line
312
+ begin
313
+ segment.unpack("U*").each do |char_int|
314
+ char = [char_int].pack("U")
315
+ accumulated_width += options[:document].width_of(char,
316
+ :size => options[:size],
317
+ :kerning => options[:kerning])
318
+ break if accumulated_width >= options[:width]
319
+ output << char
320
+ end
321
+ rescue
322
+ segment.each_char do |char|
323
+ accumulated_width += options[:document].width_of(char,
324
+ :size => options[:size],
325
+ :kerning => options[:kerning])
326
+ break if accumulated_width >= options[:width]
327
+ output << char
328
+ end
329
+ end
330
+ end
331
+ end
332
+ output
333
+ end
334
+ end
335
+ end
336
+ end
337
+ end
338
+
339
+
340
+ class String
341
+ def first_line
342
+ self.each_line { |line| return line }
343
+ end
344
+ end