# 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) 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 :width or :height are specified, the margin_box
# width and height
#
# 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 :width or :height are specified, the margin_box
# width and height
#
# 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, &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
@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