lib/prawn/document.rb in prawn-0.11.1.pre vs lib/prawn/document.rb in prawn-0.11.1
- old
+ new
@@ -7,48 +7,49 @@
# This is free software. Please see the LICENSE and COPYING files for details.
require "stringio"
require "prawn/document/page_geometry"
require "prawn/document/bounding_box"
+require "prawn/document/column_box"
require "prawn/document/internals"
require "prawn/document/span"
require "prawn/document/snapshot"
require "prawn/document/graphics_state"
module Prawn
-
+
# The Prawn::Document class is how you start creating a PDF document.
- #
- # There are three basic ways you can instantiate PDF Documents in Prawn, they
+ #
+ # There are three basic ways you can instantiate PDF Documents in Prawn, they
# are through assignment, implicit block or explicit block. Below is an exmple
# of each type, each example does exactly the same thing, makes a PDF document
# with all the defaults and puts in the default font "Hello There" and then
# saves it to the current directory as "example.pdf"
- #
+ #
# For example, assignment can be like this:
- #
+ #
# pdf = Prawn::Document.new
# pdf.text "Hello There"
# pdf.render_file "example.pdf"
- #
+ #
# Or you can do an implied block form:
- #
+ #
# Prawn::Document.generate "example.pdf" do
# text "Hello There"
# end
- #
+ #
# Or if you need to access a variable outside the scope of the block, the
# explicit block form:
- #
+ #
# words = "Hello There"
# Prawn::Document.generate "example.pdf" do |pdf|
# pdf.text words
# end
#
# Usually, the block forms are used when you are simply creating a PDF document
# that you want to immediately save or render out.
- #
+ #
# See the new and generate methods for further details on the above.
#
class Document
include Prawn::Document::Internals
include Prawn::Core::Annotations
@@ -66,15 +67,15 @@
# any subclasses.
#
# Example:
#
# module MyFancyModule
- #
+ #
# def party!
# text "It's a big party!"
# end
- #
+ #
# end
#
# Prawn::Document.extensions << MyFancyModule
#
# Prawn::Document.generate("foo.pdf") do
@@ -150,11 +151,11 @@
# # Top is 10, right is 20, bottom is 30, left is 40.
# :margin => [10, 20, 30, 40]
#
# Additionally, :page_size can be specified as a simple two value array giving
# the width and height of the document you need in PDF Points.
- #
+ #
# Usage:
#
# # New document, US Letter paper, portrait orientation
# pdf = Prawn::Document.new
#
@@ -165,13 +166,13 @@
# pdf = Prawn::Document.new(:page_size => [200, 300])
#
# # New document, with background
# pdf = Prawn::Document.new(:background => "#{Prawn::BASEDIR}/data/images/pigs.jpg")
#
- def initialize(options={},&block)
- Prawn.verify_options [:page_size, :page_layout, :margin, :left_margin,
- :right_margin, :top_margin, :bottom_margin, :skip_page_creation,
+ def initialize(options={},&block)
+ Prawn.verify_options [:page_size, :page_layout, :margin, :left_margin,
+ :right_margin, :top_margin, :bottom_margin, :skip_page_creation,
:compress, :skip_encoding, :background, :info,
:optimize_objects, :template], options
# need to fix, as the refactoring breaks this
# raise NotImplementedError if options[:skip_page_creation]
@@ -202,11 +203,11 @@
start_new_page(options)
end
end
@bounding_box = @margin_box
-
+
if block
block.arity < 1 ? instance_eval(&block) : block[self]
end
end
@@ -231,54 +232,66 @@
# pdf.start_new_page #=> Starts new page keeping current values
# pdf.start_new_page(:size => "LEGAL", :layout => :landscape)
# pdf.start_new_page(:left_margin => 50, :right_margin => 50)
# pdf.start_new_page(:margin => 100)
#
+ # A template for a page can be specified by pointing to the path of and existing pdf.
+ # One can also specify which page of the template which defaults otherwise to 1.
+ #
+ # pdf.start_new_page(:template => multipage_template.pdf, :template_page => 2)
+ #
def start_new_page(options = {})
if last_page = state.page
last_page_size = last_page.size
last_page_layout = last_page.layout
last_page_margins = last_page.margins
end
- state.page = Prawn::Core::Page.new(self,
- :size => options[:size] || last_page_size,
- :layout => options[:layout] || last_page_layout,
- :margins => last_page_margins )
+ page_options = {:size => options[:size] || last_page_size,
+ :layout => options[:layout] || last_page_layout,
+ :margins => last_page_margins}
+ if last_page
+ new_graphic_state = last_page.graphic_state.dup
+ #erase the color space so that it gets reset on new page for fussy pdf-readers
+ new_graphic_state.color_space = {}
+ page_options.merge!(:graphic_state => new_graphic_state)
+ end
+ merge_template_options(page_options, options) if options[:template]
+ state.page = Prawn::Core::Page.new(self, page_options)
+
apply_margin_options(options)
+ state.page.new_content_stream if options[:template]
+ use_graphic_settings(options[:template])
- use_graphic_settings
-
unless options[:orphan]
state.insert_page(state.page, @page_number)
@page_number += 1
- save_graphics_state
-
- canvas { image(@background, :at => bounds.top_left) } if @background
+ canvas { image(@background, :at => bounds.top_left) } if @background
@y = @bounding_box.absolute_top
float do
state.on_page_create_action(self)
end
end
- end
+ end
+
# Returns the number of pages in the document
#
# pdf = Prawn::Document.new
# pdf.page_count #=> 1
# 3.times { pdf.start_new_page }
# pdf.page_count #=> 4
#
def page_count
state.page_count
end
-
+
# Re-opens the page with the given (1-based) page number so that you can
- # draw on it.
+ # draw on it.
#
# See Prawn::Document#number_pages for a sample usage of this capability.
#
def go_to_page(k)
@page_number = k
@@ -298,11 +311,11 @@
def cursor
y - bounds.absolute_bottom
end
# Moves to the specified y position in relative terms to the bottom margin.
- #
+ #
def move_cursor_to(new_y)
self.y = new_y + bounds.absolute_bottom
end
# Executes a block and then restores the original y position
@@ -312,13 +325,13 @@
# pdf.float do
# pdf.move_down 100
# pdf.text "C"
# end
#
- # pdf.text "B"
- #
- def float
+ # pdf.text "B"
+ #
+ def float
mask(:y) { yield }
end
# Renders the PDF document to string
#
@@ -351,51 +364,51 @@
# document margin box.
#
# Another important point about bounding boxes is that all x and y measurements
# within a bounding box code block are relative to the bottom left corner of the
# bounding box.
- #
+ #
# For example:
- #
+ #
# Prawn::Document.new do
# # In the default "margin box" of a Prawn document of 0.5in along each edge
- #
+ #
# # Draw a border around the page (the manual way)
# stroke do
# line(bounds.bottom_left, bounds.bottom_right)
# line(bounds.bottom_right, bounds.top_right)
# line(bounds.top_right, bounds.top_left)
# line(bounds.top_left, bounds.bottom_left)
# end
- #
+ #
# # Draw a border around the page (the easy way)
# stroke_bounds
# end
- #
+ #
def bounds
@bounding_box
end
# Sets Document#bounds to the BoundingBox provided. See above for a brief
- # description of what a bounding box is. This function is useful if you
+ # description of what a bounding box is. This function is useful if you
# really need to change the bounding box manually, but usually, just entering
# and exiting bounding box code blocks is good enough.
#
def bounds=(bounding_box)
@bounding_box = bounding_box
end
# Moves up the document by n points relative to the current position inside
# the current bounding box.
- #
+ #
def move_up(n)
self.y += n
end
# Moves down the document by n points relative to the current position inside
# the current bounding box.
- #
+ #
def move_down(n)
self.y -= n
end
# Moves down the document and then executes a block.
@@ -436,26 +449,29 @@
def pad(y)
move_down(y)
yield
move_down(y)
end
-
-
+
+
# Indents the specified number of PDF points for the duration of the block
#
# pdf.text "some text"
# pdf.indent(20) do
# pdf.text "This is indented 20 points"
# end
# pdf.text "This starts 20 points left of the above line " +
# "and is flush with the first line"
+ # pdf.indent 20, 20 do
+ # pdf.text "This line is indented on both sides."
+ # end
#
- def indent(x, &block)
- bounds.indent(x, &block)
+ def indent(left, right = 0, &block)
+ bounds.indent(left, right, &block)
end
-
+
def mask(*fields) # :nodoc:
# Stores the current state of the named attributes, executes the block, and
# then restores the original values after the block has executed.
# -- I will remove the nodoc if/when this feature is a little less hacky
stored = {}
@@ -465,11 +481,12 @@
end
# Attempts to group the given block vertically within the current context.
# First attempts to render it in the current position on the current page.
# If that attempt overflows, it is tried anew after starting a new context
- # (page or column).
+ # (page or column). Returns a logically true value if the content fits in
+ # one page/column, false if a new page or column was needed.
#
# Raises CannotGroup if the provided content is too large to fit alone in
# the current page or column.
#
def group(second_attempt=false)
@@ -486,71 +503,158 @@
unless success
raise Prawn::Errors::CannotGroup if second_attempt
old_bounding_box.move_past_bottom
group(second_attempt=true) { yield }
- end
+ end
+
+ success
end
- # Specify a template for page numbering. This should be called
+ # Places a text box on specified pages for page numbering. This should be called
# towards the end of document creation, after all your content is already in
# place. In your template string, <page> refers to the current page, and
- # <total> refers to the total amount of pages in the doucment.
+ # <total> refers to the total amount of pages in the document. Page numbering should
+ # occur at the end of your Prawn::Document.generate block because the method iterates
+ # through existing pages after they are created.
#
- # Example:
+ # Parameters are:
+ #
+ # <tt>string</tt>:: Template string for page number wording.
+ # Should include '<page>' and, optionally, '<total>'.
+ # <tt>options</tt>:: A hash for page numbering and text box options.
+ # <tt>:page_filter</tt>:: A filter to specify which pages to place page numbers on.
+ # Refer to the method 'page_match?'
+ # <tt>:start_count_at</tt>:: The starting count to increment pages from.
+ # <tt>:total_pages</tt>:: If provided, will replace <total> with the value given.
+ # Useful to override the total number of pages when using
+ # the start_count_at option.
+ # <tt>:color</tt>:: Text fill color.
#
+ # Please refer to Prawn::Text::text_box for additional options concerning text
+ # formatting and placement.
+ #
+ # Example: Print page numbers on every page except for the first. Start counting from
+ # five.
+ #
# Prawn::Document.generate("page_with_numbering.pdf") do
- # text "Hai"
- # start_new_page
- # text "bai"
- # start_new_page
- # text "-- Hai again"
- # number_pages "<page> in a total of <total>", [bounds.right - 50, 0]
+ # number_pages "<page> in a total of <total>",
+ # {:start_count_at => 5,
+ # :page_filter => lambda{ |pg| pg != 1 },
+ # :at => [bounds.right - 50, 0],
+ # :align => :right,
+ # :size => 14}
# end
#
- def number_pages(string, position)
- page_count.times do |i|
- go_to_page(i+1)
- str = string.gsub("<page>","#{i+1}").gsub("<total>","#{page_count}")
- draw_text str, :at => position
+ def number_pages(string, options={})
+ opts = options.dup
+ start_count_at = opts.delete(:start_count_at).to_i
+ page_filter = opts.delete(:page_filter)
+ total_pages = opts.delete(:total_pages)
+ txtcolor = opts.delete(:color)
+ # An explicit height so that we can draw page numbers in the margins
+ opts[:height] = 50
+
+ start_count = false
+ pseudopage = 0
+ (1..page_count).each do |p|
+ unless start_count
+ pseudopage = case start_count_at
+ when 0
+ 1
+ else
+ start_count_at.to_i
+ end
+ end
+ if page_match?(page_filter, p)
+ go_to_page(p)
+ # have to use fill_color here otherwise text reverts back to default fill color
+ fill_color txtcolor unless txtcolor.nil?
+ total_pages = total_pages.nil? ? page_count : total_pages
+ str = string.gsub("<page>","#{pseudopage}").gsub("<total>","#{total_pages}")
+ text_box str, opts
+ start_count = true # increment page count as soon as first match found
+ end
+ pseudopage += 1 if start_count
end
end
+ # Provides a way to execute a block of code repeatedly based on a
+ # page_filter.
+ #
+ # Available page filters are:
+ # :all repeats on every page
+ # :odd repeats on odd pages
+ # :even repeats on even pages
+ # some_array repeats on every page listed in the array
+ # some_range repeats on every page included in the range
+ # some_lambda yields page number and repeats for true return values
+ def page_match?(page_filter, page_number)
+ case page_filter
+ when :all
+ true
+ when :odd
+ page_number % 2 == 1
+ when :even
+ page_number % 2 == 0
+ when Range, Array
+ page_filter.include?(page_number)
+ when Proc
+ page_filter.call(page_number)
+ end
+ end
+
+
# Returns true if content streams will be compressed before rendering,
# false otherwise
#
def compression_enabled?
!!state.compress
end
-
+
private
- def use_graphic_settings
- update_colors
- line_width(line_width) unless line_width == 1
- cap_style(cap_style) unless cap_style == :butt
- join_style(join_style) unless join_style == :miter
- dash(dash[:dash], dash) if dashed?
+ def merge_template_options(page_options, options)
+ object_id = state.store.import_page(options[:template], options[:template_page] || 1)
+ page_options.merge!(:object_id => object_id )
end
+ # setting override_settings to true ensures that a new graphic state does not end up using
+ # previous settings especially from imported template streams
+ def use_graphic_settings(override_settings = false)
+ set_fill_color if current_fill_color != "000000" || override_settings
+ set_stroke_color if current_stroke_color != "000000" || override_settings
+ write_line_width if line_width != 1 || override_settings
+ write_stroke_cap_style if cap_style != :butt || override_settings
+ write_stroke_join_style if join_style != :miter || override_settings
+ write_stroke_dash if dashed? || override_settings
+ end
+
def generate_margin_box
old_margin_box = @margin_box
page = state.page
@margin_box = BoundingBox.new(
self,
+ nil, # margin box has no parent
[ page.margins[:left], page.dimensions[-1] - page.margins[:top] ] ,
:width => page.dimensions[-2] - (page.margins[:left] + page.margins[:right]),
:height => page.dimensions[-1] - (page.margins[:top] + page.margins[:bottom])
)
+ # This check maintains indentation settings across page breaks
+ if (old_margin_box)
+ @margin_box.add_left_padding(old_margin_box.total_left_padding)
+ @margin_box.add_right_padding(old_margin_box.total_right_padding)
+ end
+
# we must update bounding box if not flowing from the previous page
#
# FIXME: This may have a bug where the old margin is restored
# when the bounding box exits.
@bounding_box = @margin_box if old_margin_box == @bounding_box
end
-
+
def apply_margin_options(options)
if options[:margin]
# Treat :margin as CSS shorthand with 1-4 values.
margin = Array(options[:margin])
positions = { 4 => [0,1,2,3], 3 => [0,1,2,1],