# encoding: utf-8 # document.rb : Implements PDF document generation for Prawn # # Copyright April 2008, Gregory Brown. All Rights Reserved. # # 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/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 # 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 include Prawn::Core::Destinations include Prawn::Document::Snapshot include Prawn::Document::GraphicsState include Prawn::Document::Security include Prawn::Text include Prawn::Graphics include Prawn::Images include Prawn::Stamp # Any module added to this array will be included into instances of # Prawn::Document at the per-object level. These will also be inherited by # 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 # party! # end # def self.extensions @extensions ||= [] end def self.inherited(base) #:nodoc: extensions.each { |e| base.extensions << e } end # Creates and renders a PDF document. # # When using the implicit block form, Prawn will evaluate the block # within an instance of Prawn::Document, simplifying your syntax. # However, please note that you will not be able to reference variables # from the enclosing scope within this block. # # # Using implicit block form and rendering to a file # Prawn::Document.generate "example.pdf" do # # self here is set to the newly instantiated Prawn::Document # # and so any variables in the outside scope are unavailable # font "Times-Roman" # draw_text "Hello World", :at => [200,720], :size => 32 # end # # If you need to access your local and instance variables, use the explicit # block form shown below. In this case, Prawn yields an instance of # PDF::Document and the block is an ordinary closure: # # # Using explicit block form and rendering to a file # content = "Hello World" # Prawn::Document.generate "example.pdf" do |pdf| # # self here is left alone # pdf.font "Times-Roman" # pdf.draw_text content, :at => [200,720], :size => 32 # end # def self.generate(filename,options={},&block) pdf = new(options,&block) pdf.render_file(filename) end # Creates a new PDF Document. The following options are available (with # the default values marked in []) # # :page_size:: One of the Document::PageGeometry sizes [LETTER] # :page_layout:: Either :portrait or :landscape # :margin:: Sets the margin on all sides in points [0.5 inch] # :left_margin:: Sets the left margin in points [0.5 inch] # :right_margin:: Sets the right margin in points [0.5 inch] # :top_margin:: Sets the top margin in points [0.5 inch] # :bottom_margin:: Sets the bottom margin in points [0.5 inch] # :skip_page_creation:: Creates a document without starting the first page [false] # :compress:: Compresses content streams before rendering them [false] # :optimize_objects:: Reduce number of PDF objects in output, at expense of render time [false] # :background:: An image path to be used as background on all pages [nil] # :info:: Generic hash allowing for custom metadata properties [nil] # :template:: The path to an existing PDF file to use as a template [nil] # # Setting e.g. the :margin to 100 points and the :left_margin to 50 will result in margins # of 100 points on every side except for the left, where it will be 50. # # The :margin can also be an array much like CSS shorthand: # # # Top and bottom are 20, left and right are 100. # :margin => [20, 100] # # Top is 50, left and right are 100, bottom is 20. # :margin => [50, 100, 20] # # 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 # # # New document, A4 paper, landscaped # pdf = Prawn::Document.new(:page_size => "A4", :page_layout => :landscape) # # # New document, Custom size # 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, :compress, :skip_encoding, :background, :info, :optimize_objects, :template], options # need to fix, as the refactoring breaks this # raise NotImplementedError if options[:skip_page_creation] self.class.extensions.reverse_each { |e| extend e } @internal_state = Prawn::Core::DocumentState.new(options) @internal_state.populate_pages_from_store(self) min_version(state.store.min_version) if state.store.min_version @background = options[:background] @font_size = 12 @bounding_box = nil @margin_box = nil @page_number = 0 options[:size] = options.delete(:page_size) options[:layout] = options.delete(:page_layout) if options[:template] fresh_content_streams(options) go_to_page(1) else if options[:skip_page_creation] || options[:template] start_new_page(options.merge(:orphan => true)) else start_new_page(options) end end @bounding_box = @margin_box if block block.arity < 1 ? instance_eval(&block) : block[self] end end attr_accessor :margin_box attr_reader :margins, :y attr_writer :font_size attr_accessor :page_number def state @internal_state end def page state.page end # Creates and advances to a new page in the document. # # Page size, margins, and layout can also be set when generating a # new page. These values will become the new defaults for page creation # # 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) # 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 ) apply_margin_options(options) 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 @y = @bounding_box.absolute_top float do state.on_page_create_action(self) 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. # # See Prawn::Document#number_pages for a sample usage of this capability. # def go_to_page(k) @page_number = k state.page = state.pages[k-1] generate_margin_box @y = @bounding_box.absolute_top end def y=(new_y) @y = new_y bounds.update_height end # The current y drawing position relative to the innermost bounding box, # or to the page margins at the top level. # 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 # # pdf.text "A" # # pdf.float do # pdf.move_down 100 # pdf.text "C" # end # # pdf.text "B" # def float mask(:y) { yield } end # Renders the PDF document to string # def render output = StringIO.new finalize_all_page_contents render_header(output) render_body(output) render_xref(output) render_trailer(output) str = output.string str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding) str end # Renders the PDF document to file. # # pdf.render_file "foo.pdf" # def render_file(filename) Kernel.const_defined?("Encoding") ? mode = "wb:ASCII-8BIT" : mode = "wb" File.open(filename,mode) { |f| f << render } end # The bounds method returns the current bounding box you are currently in, # which is by default the box represented by the margin box on the # document itself. When called from within a created bounding_box # block, the box defined by that call will be returned instead of the # 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 # 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. # # pdf.text "some text" # pdf.pad_top(100) do # pdf.text "This is 100 points below the previous line of text" # end # pdf.text "This text appears right below the previous line of text" # def pad_top(y) move_down(y) yield end # Executes a block then moves down the document # # pdf.text "some text" # pdf.pad_bottom(100) do # pdf.text "This text appears right below the previous line of text" # end # pdf.text "This is 100 points below the previous line of text" # def pad_bottom(y) yield move_down(y) end # Moves down the document by y, executes a block, then moves down the # document by y again. # # pdf.text "some text" # pdf.pad(100) do # pdf.text "This is 100 points below the previous line of text" # end # pdf.text "This is 100 points below the previous line of text" # 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" # def indent(x, &block) bounds.indent(x, &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 = {} fields.each { |f| stored[f] = send(f) } yield fields.each { |f| send("#{f}=", stored[f]) } 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). # # Raises CannotGroup if the provided content is too large to fit alone in # the current page or column. # def group(second_attempt=false) old_bounding_box = @bounding_box @bounding_box = SimpleDelegator.new(@bounding_box) def @bounding_box.move_past_bottom raise RollbackTransaction end success = transaction { yield } @bounding_box = old_bounding_box unless success raise Prawn::Errors::CannotGroup if second_attempt old_bounding_box.move_past_bottom group(second_attempt=true) { yield } end end # Specify a template 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, refers to the current page, and # refers to the total amount of pages in the doucment. # # Example: # # Prawn::Document.generate("page_with_numbering.pdf") do # text "Hai" # start_new_page # text "bai" # start_new_page # text "-- Hai again" # number_pages " in a total of ", [bounds.right - 50, 0] # end # def number_pages(string, position) page_count.times do |i| go_to_page(i+1) str = string.gsub("","#{i+1}").gsub("","#{page_count}") draw_text str, :at => position 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? end def generate_margin_box old_margin_box = @margin_box page = state.page @margin_box = BoundingBox.new( self, [ 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]) ) # 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], 2 => [0,1,0,1], 1 => [0,0,0,0] }[margin.length] [:top, :right, :bottom, :left].zip(positions).each do |p,i| options[:"#{p}_margin"] ||= margin[i] end end [:left,:right,:top,:bottom].each do |side| if margin = options[:"#{side}_margin"] state.page.margins[side] = margin end end generate_margin_box end end end