prawn-core 0.6.3 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
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
data/lib/prawn/text.rb ADDED
@@ -0,0 +1,255 @@
1
+ # encoding: utf-8
2
+
3
+ # text.rb : Implements PDF text primitives
4
+ #
5
+ # Copyright May 2008, Gregory Brown. All Rights Reserved.
6
+ #
7
+ # This is free software. Please see the LICENSE and COPYING files for details.
8
+ require "zlib"
9
+ require "prawn/text/box"
10
+
11
+ module Prawn
12
+ module Text
13
+ attr_reader :text_options
14
+ attr_reader :skip_encoding
15
+
16
+ ruby_18 { $KCODE="U" }
17
+
18
+ # Gets height of text in PDF points. See text() for valid options.
19
+ #
20
+ def height_of(string, options={})
21
+ box = Text::Box.new(string,
22
+ options.merge(:height => 100000000,
23
+ :document => self))
24
+ box.render(:dry_run => true)
25
+ height = box.height - box.descender
26
+ height += box.line_height + box.leading - box.ascender # if final_gap
27
+ height
28
+ end
29
+
30
+ # If you want text to flow onto a new page or between columns, this is the
31
+ # method to use. If, instead, if you want to place bounded text outside of
32
+ # the flow of a document (for captions, labels, charts, etc.), use Text::Box
33
+ # or its convenience method text_box.
34
+ #
35
+ # Draws text on the page. If a point is specified via the <tt>:at</tt>
36
+ # option the text will begin exactly at that point, and the string is
37
+ # assumed to be pre-formatted to properly fit the page.
38
+ #
39
+ # pdf.text "Hello World", :at => [100,100]
40
+ # pdf.text "Goodbye World", :at => [50,50], :size => 16
41
+ #
42
+ # When <tt>:at</tt> is not specified, Prawn attempts to wrap the text to
43
+ # fit within your current bounding box (or margin_box if no bounding box
44
+ # is being used ). Text will flow onto the next page when it reaches
45
+ # the bottom of the bounding box. Text wrap in Prawn does not re-flow
46
+ # linebreaks, so if you want fully automated text wrapping, be sure to
47
+ # remove newlines before attempting to draw your string.
48
+ #
49
+ # pdf.text "Will be wrapped when it hits the edge of your bounding box"
50
+ # pdf.text "This will be centered", :align => :center
51
+ # pdf.text "This will be right aligned", :align => :right
52
+ #
53
+ # If your font contains kerning pairs data that Prawn can parse, the
54
+ # text will be kerned by default. You can disable this feature by passing
55
+ # <tt>:kerning => false</tt>.
56
+ #
57
+ # === Text Positioning Details:
58
+ #
59
+ # When using the :at parameter, Prawn will position your text by the
60
+ # left-most edge of its baseline, and flow along a single line. (This
61
+ # means that :align will not work)
62
+ #
63
+ # Otherwise, the text is positioned at font.ascender below the baseline,
64
+ # making it easy to use this method within bounding boxes and spans.
65
+ #
66
+ # == Rotation
67
+ #
68
+ # Text can be rotated before it is placed on the canvas by specifying the
69
+ # <tt>:rotate</tt> option with a given angle. Rotation occurs counter-clockwise.
70
+ # Note that <tt>:rotate</tt> is only compatible when using the <tt>:at</tt> option
71
+ #
72
+ # == Encoding
73
+ #
74
+ # Note that strings passed to this function should be encoded as UTF-8.
75
+ # If you get unexpected characters appearing in your rendered document,
76
+ # check this.
77
+ #
78
+ # If the current font is a built-in one, although the string must be
79
+ # encoded as UTF-8, only characters that are available in WinAnsi
80
+ # are allowed.
81
+ #
82
+ # If an empty box is rendered to your PDF instead of the character you
83
+ # wanted it usually means the current font doesn't include that character.
84
+ #
85
+ # == Options (default values marked in [])
86
+ #
87
+ # <tt>:kerning</tt>:: <tt>boolean</tt>. Whether or not to use kerning (if it
88
+ # is available with the current font) [true]
89
+ # <tt>:size</tt>:: <tt>number</tt>. The font size to use. [current font
90
+ # size]
91
+ # <tt>:style</tt>:: The style to use. The requested style must be part of
92
+ # the current font familly. [current style]
93
+ #
94
+ # === Additional options available when <tt>:at</tt> option is provided
95
+ #
96
+ # <tt>:at</tt>:: <tt>[x, y]</tt>. The position at which to start the text
97
+ # <tt>:rotate</tt>:: <tt>number</tt>. The angle to which to rotate text
98
+ #
99
+ # === Additional options available when <tt>:at</tt> option is omitted
100
+ #
101
+ # <tt>:align</tt>:: <tt>:left</tt>, <tt>:center</tt>, or <tt>:right</tt>.
102
+ # Alignment within the bounding box [:left]
103
+ # <tt>:valign</tt>:: <tt>:top</tt>, <tt>:center</tt>, or <tt>:bottom</tt>.
104
+ # Vertical alignment within the bounding box [:top]
105
+ # <tt>:leading</tt>:: <tt>number</tt>. Additional space between lines [0]
106
+ # <tt>:final_gap</tt>:: <tt>boolean</tt>. If true, then the space between
107
+ # each line is included below the last line;
108
+ # otherwise, document.y is placed just below the
109
+ # descender of the last line printed [true]
110
+ # <tt>:wrap_block</tt>:: <tt>proc</tt>. A proc used for custom line
111
+ # wrapping. The proc must accept a single
112
+ # <tt>line</tt> of text and an <tt>options</tt> hash
113
+ # and return the string from that single line that
114
+ # can fit on the line under the conditions defined by
115
+ # <tt>options</tt>. If omitted, the default wrapping
116
+ # proc is used. The options hash passed into the
117
+ # wrap_block proc includes the following options:
118
+ # <tt>:width</tt>:: the width available for the
119
+ # current line of text
120
+ # <tt>:document</tt>:: the pdf object
121
+ # <tt>:kerning</tt>:: boolean
122
+ # <tt>:size</tt>:: the font size
123
+ #
124
+ # Raises <tt>ArgumentError</tt> if both <tt>:at</tt> and <tt>:align</tt>
125
+ # options included
126
+ #
127
+ # Raises <tt>ArgumentError</tt> if <tt>:rotate</tt> option included, but
128
+ # <tt>:at</tt> option omitted
129
+ #
130
+ def text(text, options={})
131
+ # we might modify the options. don't change the user's hash
132
+ options = options.dup
133
+ if options[:at]
134
+ inspect_options_for_text_at(options)
135
+ # we'll be messing with the strings encoding, don't change the user's
136
+ # original string
137
+ text = text.to_s.dup
138
+ options = @text_options.merge(options)
139
+ save_font do
140
+ process_text_options(options)
141
+ font.normalize_encoding!(text) unless @skip_encoding
142
+ font_size(options[:size]) { text_at(text, options) }
143
+ end
144
+ else
145
+ remaining_text = fill_text_box(text, options)
146
+ while remaining_text.length > 0
147
+ @bounding_box.move_past_bottom
148
+ previous_remaining_text = remaining_text
149
+ remaining_text = fill_text_box(remaining_text, options)
150
+ break if remaining_text == previous_remaining_text
151
+ end
152
+ end
153
+ end
154
+
155
+ # Low level text placement method. All font and size alterations
156
+ # should already be set
157
+ #
158
+ def text_at(text, options)
159
+ x,y = translate(options[:at])
160
+ add_text_content(text,x,y,options)
161
+ end
162
+
163
+ # These should be used as a base. Extensions may build on this list
164
+ VALID_TEXT_OPTIONS = [:kerning, :size, :style]
165
+
166
+ # Low level call to set the current font style and extract text options from
167
+ # an options hash. Should be called from within a save_font block
168
+ #
169
+ def process_text_options(options)
170
+ if options[:style]
171
+ raise "Bad font family" unless font.family
172
+ font(font.family, :style => options[:style])
173
+ end
174
+
175
+ # must compare against false to keep kerning on as default
176
+ unless options[:kerning] == false
177
+ options[:kerning] = font.has_kerning_data?
178
+ end
179
+
180
+ options[:size] ||= font_size
181
+ end
182
+
183
+ private
184
+
185
+ def fill_text_box(text, options)
186
+ final_gap = inspect_options_for_text_box(options)
187
+ bottom = @bounding_box.stretchy? ? @margin_box.absolute_bottom :
188
+ @bounding_box.absolute_bottom
189
+
190
+ options[:height] = y - bottom
191
+ options[:width] = bounds.width
192
+ options[:at] = [@bounding_box.left_side - @bounding_box.absolute_left,
193
+ y - @bounding_box.absolute_bottom]
194
+
195
+ box = Text::Box.new(text, options)
196
+ remaining_text = box.render
197
+
198
+ self.y -= box.height - box.descender
199
+ self.y -= box.line_height + box.leading - box.ascender if final_gap
200
+
201
+ remaining_text
202
+ end
203
+
204
+ def inspect_options_for_text_at(options)
205
+ if options[:align]
206
+ raise ArgumentError, "The :align option does not work with :at"
207
+ end
208
+ valid_options = VALID_TEXT_OPTIONS.dup.concat([:at, :rotate])
209
+ Prawn.verify_options(valid_options, options)
210
+ end
211
+
212
+ def inspect_options_for_text_box(options)
213
+ if options[:rotate]
214
+ raise ArgumentError, "Rotated text may only be used with :at"
215
+ end
216
+ options.merge!(:document => self)
217
+ final_gap = options[:final_gap].nil? ? true : options[:final_gap]
218
+ options.delete(:final_gap)
219
+ final_gap
220
+ end
221
+
222
+ def move_text_position(dy)
223
+ bottom = @bounding_box.stretchy? ? @margin_box.absolute_bottom :
224
+ @bounding_box.absolute_bottom
225
+
226
+ @bounding_box.move_past_bottom if (y - dy) < bottom
227
+
228
+ self.y -= dy
229
+ end
230
+
231
+ def add_text_content(text, x, y, options)
232
+ chunks = font.encode_text(text,options)
233
+
234
+ add_content "\nBT"
235
+
236
+ if options[:rotate]
237
+ rad = options[:rotate].to_i * Math::PI / 180
238
+ arr = [ Math.cos(rad), Math.sin(rad), -Math.sin(rad), Math.cos(rad), x, y ]
239
+ add_content "%.3f %.3f %.3f %.3f %.3f %.3f Tm" % arr
240
+ else
241
+ add_content "#{x} #{y} Td"
242
+ end
243
+
244
+ chunks.each do |(subset, string)|
245
+ font.add_to_current_page(subset)
246
+ add_content "/#{font.identifier_for(subset)} #{font_size} Tf"
247
+
248
+ operation = options[:kerning] && string.is_a?(Array) ? "TJ" : "Tj"
249
+ add_content Prawn::PdfObject(string, true) << " " << operation
250
+ end
251
+
252
+ add_content "ET\n"
253
+ end
254
+ end
255
+ end
@@ -55,7 +55,6 @@ describe "when generating a document from a subclass" do
55
55
 
56
56
  end
57
57
 
58
-
59
58
  describe "When creating multi-page documents" do
60
59
 
61
60
  before(:each) { create_pdf }
@@ -94,6 +93,84 @@ describe "When beginning each new page" do
94
93
  @pdf.instance_variable_defined?(:@background).should == true
95
94
  @pdf.instance_variable_get(:@background).should == @filename
96
95
  end
96
+
97
+
98
+ end
99
+
100
+
101
+ end
102
+
103
+ describe "The page_number method" do
104
+ it "should be 1 for a new document" do
105
+ pdf = Prawn::Document.new
106
+ pdf.page_number.should == 1
107
+ end
108
+
109
+ it "should be 0 for documents with no pages" do
110
+ pdf = Prawn::Document.new(:skip_page_creation => true)
111
+ pdf.page_number.should == 0
112
+ end
113
+
114
+ it "should be changed by go_to_page" do
115
+ pdf = Prawn::Document.new
116
+ 10.times { pdf.start_new_page }
117
+ pdf.go_to_page 3
118
+ pdf.page_number.should == 3
119
+ end
120
+
121
+ end
122
+
123
+ describe "on_page_create callback" do
124
+ before do
125
+ create_pdf
126
+ end
127
+
128
+ it "should be invoked with document" do
129
+ called_with = nil
130
+
131
+ @pdf.on_page_create { |*args| called_with = args }
132
+
133
+ @pdf.start_new_page
134
+
135
+ called_with.should == [@pdf]
136
+ end
137
+
138
+ it "should be invoked for each new page" do
139
+ trigger = mock()
140
+ trigger.expects(:fire).times(5)
141
+
142
+ @pdf.on_page_create { trigger.fire }
143
+
144
+ 5.times { @pdf.start_new_page }
145
+ end
146
+
147
+ it "should be replaceable" do
148
+ trigger1 = mock()
149
+ trigger1.expects(:fire).times(1)
150
+
151
+ trigger2 = mock()
152
+ trigger2.expects(:fire).times(1)
153
+
154
+ @pdf.on_page_create { trigger1.fire }
155
+
156
+ @pdf.start_new_page
157
+
158
+ @pdf.on_page_create { trigger2.fire }
159
+
160
+ @pdf.start_new_page
161
+ end
162
+
163
+ it "should be clearable by calling on_page_create without a block" do
164
+ trigger = mock()
165
+ trigger.expects(:fire).times(1)
166
+
167
+ @pdf.on_page_create { trigger.fire }
168
+
169
+ @pdf.start_new_page
170
+
171
+ @pdf.on_page_create
172
+
173
+ @pdf.start_new_page
97
174
  end
98
175
 
99
176
  end
@@ -148,6 +225,21 @@ describe "When reopening pages" do
148
225
  lambda{ PDF::Inspector::Page.analyze(@pdf.render) }.
149
226
  should.not.raise(PDF::Reader::MalformedPDFError)
150
227
  end
228
+
229
+ it "should insert pages after the current page when calling start_new_page" do
230
+ pdf = Prawn::Document.new
231
+ 3.times { |i| pdf.text "Old page #{i+1}"; pdf.start_new_page }
232
+ pdf.go_to_page 1
233
+ pdf.start_new_page
234
+ pdf.text "New page 2"
235
+
236
+ pdf.page_number.should == 2
237
+
238
+ pages = PDF::Inspector::Page.analyze(pdf.render).pages
239
+ pages.size.should == 5
240
+ pages[1][:strings].should == ["New page 2"]
241
+ pages[2][:strings].should == ["Old page 2"]
242
+ end
151
243
  end
152
244
 
153
245
  describe "When setting page size" do
@@ -255,17 +347,46 @@ describe "The render() feature" do
255
347
  pdf = Prawn::Document.new
256
348
 
257
349
  seq = sequence("callback_order")
258
-
350
+
259
351
  # Verify the order: finalize -> fire callbacks -> render body
260
352
  pdf.expects(:finalize_all_page_contents).in_sequence(seq)
261
353
  trigger = mock()
262
354
  trigger.expects(:fire).in_sequence(seq)
355
+
356
+ # Store away the render_body method to be called below
357
+ render_body = pdf.method(:render_body)
263
358
  pdf.expects(:render_body).in_sequence(seq)
264
-
359
+
265
360
  pdf.before_render{ trigger.fire }
266
-
361
+
362
+ # Render the body to set up object offsets
363
+ render_body.call(StringIO.new)
267
364
  pdf.render
268
365
  end
366
+
367
+ end
368
+
369
+ describe "The :optimize_objects option" do
370
+ before(:all) do
371
+ @wasteful_doc = lambda do
372
+ transaction { start_new_page; text "Hidden text"; rollback }
373
+ text "Hello world"
374
+ end
375
+ end
376
+
377
+ it "should result in fewer objects when enabled" do
378
+ wasteful_pdf = Prawn::Document.new(&@wasteful_doc)
379
+ frugal_pdf = Prawn::Document.new(:optimize_objects => true,
380
+ &@wasteful_doc)
381
+ frugal_pdf.render.size.should.be < wasteful_pdf.render.size
382
+ end
383
+
384
+ it "should default to :false" do
385
+ default_pdf = Prawn::Document.new(&@wasteful_doc)
386
+ wasteful_pdf = Prawn::Document.new(:optimize_objects => false,
387
+ &@wasteful_doc)
388
+ default_pdf.render.size.should == wasteful_pdf.render.size
389
+ end
269
390
  end
270
391
 
271
392
  describe "PDF file versions" do
@@ -40,3 +40,36 @@ describe "Prawn::ObjectStore" do
40
40
  @store.map{|ref| ref.identifier}[-3..-1].should == [10, 11, 12]
41
41
  end
42
42
  end
43
+
44
+ describe "Prawn::ObjectStore#compact" do
45
+ it "should do nothing to an ObjectStore with all live refs" do
46
+ store = Prawn::ObjectStore.new
47
+ store.info.data[:Blah] = store.ref(:some => "structure")
48
+ old_size = store.size
49
+ store.compact
50
+
51
+ store.size.should == old_size
52
+ end
53
+
54
+ it "should remove dead objects, renumbering live objects from 1" do
55
+ store = Prawn::ObjectStore.new
56
+ store.ref(:some => "structure")
57
+ old_size = store.size
58
+ store.compact
59
+
60
+ store.size.should.be < old_size
61
+ store.map{ |o| o.identifier }.should == (1..store.size).to_a
62
+ end
63
+
64
+ it "should detect and remove dead objects that were once live" do
65
+ store = Prawn::ObjectStore.new
66
+ store.info.data[:Blah] = store.ref(:some => "structure")
67
+ store.info.data[:Blah] = :overwritten
68
+ old_size = store.size
69
+ store.compact
70
+
71
+ store.size.should.be < old_size
72
+ store.map{ |o| o.identifier }.should == (1..store.size).to_a
73
+ end
74
+ end
75
+
@@ -0,0 +1,79 @@
1
+ require File.join(File.expand_path(File.dirname(__FILE__)), "spec_helper")
2
+
3
+ describe "Repeaters" do
4
+
5
+ it "creates a stamp and increments Prawn::Repeater.count on initialize" do
6
+ orig_count = Prawn::Repeater.count
7
+
8
+ doc = sample_document
9
+ doc.expects(:create_stamp).with("prawn_repeater(#{orig_count})")
10
+
11
+ r = repeater(doc, :all) { :do_nothing }
12
+
13
+ assert_equal orig_count + 1, Prawn::Repeater.count
14
+ end
15
+
16
+ it "must provide an :all filter" do
17
+ doc = sample_document
18
+ r = repeater(doc, :all) { :do_nothing }
19
+
20
+ assert (1..doc.page_count).all? { |i| r.match?(i) }
21
+ end
22
+
23
+ it "must provide an :odd filter" do
24
+ doc = sample_document
25
+ r = repeater(doc, :odd) { :do_nothing }
26
+
27
+ odd, even = (1..doc.page_count).partition { |e| e % 2 == 1 }
28
+
29
+ assert odd.all? { |i| r.match?(i) }
30
+ assert ! even.any? { |i| r.match?(i) }
31
+ end
32
+
33
+ it "must be able to filter by an array of page numbers" do
34
+ doc = sample_document
35
+ r = repeater(doc, [1,2,7]) { :do_nothing }
36
+
37
+ assert_equal [1,2,7], (1..10).select { |i| r.match?(i) }
38
+ end
39
+
40
+ it "must be able to filter by a range of page numbers" do
41
+ doc = sample_document
42
+ r = repeater(doc, 2..4) { :do_nothing }
43
+
44
+ assert_equal [2,3,4], (1..10).select { |i| r.match?(i) }
45
+ end
46
+
47
+ it "must be able to filter by an arbitrary proc" do
48
+ doc = sample_document
49
+ r = repeater(doc, lambda { |x| x == 1 or x % 3 == 0 })
50
+
51
+ assert_equal [1,3,6,9], (1..10).select { |i| r.match?(i) }
52
+ end
53
+
54
+ it "must try to run a stamp if the page number matches" do
55
+ doc = sample_document
56
+ doc.expects(:stamp)
57
+
58
+ repeater(doc, :odd).run(3)
59
+ end
60
+
61
+ it "must not try to run a stamp if the page number matches" do
62
+ doc = sample_document
63
+
64
+ doc.expects(:stamp).never
65
+ repeater(doc, :odd).run(2)
66
+ end
67
+
68
+ def sample_document
69
+ doc = Prawn::Document.new(:skip_page_creation => true)
70
+ 10.times { |e| doc.start_new_page }
71
+
72
+ doc
73
+ end
74
+
75
+ def repeater(*args, &b)
76
+ Prawn::Repeater.new(*args,&b)
77
+ end
78
+
79
+ end
data/spec/stamp_spec.rb CHANGED
@@ -9,6 +9,14 @@ describe "create_stamp before any page is added" do
9
9
  end
10
10
  }.should.not.raise(Prawn::Errors::NotOnPage)
11
11
  end
12
+ it "should work with setting color" do
13
+ @pdf = Prawn::Document.new(:skip_page_creation => true)
14
+ lambda {
15
+ @pdf.create_stamp("my_stamp") do
16
+ @pdf.fill_color = 'ff0000'
17
+ end
18
+ }.should.not.raise(Prawn::Errors::NotOnPage)
19
+ end
12
20
  end
13
21
 
14
22
  describe "Document with a stamp" do