lib/prawn/document.rb in prawn-0.15.0 vs lib/prawn/document.rb in prawn-1.0.0.rc1

- old
+ new

@@ -5,18 +5,18 @@ # 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/column_box" +require "prawn/document/internals" +require "prawn/document/span" +require "prawn/document/snapshot" +require "prawn/document/graphics_state" -require_relative "document/bounding_box" -require_relative "document/column_box" -require_relative "document/internals" -require_relative "document/span" -require_relative "document/snapshot" -require_relative "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 @@ -50,31 +50,20 @@ # # See the new and generate methods for further details on the above. # class Document include Prawn::Document::Internals - include PDF::Core::Annotations - include PDF::Core::Destinations + 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 - include Prawn::SoftMask - # @group Extension API - - # NOTE: We probably need to rethink the options validation system, but this - # constant temporarily allows for extensions to modify the list. - - VALID_OPTIONS = [:page_size, :page_layout, :margin, :left_margin, - :right_margin, :top_margin, :bottom_margin, :skip_page_creation, - :compress, :skip_encoding, :background, :info, - :optimize_objects, :text_formatter, :print_scaling] - # 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: @@ -91,32 +80,18 @@ # # Prawn::Document.generate("foo.pdf") do # party! # end # - # def self.extensions @extensions ||= [] end - # @private - def self.inherited(base) + def self.inherited(base) #:nodoc: extensions.each { |e| base.extensions << e } end - # @group Stable Attributes - - attr_accessor :margin_box - attr_reader :margins, :y - attr_accessor :page_number - - # @group Extension Attributes - - attr_accessor :text_formatter - - # @group Stable API - # 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 @@ -159,13 +134,12 @@ # <tt>:bottom_margin</tt>:: Sets the bottom margin in points [0.5 inch] # <tt>:skip_page_creation</tt>:: Creates a document without starting the first page [false] # <tt>:compress</tt>:: Compresses content streams before rendering them [false] # <tt>:optimize_objects</tt>:: Reduce number of PDF objects in output, at expense of render time [false] # <tt>:background</tt>:: An image path to be used as background on all pages [nil] - # <tt>:background_scale</tt>:: Backgound image scale [1] [nil] # <tt>:info</tt>:: Generic hash allowing for custom metadata properties [nil] - # <tt>:text_formatter</tt>: The text formatter to use for <tt>:inline_format</tt>ted text [Prawn::Text::Formatted::Parser] + # <tt>:template</tt>:: 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: @@ -190,62 +164,85 @@ # # # New document, Custom size # pdf = Prawn::Document.new(:page_size => [200, 300]) # # # New document, with background - # pdf = Prawn::Document.new(:background => "#{Prawn::DATADIR}/images/pigs.jpg") + # pdf = Prawn::Document.new(:background => "#{Prawn::BASEDIR}/data/images/pigs.jpg") # def initialize(options={},&block) options = options.dup - Prawn.verify_options VALID_OPTIONS, options + 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 = PDF::Core::DocumentState.new(options) + @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 - min_version(1.6) if options[:print_scaling] == :none - @background = options[:background] - @background_scale = options[:background_scale] || 1 @font_size = 12 @bounding_box = nil @margin_box = nil @page_number = 0 - @text_formatter = options.delete(:text_formatter) || Text::Formatted::Parser - options[:size] = options.delete(:page_size) options[:layout] = options.delete(:page_layout) - initialize_first_page(options) + 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 - # @group Stable API + 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) # + # 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 @@ -253,34 +250,36 @@ 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 if last_page.graphic_state + 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 = {} if new_graphic_state + 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 = PDF::Core::Page.new(self, page_options) + state.page = Prawn::Core::Page.new(self, page_options) apply_margin_options(options) generate_margin_box # Reset the bounding box if the new page has different size or layout if last_page && (last_page.size != state.page.size || last_page.layout != state.page.layout) @bounding_box = @margin_box end - use_graphic_settings + state.page.new_content_stream if options[:template] + use_graphic_settings(options[:template]) unless options[:orphan] state.insert_page(state.page, @page_number) @page_number += 1 - canvas { image(@background, :scale => @background_scale, :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 @@ -347,38 +346,32 @@ yield go_to_page(original_page) unless page_number == original_page self.y = original_y end - # Renders the PDF document to string. - # Pass an open file descriptor to render to file. + # Renders the PDF document to string # - def render(output = StringIO.new) - if output.instance_of?(StringIO) - output.set_encoding(::Encoding::ASCII_8BIT) - end + def render + output = StringIO.new finalize_all_page_contents render_header(output) render_body(output) render_xref(output) render_trailer(output) - if output.instance_of?(StringIO) - str = output.string - str.force_encoding(::Encoding::ASCII_8BIT) - return str - else - return nil - end + 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) - File.open(filename, "wb") { |f| render(f) } + 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 <tt>bounding_box</tt> @@ -410,11 +403,10 @@ @bounding_box end # Returns the innermost non-stretchy bounding box. # - # @private def reference_bounds @bounding_box.reference_bounds end # Sets Document#bounds to the BoundingBox provided. See above for a brief @@ -496,38 +488,79 @@ # 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 = {} + 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). 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) + 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 + + success + end + # 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 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. # # Parameters are: - # - # <tt>string</tt>:: Template string for page number wording. + # + # <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. + # <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 + # 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 - # number_pages "<page> in a total of <total>", + # 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} @@ -542,86 +575,46 @@ :all end 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 unless opts.has_key?(:height) - + 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 + 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 + end pseudopage += 1 if start_count end end - # Returns true if content streams will be compressed before rendering, - # false otherwise - # - def compression_enabled? - !!state.compress - end - - # @group Experimental API - - # 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). 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) - old_bounding_box = @bounding_box - @bounding_box = SimpleDelegator.new(@bounding_box) - - # @private - 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 - - success - end - # Provides a way to execute a block of code repeatedly based on a - # page_filter. + # 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 + # 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 @@ -631,51 +624,29 @@ when Range, Array page_filter.include?(page_number) when Proc page_filter.call(page_number) end - end + end - # @private - - def mask(*fields) - # 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 - # @group Extension API - - def initialize_first_page(options) - if options[:skip_page_creation] - start_new_page(options.merge(:orphan => true)) - else - start_new_page(options) - end + # Returns true if content streams will be compressed before rendering, + # false otherwise + # + def compression_enabled? + !!state.compress end - ## Internals. Don't depend on them! + private - # @private - def state - @internal_state + 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 - # @private - def page - state.page - end - - private - - # setting override_settings to true ensures that a new graphic state does not end up using - # previous settings. + # 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 @@ -723,12 +694,8 @@ [:left,:right,:top,:bottom].each do |side| if margin = options[:"#{side}_margin"] state.page.margins[side] = margin end end - end - - def font_metric_cache #:nodoc: - @font_metric_cache ||= FontMetricCache.new( self ) end end end