lib/prawn/document.rb in prawn-1.0.0.rc2 vs lib/prawn/document.rb in prawn-1.0.0

- old
+ new

@@ -5,18 +5,17 @@ # 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/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,21 +49,30 @@ # # 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 PDF::Core::Annotations + include PDF::Core::Destinations 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, + :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: @@ -81,18 +89,32 @@ # # Prawn::Document.generate("foo.pdf") do # party! # end # + # def self.extensions @extensions ||= [] end - def self.inherited(base) #:nodoc: + # @private + def self.inherited(base) 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 @@ -133,15 +155,14 @@ # <tt>:right_margin</tt>:: Sets the right margin in points [0.5 inch] # <tt>:top_margin</tt>:: Sets the top margin in points [0.5 inch] # <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>:template</tt>:: The path to an existing PDF file to use as a template [nil] + # <tt>:text_formatter</tt>: The text formatter to use for <tt>:inline_format</tt>ted text [Prawn::Text::Formatted::Parser] # # 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: @@ -171,84 +192,57 @@ # pdf = Prawn::Document.new(:background => "#{Prawn::DATADIR}/images/pigs.jpg") # def initialize(options={},&block) options = options.dup - 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 + Prawn.verify_options VALID_OPTIONS, 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 = PDF::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) - 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 + initialize_first_page(options) @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 + # @group Stable API - 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) - # - # Note: templates get indexed by either the object_id of the filename or stream - # entered so that if you reuse the same template multiple times be sure to use the - # same instance for more efficient use of resources and smaller rendered pdfs. 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 @@ -256,30 +250,28 @@ 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 + new_graphic_state = last_page.graphic_state.dup if last_page.graphic_state #erase the color space so that it gets reset on new page for fussy pdf-readers - new_graphic_state.color_space = {} + new_graphic_state.color_space = {} if new_graphic_state 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) + state.page = PDF::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 - 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 @@ -352,32 +344,38 @@ yield go_to_page(original_page) unless page_number == original_page self.y = original_y end - # Renders the PDF document to string + # Renders the PDF document to string. + # Pass an open file descriptor to render to file. # - def render - output = StringIO.new + def render(output = StringIO.new) + if output.instance_of?(StringIO) + output.set_encoding(::Encoding::ASCII_8BIT) + end 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 + if output.instance_of?(StringIO) + str = output.string + str.force_encoding(::Encoding::ASCII_8BIT) + return str + else + return nil + end 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 } + File.open(filename, "wb") { |f| render(f) } 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> @@ -409,10 +407,11 @@ @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 @@ -494,51 +493,10 @@ # 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 @@ -607,10 +565,46 @@ 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. + # + # @private + def group(*a, &b) + raise NotImplementedError, + "Document#group has been disabled because its implementation "+ + "lead to corrupted documents whenever a page boundary was "+ + "crossed. We will try to work on reimplementing it in a "+ + "future release" + end + + # @private + def transaction + raise NotImplementedError, + "Document#transaction has been disabled because its implementation "+ + "lead to corrupted documents whenever a page boundary was "+ + "crossed. We will try to work on reimplementing it in a "+ + "future release" + 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 @@ -632,27 +626,49 @@ when Proc page_filter.call(page_number) 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 - # Returns true if content streams will be compressed before rendering, - # false otherwise - # - def compression_enabled? - !!state.compress + # @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 end - private + ## Internals. Don't depend on them! - 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, :page_template => true) + # @private + def state + @internal_state 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 especially from imported template streams + # previous settings. 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 @@ -678,13 +694,11 @@ @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 + @bounding_box = @margin_box unless @bounding_box && @bounding_box.parent end def apply_margin_options(options) if options[:margin] # Treat :margin as CSS shorthand with 1-4 values. @@ -700,8 +714,12 @@ [: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