sandal / prawn

Fast, Nimble PDF Writer for Ruby

This URL has Read+Write access

prawn / lib / prawn / document / bounding_box.rb
100644 354 lines (312 sloc) 11.145 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# encoding: utf-8
 
module Prawn
  class Document
    
    # A bounding box serves two important purposes:
    # * Provide bounds for flowing text, starting at a given point
    # * Translate the origin (0,0) for graphics primitives, for the purposes
    # of simplifying coordinate math.
    #
    # When flowing text, the usage of a bounding box is simple. Text will
    # begin at the point specified, flowing the width of the bounding box.
    # After the block exits, the text drawing position will be moved to
    # the bottom of the bounding box (y - height). If flowing text exceeds
    # the height of the bounding box, the text will be continued on the next
    # page, starting again at the top-left corner of the bounding box.
    #
    # pdf.bounding_box([100,500], :width => 100, :height => 300) do
    # pdf.text "This text will flow in a very narrow box starting" +
    # "from [100,500]. The pointer will then be moved to [100,200]" +
    # "and return to the margin_box"
    # end
    #
    # When translating coordinates, the idea is to allow the user to draw
    # relative to the origin, and then translate their drawing to a specified
    # area of the document, rather than adjust all their drawing coordinates
    # to match this new region.
    #
    # Take for example two triangles which share one point, drawn from the
    # origin:
    #
    # pdf.polygon [0,250], [0,0], [150,100]
    # pdf.polygon [100,0], [150,100], [200,0]
    #
    # It would be easy enough to translate these triangles to another point,
    # e.g [200,200]
    #
    # pdf.polygon [200,450], [200,200], [350,300]
    # pdf.polygon [300,200], [350,300], [400,200]
    #
    # However, each time you want to move the drawing, you'd need to alter
    # every point in the drawing calls, which as you might imagine, can become
    # tedious.
    #
    # If instead, we think of the drawing as being bounded by a box, we can
    # see that the image is 200 points wide by 250 points tall.
    #
    # To translate it to a new origin, we simply select a point at (x,y+height)
    #
    # Using the [200,200] example:
    #
    # pdf.bounding_box([200,450], :width => 200, :height => 250) do
    # pdf.polygon [0,250], [0,0], [150,100]
    # pdf.polygon [100,0], [150,100], [200,0]
    # end
    #
    # Notice that the drawing is still relative to the origin. If we want to
    # move this drawing around the document, we simply need to recalculate the
    # top-left corner of the rectangular bounding-box, and all of our graphics
    # calls remain unmodified.
    #
    # By default, bounding boxes are specified relative to the document's
    # margin_box (which is itself a bounding box). You can also nest bounding
    # boxes, allowing you to build components which are relative to each other
    #
    # pdf.bouding_box([200,450], :width => 200, :height => 250) do
    # pdf.bounding_box([50,200], :width => 50, :height => 50) do
    # # a 50x50 bounding box that starts 50 pixels left and 50 pixels down
    # # the parent bounding box.
    # end
    # end
    #
    # If you wish to position the bounding boxes at absolute coordinates rather
    # than relative to the margins or other bounding boxes, you can use canvas()
    #
    # pdf.canvas do
    # pdf.bounding_box([200,450], :width => 200, :height => 250) do
    # # positioned at 'real' (200,450)
    # end
    # end
    #
    # Of course, if you use canvas, you will be responsible for ensuring that
    # you remain within the printable area of your document.
    #
    def bounding_box(*args, &block)
      init_bounding_box(block) do |_|
        translate!(args[0])
        @bounding_box = BoundingBox.new(self, *args)
      end
    end
    
        
    # A LazyBoundingBox is simply a BoundingBox with an action tied to it to be
    # executed later. The lazy_bounding_box method takes the same arguments as
    # bounding_box, but returns a LazyBoundingBox object instead of executing
    # the code block directly.
    #
    # You can then call LazyBoundingBox#draw at any time (or multiple times if
    # you wish), and the contents of the block will then be run. This can be
    # useful for assembling repeating page elements or reusable components.
    #
    # file = "lazy_bounding_boxes.pdf"
    # Prawn::Document.generate(file, :skip_page_creation => true) do
    # point = [bounds.right-50, bounds.bottom + 25]
    # page_counter = lazy_bounding_box(point, :width => 50) do
    # text "Page: #{page_count}"
    # end
    #
    # 10.times do
    # start_new_page
    # text "Some text"
    # page_counter.draw
    # end
    # end
    #
    def lazy_bounding_box(*args,&block)
      translate!(args[0])
      box = LazyBoundingBox.new(self,*args)
      box.action(&block)
      return box
    end
    
    # A shortcut to produce a bounding box which is mapped to the document's
    # absolute coordinates, regardless of how things are nested or margin sizes.
    #
    # pdf.canvas do
    # pdf.line pdf.bounds.bottom_left, pdf.bounds.top_right
    # end
    #
    def canvas(&block)
      init_bounding_box(block, :hold_position => true) do |_|
        @bounding_box = BoundingBox.new(self, [0,page_dimensions[3]],
          :width => page_dimensions[2],
          :height => page_dimensions[3]
        )
      end
    end
       
    # A header is a LazyBoundingBox drawn relative to the margins that can be
    # repeated on every page of the document.
    #
    # Unless <tt>:width</tt> or <tt>:height</tt> are specified, the margin_box
    # width and height are used.
    #
    # header margin_box.top_left do
    # text "Here's My Fancy Header", :size => 25, :align => :center
    # stroke_horizontal_rule
    # end
    #
    def header(top_left,options={},&block)
      @header = repeating_page_element(top_left,options,&block)
    end
        
    # A footer is a LazyBoundingBox drawn relative to the margins that can be
    # repeated on every page of the document.
    #
    # Unless <tt>:width</tt> or <tt>:height</tt> are specified, the margin_box
    # width and height are used.
    #
    # footer [margin_box.left, margin_box.bottom + 25] do
    # stroke_horizontal_rule
    # text "And here's a sexy footer", :size => 16
    # end
    #
    def footer(top_left,options={},&block)
      @footer = repeating_page_element(top_left,options,&block)
    end
    
    private
    
    def init_bounding_box(user_block, options={}, &init_block)
      parent_box = @bounding_box
 
      init_block.call(parent_box)
 
      self.y = @bounding_box.absolute_top
      user_block.call
      self.y = @bounding_box.absolute_bottom unless options[:hold_position]
 
      @bounding_box = parent_box
    end
    
    def repeating_page_element(top_left,options={},&block)
      r = LazyBoundingBox.new(self, translate(top_left),
        :width => options[:width] || margin_box.width,
        :height => options[:height] || margin_box.height )
      r.action(&block)
      return r
    end
 
    class BoundingBox
      
      def initialize(parent, point, options={}) #:nodoc:
        @parent = parent
        @x, @y = point
        @width, @height = options[:width], options[:height]
      end
      
      # The translated origin (x,y-height) which describes the location
      # of the bottom left corner of the bounding box
      #
      def anchor
        [@x, @y - height]
      end
      
      # Relative left x-coordinate of the bounding box. (Always 0)
      #
      def left
        0
      end
      
      # Relative right x-coordinate of the bounding box. (Equal to the box width)
      #
      def right
        @width
      end
      
      # Relative top y-coordinate of the bounding box. (Equal to the box height)
      #
      def top
        height
      end
      
      # Relative bottom y-coordinate of the bounding box (Always 0)
      #
      def bottom
        0
      end
 
      # Relative top-left point of the bounding_box
      #
      def top_left
        [left,top]
      end
 
      # Relative top-right point of the bounding box
      #
      def top_right
        [right,top]
      end
 
      # Relative bottom-right point of the bounding box
      #
      def bottom_right
        [right,bottom]
      end
 
      # Relative bottom-left point of the bounding box
      #
      def bottom_left
        [left,bottom]
      end
      
      # Absolute left x-coordinate of the bounding box
      #
      def absolute_left
        @x
      end
      
      # Absolute right x-coordinate of the bounding box
      #
      def absolute_right
        @x + width
      end
      
      # Absolute top y-coordinate of the bounding box
      #
      def absolute_top
        @y
      end
      
      # Absolute bottom y-coordinate of the bottom box
      #
      def absolute_bottom
        @y - height
      end
 
      # Absolute top-left point of the bounding box
      #
      def absolute_top_left
        [absolute_left, absolute_top]
      end
 
      # Absolute top-right point of the bounding box
      #
      def absolute_top_right
        [absolute_right, absolute_top]
      end
 
      # Absolute bottom-left point of the bounding box
      #
      def absolute_bottom_left
        [absolute_left, absolute_bottom]
      end
 
      # Absolute bottom-left point of the bounding box
      #
      def absolute_bottom_right
        [absolute_right, absolute_bottom]
      end
      
      # Width of the bounding box
      #
      def width
        @width
      end
      
      # Height of the bounding box. If the box is 'stretchy' (unspecified
      # height attribute), height is calculated as the distance from the top of
      # the box to the current drawing position.
      #
      def height
        @height || absolute_top - @parent.y
      end
       
      # Returns +false+ when the box has a defined height, +true+ when the height
      # is being calculated on the fly based on the current vertical position.
      #
      def stretchy?
        !@height
      end
      
    end
       
    class LazyBoundingBox < BoundingBox
       
       # Defines the block to be executed by LazyBoundingBox#draw.
       # Usually, this will be used via a higher level interface.
       # See the documentation for Document#lazy_bounding_box, Document#header,
       # and Document#footer
       #
       def action(&block)
         @action = block
       end
       
       # Sets Document#bounds to use the LazyBoundingBox for its bounds,
       # runs the block specified by LazyBoundingBox#action,
       # and then restores the original bounds of the document.
       #
       def draw
         @parent.mask(:y) do
           parent_box = @parent.bounds
           @parent.bounds = self
           @parent.y = absolute_top
           @action.call
           @parent.bounds = parent_box
         end
       end
 
    end
    
  end
end