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