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