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