prawn-core 0.5.1 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. data/HACKING +46 -0
  2. data/README +9 -3
  3. data/Rakefile +7 -6
  4. data/examples/bounding_box/stretched_nesting.rb +67 -0
  5. data/examples/general/margin.rb +36 -0
  6. data/examples/general/multi_page_layout.rb +3 -1
  7. data/examples/general/page_numbering.rb +15 -0
  8. data/examples/general/stamp.rb +45 -0
  9. data/examples/graphics/stroke_cap_and_join.rb +45 -0
  10. data/examples/graphics/stroke_dash.rb +42 -0
  11. data/examples/graphics/transparency.rb +26 -0
  12. data/examples/text/text_box_returning_excess.rb +51 -0
  13. data/lib/prawn/byte_string.rb +7 -0
  14. data/lib/prawn/core.rb +7 -8
  15. data/lib/prawn/document/annotations.rb +3 -2
  16. data/lib/prawn/document/bounding_box.rb +15 -10
  17. data/lib/prawn/document/column_box.rb +1 -3
  18. data/lib/prawn/document/destinations.rb +11 -10
  19. data/lib/prawn/document/internals.rb +62 -19
  20. data/lib/prawn/document/snapshot.rb +71 -0
  21. data/lib/prawn/document/text/box.rb +7 -0
  22. data/lib/prawn/document/text/wrapping.rb +3 -0
  23. data/lib/prawn/document/text.rb +9 -2
  24. data/lib/prawn/document.rb +141 -25
  25. data/lib/prawn/errors.rb +12 -0
  26. data/lib/prawn/font/afm.rb +1 -1
  27. data/lib/prawn/font/ttf.rb +5 -5
  28. data/lib/prawn/font.rb +8 -5
  29. data/lib/prawn/graphics/cap_style.rb +35 -0
  30. data/lib/prawn/graphics/dash.rb +69 -0
  31. data/lib/prawn/graphics/join_style.rb +35 -0
  32. data/lib/prawn/graphics/transparency.rb +56 -0
  33. data/lib/prawn/graphics.rb +9 -1
  34. data/lib/prawn/images.rb +4 -4
  35. data/lib/prawn/name_tree.rb +2 -1
  36. data/lib/prawn/object_store.rb +63 -0
  37. data/lib/prawn/pdf_object.rb +4 -0
  38. data/lib/prawn/reference.rb +18 -5
  39. data/lib/prawn/stamp.rb +87 -0
  40. data/spec/bounding_box_spec.rb +9 -0
  41. data/spec/document_spec.rb +58 -5
  42. data/spec/images_spec.rb +1 -1
  43. data/spec/name_tree_spec.rb +14 -5
  44. data/spec/object_store_spec.rb +42 -0
  45. data/spec/pdf_object_spec.rb +5 -0
  46. data/spec/reference_spec.rb +40 -0
  47. data/spec/snapshot_spec.rb +115 -0
  48. data/spec/spec_helper.rb +1 -4
  49. data/spec/stamp_spec.rb +98 -0
  50. data/spec/stroke_styles_spec.rb +152 -0
  51. data/spec/text_box_spec.rb +26 -0
  52. data/spec/text_spec.rb +8 -1
  53. data/spec/transparency_spec.rb +61 -0
  54. data/vendor/pdf-inspector/lib/pdf/inspector/extgstate.rb +18 -0
  55. data/vendor/pdf-inspector/lib/pdf/inspector/graphics.rb +40 -1
  56. data/vendor/pdf-inspector/lib/pdf/inspector/page.rb +12 -3
  57. data/vendor/pdf-inspector/lib/pdf/inspector.rb +2 -1
  58. metadata +26 -2
@@ -11,6 +11,7 @@ require 'prawn/name_tree'
11
11
  module Prawn
12
12
  class Document
13
13
  module Destinations
14
+
14
15
  # The maximum number of children to fit into a single node in the Dests tree.
15
16
  NAME_TREE_CHILDREN_LIMIT = 20 #:nodoc:
16
17
 
@@ -19,7 +20,7 @@ module Prawn
19
20
  # (For more on name trees, see section 3.8.4 in the PDF spec.)
20
21
  #
21
22
  def dests
22
- names.data[:Dests] ||= ref(Prawn::NameTree::Node.new(self, NAME_TREE_CHILDREN_LIMIT))
23
+ names.data[:Dests] ||= ref!(Prawn::NameTree::Node.new(self, NAME_TREE_CHILDREN_LIMIT))
23
24
  end
24
25
 
25
26
  # Adds a new destination to the dests name tree (see #dests). The
@@ -27,63 +28,63 @@ module Prawn
27
28
  # it is not already one.
28
29
  #
29
30
  def add_dest(name, reference)
30
- reference = ref(reference) unless reference.is_a?(Prawn::Reference)
31
+ reference = ref!(reference) unless reference.is_a?(Prawn::Reference)
31
32
  dests.data.add(name, reference)
32
33
  end
33
34
 
34
35
  # Return a Dest specification for a specific location (and optional zoom
35
36
  # level).
36
37
  #
37
- def dest_xyz(left, top, zoom=nil, page=@current_page)
38
+ def dest_xyz(left, top, zoom=nil, page=current_page)
38
39
  [page, :XYZ, left, top, zoom]
39
40
  end
40
41
 
41
42
  # Return a Dest specification that will fit the given page into the
42
43
  # viewport.
43
44
  #
44
- def dest_fit(page=@current_page)
45
+ def dest_fit(page=current_page)
45
46
  [page, :Fit]
46
47
  end
47
48
 
48
49
  # Return a Dest specification that will fit the given page horizontally
49
50
  # into the viewport, aligned vertically at the given top coordinate.
50
51
  #
51
- def dest_fit_horizontally(top, page=@current_page)
52
+ def dest_fit_horizontally(top, page=current_page)
52
53
  [page, :FitH, top]
53
54
  end
54
55
 
55
56
  # Return a Dest specification that will fit the given page vertically
56
57
  # into the viewport, aligned horizontally at the given left coordinate.
57
58
  #
58
- def dest_fit_vertically(left, page=@current_page)
59
+ def dest_fit_vertically(left, page=current_page)
59
60
  [page, :FitV, left]
60
61
  end
61
62
 
62
63
  # Return a Dest specification that will fit the given rectangle into the
63
64
  # viewport, for the given page.
64
65
  #
65
- def dest_fit_rect(left, bottom, right, top, page=@current_page)
66
+ def dest_fit_rect(left, bottom, right, top, page=current_page)
66
67
  [page, :FitR, left, bottom, right, top]
67
68
  end
68
69
 
69
70
  # Return a Dest specfication that will fit the given page's bounding box
70
71
  # into the viewport.
71
72
  #
72
- def dest_fit_bounds(page=@current_page)
73
+ def dest_fit_bounds(page=current_page)
73
74
  [page, :FitB]
74
75
  end
75
76
 
76
77
  # Same as #dest_fit_horizontally, but works on the page's bounding box
77
78
  # instead of the entire page.
78
79
  #
79
- def dest_fit_bounds_horizontally(top, page=@current_page)
80
+ def dest_fit_bounds_horizontally(top, page=current_page)
80
81
  [page, :FitBH, top]
81
82
  end
82
83
 
83
84
  # Same as #dest_fit_vertically, but works on the page's bounding box
84
85
  # instead of the entire page.
85
86
  #
86
- def dest_fit_bounds_vertically(left, page=@current_page)
87
+ def dest_fit_bounds_vertically(left, page=current_page)
87
88
  [page, :FitBV, left]
88
89
  end
89
90
  end
@@ -17,7 +17,9 @@ module Prawn
17
17
  module Internals
18
18
 
19
19
  # Creates a new Prawn::Reference and adds it to the Document's object
20
- # list. The +data+ argument is anything that Prawn::PdfObject() can convert.
20
+ # list. The +data+ argument is anything that Prawn::PdfObject() can convert.
21
+ #
22
+ # Returns the identifier which points to the reference in the ObjectStore
21
23
  #
22
24
  # If a block is given, it will be invoked just before the object is written
23
25
  # out to the PDF document stream. This allows you to do deferred processing
@@ -25,8 +27,40 @@ module Prawn
25
27
  # about until the last page of the document is finished).
26
28
  #
27
29
  def ref(data, &block)
28
- @objects.push(Prawn::Reference.new(@objects.size + 1, data, &block)).last
30
+ ref!(data, &block).identifier
29
31
  end
32
+
33
+ # Like ref, but returns the actual reference instead of its identifier.
34
+ #
35
+ # While you can use this to build up nested references within the object
36
+ # tree, it is recommended to persist only identifiers, and them provide
37
+ # helper methods to look up the actual references in the ObjectStore
38
+ # if needed. If you take this approach, Prawn::Document::Snapshot
39
+ # will probably work with your extension
40
+ #
41
+ def ref!(data, &block)
42
+ @store.ref(data, &block)
43
+ end
44
+
45
+ def page_content
46
+ @store[@page_content]
47
+ end
48
+
49
+ def current_page
50
+ @store[@current_page]
51
+ end
52
+
53
+ # Grabs the reference for the current page content
54
+ #
55
+ def page_content
56
+ @active_stamp_stream || @store[@page_content]
57
+ end
58
+
59
+ # Grabs the reference for the current page
60
+ #
61
+ def current_page
62
+ @active_stamp_dictionary || @store[@current_page]
63
+ end
30
64
 
31
65
  # Appends a raw string to the current page content.
32
66
  #
@@ -37,20 +71,20 @@ module Prawn
37
71
  # pdf.add_content("S") # stroke
38
72
  #
39
73
  def add_content(str)
40
- @page_content << str << "\n"
74
+ page_content << str << "\n"
41
75
  end
42
76
 
43
77
  # Add a new type to the current pages ProcSet
44
78
  #
45
79
  def proc_set(*types)
46
- @current_page.data[:ProcSet] ||= ref([])
47
- @current_page.data[:ProcSet].data |= types
80
+ current_page.data[:ProcSet] ||= ref!([])
81
+ current_page.data[:ProcSet].data |= types
48
82
  end
49
83
 
50
84
  # The Resources dictionary for the current page
51
85
  #
52
86
  def page_resources
53
- @current_page.data[:Resources] ||= {}
87
+ current_page.data[:Resources] ||= {}
54
88
  end
55
89
 
56
90
  # The Font dictionary for the current page
@@ -63,24 +97,28 @@ module Prawn
63
97
  #
64
98
  def page_xobjects
65
99
  page_resources[:XObject] ||= {}
66
- end
100
+ end
101
+
102
+ def page_ext_gstates
103
+ page_resources[:ExtGState] ||= {}
104
+ end
67
105
 
68
106
  # The Name dictionary (PDF spec 3.6.3) for this document. It is
69
107
  # lazily initialized, so that documents that do not need a name
70
108
  # dictionary do not incur the additional overhead.
71
109
  #
72
110
  def names
73
- @root.data[:Names] ||= ref(:Type => :Names)
111
+ @store.root.data[:Names] ||= ref!(:Type => :Names)
74
112
  end
75
113
 
76
114
  private
77
115
 
78
116
  def finish_page_content
79
- @header.draw if @header
80
- @footer.draw if @footer
117
+ @header.draw if defined?(@header) and @header
118
+ @footer.draw if defined?(@footer) and @footer
81
119
  add_content "Q"
82
- @page_content.compress_stream if compression_enabled?
83
- @page_content.data[:Length] = @page_content.stream.size
120
+ page_content.compress_stream if compression_enabled?
121
+ page_content.data[:Length] = page_content.stream.size
84
122
  end
85
123
 
86
124
  # raise the PDF version of the file we're going to generate.
@@ -91,39 +129,44 @@ module Prawn
91
129
  end
92
130
 
93
131
  # Write out the PDF Header, as per spec 3.4.1
132
+ #
94
133
  def render_header(output)
95
134
  # pdf version
96
135
  output << "%PDF-#{@version}\n"
97
136
 
98
137
  # 4 binary chars, as recommended by the spec
99
- output << "\xFF\xFF\xFF\xFF\n"
138
+ output << "%\xFF\xFF\xFF\xFF\n"
100
139
  end
101
140
 
102
141
  # Write out the PDF Body, as per spec 3.4.2
142
+ #
103
143
  def render_body(output)
104
- @objects.each do |ref|
144
+ @store.each do |ref|
105
145
  ref.offset = output.size
106
146
  output << ref.object
107
147
  end
108
148
  end
109
149
 
110
150
  # Write out the PDF Cross Reference Table, as per spec 3.4.3
151
+ #
111
152
  def render_xref(output)
112
153
  @xref_offset = output.size
113
154
  output << "xref\n"
114
- output << "0 #{@objects.size + 1}\n"
155
+ output << "0 #{@store.size + 1}\n"
115
156
  output << "0000000000 65535 f \n"
116
- @objects.each do |ref|
157
+ @store.each do |ref|
117
158
  output.printf("%010d", ref.offset)
118
159
  output << " 00000 n \n"
119
160
  end
120
161
  end
121
162
 
122
163
  # Write out the PDF Trailer, as per spec 3.4.4
164
+ #
123
165
  def render_trailer(output)
124
- trailer_hash = {:Size => @objects.size + 1,
125
- :Root => @root,
126
- :Info => @info}
166
+ trailer_hash = {:Size => @store.size + 1,
167
+ :Root => @store.root,
168
+ :Info => @store.info}
169
+ trailer_hash.merge!(@trailer) if @trailer
127
170
 
128
171
  output << "trailer\n"
129
172
  output << Prawn::PdfObject(trailer_hash) << "\n"
@@ -0,0 +1,71 @@
1
+ # encoding: utf-8
2
+
3
+ # snapshot.rb : Implements transactional rendering for Prawn
4
+ #
5
+ # Copyright August 2008, Brad Ediger. All Rights Reserved.
6
+ #
7
+ # This is free software. Please see the LICENSE and COPYING files for details.
8
+ require 'delegate'
9
+
10
+ module Prawn
11
+ class Document
12
+ module Snapshot
13
+
14
+ RollbackTransaction = Class.new(StandardError)
15
+
16
+ # Call this within a +transaction+ block to roll back the transaction and
17
+ # prevent any of its data from being rendered. You must reset the
18
+ # y-position yourself if you have performed any drawing operations that
19
+ # modify it.
20
+ def rollback
21
+ raise RollbackTransaction
22
+ end
23
+
24
+ # Run a block of drawing operations, to be completed atomically. If
25
+ # +rollback+ is called or a RollbackTransaction exception is raised
26
+ # inside the block, all actions taken inside the block will be rolled
27
+ # back (with the exception of y-position, which you must restore
28
+ # yourself).
29
+ #
30
+ # Returns true on success, or false if the transaction was rolled back.
31
+ def transaction
32
+ snap = take_snapshot
33
+ yield
34
+ true
35
+ rescue RollbackTransaction
36
+ restore_snapshot(snap)
37
+ false
38
+ end
39
+
40
+ private
41
+
42
+ # Takes a current snapshot of the document's state, sufficient to
43
+ # reconstruct it after it was amended.
44
+ def take_snapshot
45
+ {:page_content => Marshal.load(Marshal.dump(page_content)),
46
+ :current_page => Marshal.load(Marshal.dump(current_page)),
47
+ :page_kids => @store.pages.data[:Kids].map{|kid| kid.identifier},
48
+ :dests => Marshal.load(Marshal.dump(names.data[:Dests]))}
49
+ end
50
+
51
+ # Rolls the page state back to the state of the given snapshot.
52
+ def restore_snapshot(shot)
53
+ # Because these objects are referenced by identifier from the Pages
54
+ # dictionary, we can't just restore them over the current refs in
55
+ # page_content and current_page. We have to restore them over the old
56
+ # ones.
57
+ @page_content = shot[:page_content].identifier
58
+ page_content.replace shot[:page_content]
59
+
60
+ @current_page = shot[:current_page].identifier
61
+ current_page.replace shot[:current_page]
62
+
63
+ @store.pages.data[:Kids] = shot[:page_kids].map{|id| @store[id]}
64
+ @store.pages.data[:Count] = shot[:page_kids].size
65
+
66
+ names.data[:Dests] = shot[:dests]
67
+ end
68
+
69
+ end
70
+ end
71
+ end
@@ -23,6 +23,8 @@ module Prawn
23
23
  end
24
24
 
25
25
  module Text
26
+ # FIXME: requires documentation
27
+ #
26
28
  class Box #:nodoc:
27
29
  def initialize(text,options={})
28
30
  @document = options[:for]
@@ -54,6 +56,8 @@ module Prawn
54
56
  unless @overflow == :expand
55
57
  @document.y = y + @document.bounds.absolute_bottom - @height
56
58
  end
59
+
60
+ @excess_text
57
61
  end
58
62
 
59
63
  private
@@ -71,6 +75,9 @@ module Prawn
71
75
  when :ellipses
72
76
  @text[-3..-1] = "..." if @text.size > 3
73
77
  end
78
+ @excess_text = @document.naive_unwrap(lines[max_lines..-1].join)
79
+ else
80
+ @excess_text = ""
74
81
  end
75
82
  end
76
83
 
@@ -53,6 +53,9 @@ module Prawn
53
53
  output
54
54
  end
55
55
 
56
+ def naive_unwrap(string)
57
+ string.gsub(/(\S)\n/, '\1 ')
58
+ end
56
59
  end
57
60
  end
58
61
  end
@@ -43,8 +43,10 @@ module Prawn
43
43
  #
44
44
  # === Text Positioning Details:
45
45
  #
46
- # When using the :at parameter, Prawn will position your text by its
47
- # baseline, and flow along a single line.
46
+ # When using the :at parameter, Prawn will position your text by the
47
+ # left-most edge of its baseline, and flow along a single line. (This
48
+ # means that :align will not work)
49
+ #
48
50
  #
49
51
  # Otherwise, the text is positioned at font.ascender below the baseline,
50
52
  # making it easy to use this method within bounding boxes and spans.
@@ -79,6 +81,11 @@ module Prawn
79
81
  font.normalize_encoding!(text) unless @skip_encoding
80
82
 
81
83
  if options[:at]
84
+
85
+ if options[:align]
86
+ raise ArgumentError, "The :align option does not work with :at"
87
+ end
88
+
82
89
  x,y = translate(options[:at])
83
90
  font_size(options[:size]) { add_text_content(text,x,y,options) }
84
91
  else
@@ -15,6 +15,7 @@ require "prawn/document/span"
15
15
  require "prawn/document/text"
16
16
  require "prawn/document/annotations"
17
17
  require "prawn/document/destinations"
18
+ require "prawn/document/snapshot"
18
19
 
19
20
  module Prawn
20
21
 
@@ -58,13 +59,20 @@ module Prawn
58
59
  include Internals
59
60
  include Annotations
60
61
  include Destinations
62
+ include Snapshot
61
63
  include Prawn::Graphics
62
64
  include Prawn::Images
65
+ include Prawn::Stamp
63
66
 
64
- attr_accessor :y, :margin_box
65
- attr_reader :margins, :page_size, :page_layout
67
+ attr_accessor :margin_box
68
+ attr_reader :margins, :page_size, :page_layout, :y
66
69
  attr_writer :font_size
67
70
 
71
+
72
+ def self.extensions
73
+ @extensions ||= []
74
+ end
75
+
68
76
  # Creates and renders a PDF document.
69
77
  #
70
78
  # When using the implicit block form, Prawn will evaluate the block
@@ -102,6 +110,7 @@ module Prawn
102
110
  #
103
111
  # <tt>:page_size</tt>:: One of the Document::PageGeometry sizes [LETTER]
104
112
  # <tt>:page_layout</tt>:: Either <tt>:portrait</tt> or <tt>:landscape</tt>
113
+ # <tt>:margin</tt>:: Sets the margin on all sides in points [0.5 inch]
105
114
  # <tt>:left_margin</tt>:: Sets the left margin in points [0.5 inch]
106
115
  # <tt>:right_margin</tt>:: Sets the right margin in points [0.5 inch]
107
116
  # <tt>:top_margin</tt>:: Sets the top margin in points [0.5 inch]
@@ -111,7 +120,19 @@ module Prawn
111
120
  # <tt>:background</tt>:: An image path to be used as background on all pages [nil]
112
121
  # <tt>:info</tt>:: Generic hash allowing for custom metadata properties [nil]
113
122
  # <tt>:text_options</tt>:: A set of default options to be handed to text(). Be careful with this.
114
-
123
+ #
124
+ # Setting e.g. the :margin to 100 points and the :left_margin to 50 will result in margins
125
+ # of 100 points on every side except for the left, where it will be 50.
126
+ #
127
+ # The :margin can also be an array much like CSS shorthand:
128
+ #
129
+ # # Top and bottom are 20, left and right are 100.
130
+ # :margin => [20, 100]
131
+ # # Top is 50, left and right are 100, bottom is 20.
132
+ # :margin => [50, 100, 20]
133
+ # # Top is 10, right is 20, bottom is 30, left is 40.
134
+ # :margin => [10, 20, 30, 40]
135
+ #
115
136
  # Additionally, :page_size can be specified as a simple two value array giving
116
137
  # the width and height of the document you need in PDF Points.
117
138
  #
@@ -130,9 +151,11 @@ module Prawn
130
151
  # pdf = Prawn::Document.new(:background => "#{Prawn::BASEDIR}/data/images/pigs.jpg")
131
152
  #
132
153
  def initialize(options={},&block)
133
- Prawn.verify_options [:page_size, :page_layout, :left_margin,
154
+ Prawn.verify_options [:page_size, :page_layout, :margin, :left_margin,
134
155
  :right_margin, :top_margin, :bottom_margin, :skip_page_creation,
135
156
  :compress, :skip_encoding, :text_options, :background, :info], options
157
+
158
+ self.class.extensions.reverse_each { |e| extend e }
136
159
 
137
160
  options[:info] ||= {}
138
161
  options[:info][:Creator] ||= "Prawn"
@@ -145,23 +168,28 @@ module Prawn
145
168
  end
146
169
 
147
170
  @version = 1.3
148
- @objects = []
149
- @info = ref(options[:info])
150
- @pages = ref(:Type => :Pages, :Count => 0, :Kids => [])
151
- @root = ref(:Type => :Catalog, :Pages => @pages)
152
- @page_size = options[:page_size] || "LETTER"
153
- @page_layout = options[:page_layout] || :portrait
154
- @compress = options[:compress] || false
155
- @skip_encoding = options[:skip_encoding]
156
- @background = options[:background]
157
- @font_size = 12
171
+ @store = ObjectStore.new(options[:info])
172
+ @trailer = {}
173
+
174
+ @page_size = options[:page_size] || "LETTER"
175
+ @page_layout = options[:page_layout] || :portrait
176
+ @compress = options[:compress] || false
177
+ @skip_encoding = options[:skip_encoding]
178
+ @background = options[:background]
179
+ @font_size = 12
180
+ @page_content = nil
181
+ @bounding_box = nil
182
+ @margin_box = nil
158
183
 
159
184
  @text_options = options[:text_options] || {}
185
+
186
+ apply_margin_option(options) if options[:margin]
160
187
 
161
- @margins = { :left => options[:left_margin] || 36,
162
- :right => options[:right_margin] || 36,
163
- :top => options[:top_margin] || 36,
164
- :bottom => options[:bottom_margin] || 36 }
188
+ default_margin = 36 # 0.5 inch
189
+ @margins = { :left => options[:left_margin] || default_margin,
190
+ :right => options[:right_margin] || default_margin,
191
+ :top => options[:top_margin] || default_margin,
192
+ :bottom => options[:bottom_margin] || default_margin }
165
193
 
166
194
  generate_margin_box
167
195
 
@@ -182,22 +210,25 @@ module Prawn
182
210
  # pdf.start_new_page #=> Starts new page keeping current values
183
211
  # pdf.start_new_page(:size => "LEGAL", :layout => :landscape)
184
212
  # pdf.start_new_page(:left_margin => 50, :right_margin => 50)
213
+ # pdf.start_new_page(:margin => 100)
185
214
  #
186
215
  def start_new_page(options = {})
187
216
  @page_size = options[:size] if options[:size]
188
217
  @page_layout = options[:layout] if options[:layout]
218
+
219
+ apply_margin_option(options) if options[:margin]
189
220
 
190
221
  [:left,:right,:top,:bottom].each do |side|
191
- if options[:"#{side}_margin"]
192
- @margins[side] = options[:"#{side}_margin"]
222
+ if margin = options[:"#{side}_margin"]
223
+ @margins[side] = margin
193
224
  end
194
225
  end
195
226
 
196
227
  finish_page_content if @page_content
197
228
  build_new_page_content
198
229
 
199
- @pages.data[:Kids] << @current_page
200
- @pages.data[:Count] += 1
230
+ @store.pages.data[:Kids] << current_page
231
+ @store.pages.data[:Count] += 1
201
232
 
202
233
  add_content "q"
203
234
 
@@ -214,7 +245,12 @@ module Prawn
214
245
  # pdf.page_count #=> 4
215
246
  #
216
247
  def page_count
217
- @pages.data[:Count]
248
+ @store.pages.data[:Count]
249
+ end
250
+
251
+ def y=(new_y)
252
+ @y = new_y
253
+ bounds.update_height
218
254
  end
219
255
 
220
256
  # The current y drawing position relative to the innermost bounding box,
@@ -224,6 +260,13 @@ module Prawn
224
260
  y - bounds.absolute_bottom
225
261
  end
226
262
 
263
+
264
+ # Moves to the specified y position in relative terms to the bottom margin.
265
+ #
266
+ def move_cursor_to(new_y)
267
+ self.y = new_y + bounds.absolute_bottom
268
+ end
269
+
227
270
  # Renders the PDF document to string, useful for example in a Rails
228
271
  # application where you want to stream out the PDF to a web browser:
229
272
  #
@@ -376,6 +419,67 @@ module Prawn
376
419
  fields.each { |f| send("#{f}=", stored[f]) }
377
420
  end
378
421
 
422
+ # Raised if group() is called with a block that is too big to be
423
+ # rendered in the current context.
424
+ #
425
+ CannotGroup = Class.new(StandardError)
426
+
427
+ # Attempts to group the given block vertically within the current context.
428
+ # First attempts to render it in the current position on the current page.
429
+ # If that attempt overflows, it is tried anew after starting a new context
430
+ # (page or column).
431
+ #
432
+ # Raises CannotGroup if the provided content is too large to fit alone in
433
+ # the current page or column.
434
+ #
435
+ def group(second_attempt=false)
436
+ old_bounding_box = @bounding_box
437
+ @bounding_box = SimpleDelegator.new(@bounding_box)
438
+
439
+ def @bounding_box.move_past_bottom
440
+ raise RollbackTransaction
441
+ end
442
+
443
+ success = transaction { yield }
444
+
445
+ unless success
446
+ raise CannotGroup if second_attempt
447
+ old_bounding_box.move_past_bottom
448
+ group(second_attempt=true) { yield }
449
+ end
450
+
451
+ @bounding_box = old_bounding_box
452
+ end
453
+
454
+ # Specify a template for page numbering. This should be called
455
+ # towards the end of document creation, after all your content is already in
456
+ # place. In your template string, <page> refers to the current page, and
457
+ # <total> refers to the total amount of pages in the doucment.
458
+ #
459
+ # Example:
460
+ #
461
+ # Prawn::Document.generate("page_with_numbering.pdf") do
462
+ # text "Hai"
463
+ # start_new_page
464
+ # text "bai"
465
+ # start_new_page
466
+ # text "-- Hai again"
467
+ # number_pages "<page> in a total of <total>", [bounds.right - 50, 0]
468
+ # end
469
+ def number_pages(string, position)
470
+ page_count.times do |i|
471
+ go_to_page(i)
472
+ str = string.gsub("<page>","#{i+1}").gsub("<total>","#{page_count}")
473
+ text str, :at => position
474
+ end
475
+ end
476
+
477
+ def go_to_page(k) # :nodoc:
478
+ jump_to = @store.pages.data[:Kids][k]
479
+ @current_page = jump_to.identifier
480
+ @page_content = jump_to.data[:Contents].identifier
481
+ end
482
+
379
483
  # Returns true if content streams will be compressed before rendering,
380
484
  # false otherwise
381
485
  #
@@ -392,10 +496,11 @@ module Prawn
392
496
  @page_content = ref(:Length => 0)
393
497
 
394
498
  @current_page = ref(:Type => :Page,
395
- :Parent => @pages,
499
+ :Parent => @store.pages,
396
500
  :MediaBox => page_dimensions,
397
- :Contents => @page_content)
501
+ :Contents => page_content)
398
502
  update_colors
503
+ undash if dashed?
399
504
  end
400
505
 
401
506
  def generate_margin_box
@@ -413,6 +518,17 @@ module Prawn
413
518
  # when the bounding box exits.
414
519
  @bounding_box = @margin_box if old_margin_box == @bounding_box
415
520
  end
521
+
522
+ def apply_margin_option(options)
523
+ # Treat :margin as CSS shorthand with 1-4 values.
524
+ margin = Array(options[:margin])
525
+ positions = { 4 => [0,1,2,3], 3 => [0,1,2,1],
526
+ 2 => [0,1,0,1], 1 => [0,0,0,0] }[margin.length]
527
+
528
+ [:top, :right, :bottom, :left].zip(positions).each do |p,i|
529
+ options[:"#{p}_margin"] ||= margin[i]
530
+ end
531
+ end
416
532
 
417
533
  end
418
534
  end
data/lib/prawn/errors.rb CHANGED
@@ -45,5 +45,17 @@ module Prawn
45
45
  # type. This can either a completely unsupported format, or a dialect of a
46
46
  # supported format (ie. some types of PNG)
47
47
  UnsupportedImageType = Class.new(StandardError)
48
+
49
+ # This error is raised when a named element has alredy been
50
+ # created. For example, in the stamp module, stamps must have
51
+ # unique names within a document
52
+ NameTaken = Class.new(StandardError)
53
+
54
+ # This error is raised when a name is not a valid format
55
+ InvalidName = Class.new(StandardError)
56
+
57
+ # This error is raised when an object is attempted to be
58
+ # referenced by name, but no such name is associated with an object
59
+ UndefinedObjectName = Class.new(StandardError)
48
60
  end
49
61
  end
@@ -95,7 +95,7 @@ module Prawn
95
95
  private
96
96
 
97
97
  def register(subset)
98
- @document.ref(:Type => :Font,
98
+ @document.ref!(:Type => :Font,
99
99
  :Subtype => :Type1,
100
100
  :BaseFont => name.to_sym,
101
101
  :Encoding => :WinAnsiEncoding)