# encoding: utf-8 # # generates outline dictionary and items for document # # Author Jonathan Greenberg require 'forwardable' module Prawn class Document # See Outline#define below for documentation def define_outline(&block) outline.define(&block) end # The Outline dictionary (12.3.3) for this document. It is # lazily initialized, so that documents that do not have an outline # do not incur the additional overhead. def outline_root(outline_root) @store.root.data[:Outlines] ||= ref!(outline_root) end # Lazily instantiates an Outline object for document. This is used as point of entry # to methods to build the outline tree. def outline @outline ||= Outline.new(self) end end # The Outline class organizes the outline tree items for the document. # Note that the prev and parent instance variables are adjusted while navigating # through the nested blocks. These variables along with the presence or absense # of blocks are the primary means by which the relations for the various # OutlineItems and the OutlineRoot are set. Unfortunately, the best way to # understand how this works is to follow the method calls through a real example. # # Some ideas for the organization of this class were gleaned from name_tree. In # particular the way in which the OutlineItems are finally rendered into document # objects in PdfObject through a hash. # class Outline extend Forwardable def_delegator :@document, :page_number attr_accessor :parent attr_accessor :prev attr_accessor :document attr_accessor :outline_root attr_accessor :items def initialize(document) @document = document @outline_root = document.outline_root(OutlineRoot.new) @parent = outline_root @prev = nil @items = {} end # Defines an outline for the document. # The outline is an optional nested index that appears on the side of a PDF # document usually with direct links to pages. The outline DSL is defined by nested # blocks involving two methods: section and page. # # section(title, options{}, &block) # title: the outline text that appears for the section. # options: page - optional integer defining the page number for a destination link. # - currently only :FIT destination supported with link to top of page. # closed - whether the section should show its nested outline elements. # - defaults to false. # page(page, options{}) # page: integer defining the page number for the destination link. # currently only :FIT destination supported with link to top of page. # set to nil if destination link is not desired. # options: title - the outline text that appears for the section. # closed - whether the section should show its nested outline elements. # - defaults to false. # # The syntax is best illustrated with an example: # # Prawn::Document.generate(outlined document) do # text "Page 1. This is the first Chapter. " # start_new_page # text "Page 2. More in the first Chapter. " # start_new_page # define_outline do # section 'Chapter 1', :page => 1, :closed => true do # page 1, :title => 'Page 1' # page 2, :title => 'Page 2' # end # end # end # # It should be noted that not defining a title for a page element will raise # a RequiredOption error # def define(&block) if block block.arity < 1 ? instance_eval(&block) : block[self] end end # Adds an outine section to the outline tree (see define_outline). # Although you will probably choose to exclusively use define_outline so # that your outline tree is contained and easy to manage, this method # gives you the option to add sections to the outline tree at any point # during document generation. Note that the section will be added at the # top level at the end of the outline. For more a more flexible API try # using outline.insert_section_after. # # block uses the same DSL syntax as define_outline, for example: # # outline.add_section do # section 'Added Section', :page => 3 do # page 3, :title => 'Page 3' # end # end def add_section(&block) @parent = outline_root @prev = outline_root.data.last if block block.arity < 1 ? instance_eval(&block) : block[self] end end # Inserts an outline section to the outline tree (see define_outline). # Although you will probably choose to exclusively use define_outline so # that your outline tree is contained and easy to manage, this method # gives you the option to insert sections to the outline tree at any point # during document generation. Unlike outline.add_section, this method allows # you to enter a section after any other item at any level in the outline tree. # Currently the only way to locate the place of entry is with the title for the # item. If your titles names are not unique consider using define_outline. # # block uses the same DSL syntax as define_outline, for example: # # go_to_page 2 # start_new_page # text "Inserted Page" # outline.insert_section_after :title => 'Page 2' do # page page_number, :title => "Inserted Page" # end # def insert_section_after(title, &block) @prev = items[title] if @prev @parent = @prev.data.parent nxt = @prev.data.next if block block.arity < 1 ? instance_eval(&block) : block[self] end adjust_relations(nxt) else raise Prawn::Errors::UnknownOutlineTitle, "\n No outline item with title: '#{title}' exists in the outline tree" end end private def section(title, options = {}, &block) add_outline_item(title, options, &block) end def page(page = nil, options = {}) if options[:title] title = options[:title] options[:page] = page else raise Prawn::Errors::RequiredOption, "\nTitle is a required option for page" end add_outline_item(title, options) end def add_outline_item(title, options, &block) outline_item = create_outline_item(title, options) set_relations(outline_item) increase_count set_variables_for_block(outline_item, block) block.call if block reset_parent(outline_item) end def create_outline_item(title, options) outline_item = OutlineItem.new(title, parent, options) if options[:page] page_index = options[:page] - 1 outline_item.dest = [document.pages[page_index].dictionary, :Fit] end outline_item.prev = prev if @prev items[title] = document.ref!(outline_item) end def set_relations(outline_item) prev.data.next = outline_item if prev parent.data.first = outline_item unless prev parent.data.last = outline_item end def increase_count counting_parent = parent while counting_parent counting_parent.data.count += 1 if counting_parent == outline_root counting_parent = nil else counting_parent = counting_parent.data.parent end end end def set_variables_for_block(outline_item, block) self.prev = block ? nil : outline_item self.parent = outline_item if block end def reset_parent(outline_item) if parent == outline_item self.prev = outline_item self.parent = outline_item.data.parent end end def adjust_relations(nxt) if nxt nxt.data.prev = @prev @prev.data.next = nxt @parent.data.last = nxt else @parent.data.last = @prev end end end class OutlineRoot #:nodoc: attr_accessor :count, :first, :last def initialize @count = 0 end def to_hash {:Type => :Outlines, :Count => count, :First => first, :Last => last} end end class OutlineItem #:nodoc: attr_accessor :count, :first, :last, :next, :prev, :parent, :title, :dest, :closed def initialize(title, parent, options) @closed = options[:closed] @title = title @parent = parent @count = 0 end def to_hash hash = { :Title => Prawn::LiteralString.new(title), :Parent => parent, :Count => closed ? -count : count } [{:First => first}, {:Last => last}, {:Next => @next}, {:Prev => prev}, {:Dest => dest}].each do |h| unless h.values.first.nil? hash.merge!(h) end end hash end end end