Prawn: Table of content with page numbers
Asked Answered
P

4

8

I need to create a table of contents with Prawn. I have add_dest function calls in my code and the right links in the table of content:

add_dest('Komplett', dest_fit(page_count - 1))

and

text "* <link anchor='Komplett'> Vollstaendiges Mitgliederverzeichnis </link>", :inline_format = true

This works and I get clickable links which forward me to the right pages. However, I need to have page numbers in the table of content. How do I get it printed out?

Paradisiacal answered 16/10, 2012 at 9:36 Comment(5)
duplicate of #2688333Beanery
In my Prawn version there is no such file :-(Paradisiacal
I am using prawn-0.12.0 and there is no example/outline.rbParadisiacal
its examples/general/outlines.rbBeanery
And exactly this file is missing! I have only "context_sensitive_headers.rb" in this directory, but NOT outline.rb.Paradisiacal
B
3

you should read the chapter on Outline in this document http://prawn.majesticseacreature.com/manual.pdf, p.96. It explains with examples on how to create TOC.

UPDATE

destinations, page_references = {}, {}

page_count.downto(1).each {|num| page_references[num] = state.store.object_id_for_page(num)}

dests.data.to_hash.each_value do |values|
    values.each do |value|
        value_array             = value.to_s.split(":")
        dest_name               = value_array[0]
        dest_id                 = value_array[1].split[0]
        destinations[dest_name] = Integer(dest_id)
    end 
end 

state.store.each do |reference| 
    if !(dest_name = destinations.key(reference.identifier)).nil?
        puts "Destination - #{dest_name} is on Page #{page_references.key(Integer(reference.data[0].to_s.split[0]))}"
    end 
end   
Beanery answered 16/10, 2012 at 17:0 Comment(5)
MAybe I am wrong or I can't read - but this describes how to create an outline, which is (easily described) a link list to sections or pages within an PDF reader. I have seen this already. What I need is to discover dynamically page number in a table of contents in the document itself, where content changes from time to time (entires go, entries leave) I am looking for something similar the feature of enumerating pages after doc creation. To discover a page number dynamically, assign it to a variable and after doc creation to insert this to the beginning of the document in a table of contentParadisiacal
I have updated my answer. Its hackish but gives you page numbers where each destination(section) is defined. You could take a clue from it on how to actually update TOC.Beanery
Ok, works so far - but still I have to assign page numbers statically. For the time being I will drop dynamic TOC from my to-do list. Anyhow, thanks a lot for helping me.Paradisiacal
I don't really understand this code - where are state and dests defined? And how do you retroactively insert this data into the contents page (which might be at the start of the document)?Skiing
The code above can be simplified in newer versions of Prawn. Using Prawn 2.2.2, you can use destinations = pdf.dests.data.children.collect { |child| [child.name, child.value.data] } to get an array containing names and page numbers for each destination.Labana
H
5

I would suggest a much simpler solution.

  1. Use pdf.page_number to store the page number of all your sections in a hash as you populate the pages

  2. In the code, output the table of contents after populating the rest of your pages. Insert the TOC into the doc in the right spot by navigating in the PDF pdf.go_to_page(page_num).

For example:

render "pdf/frontpage", p: p
toc.merge!(p.page_number => "Section_Title")

p.start_new_page
toc.merge!(p.page_number => "Section_Title")
render "pdf/calendar"

p.start_new_page
toc.merge!(p.page_number => "Section_Title")
render "pdf/another_section"

p.go_to_page(1)
p.start_new_page
toc.merge!(p.page_number => "Table of Contents")
render "pdf/table_of_contents", table_of_contents: toc
Hauge answered 30/8, 2017 at 21:47 Comment(0)
B
3

you should read the chapter on Outline in this document http://prawn.majesticseacreature.com/manual.pdf, p.96. It explains with examples on how to create TOC.

UPDATE

destinations, page_references = {}, {}

page_count.downto(1).each {|num| page_references[num] = state.store.object_id_for_page(num)}

dests.data.to_hash.each_value do |values|
    values.each do |value|
        value_array             = value.to_s.split(":")
        dest_name               = value_array[0]
        dest_id                 = value_array[1].split[0]
        destinations[dest_name] = Integer(dest_id)
    end 
end 

state.store.each do |reference| 
    if !(dest_name = destinations.key(reference.identifier)).nil?
        puts "Destination - #{dest_name} is on Page #{page_references.key(Integer(reference.data[0].to_s.split[0]))}"
    end 
end   
Beanery answered 16/10, 2012 at 17:0 Comment(5)
MAybe I am wrong or I can't read - but this describes how to create an outline, which is (easily described) a link list to sections or pages within an PDF reader. I have seen this already. What I need is to discover dynamically page number in a table of contents in the document itself, where content changes from time to time (entires go, entries leave) I am looking for something similar the feature of enumerating pages after doc creation. To discover a page number dynamically, assign it to a variable and after doc creation to insert this to the beginning of the document in a table of contentParadisiacal
I have updated my answer. Its hackish but gives you page numbers where each destination(section) is defined. You could take a clue from it on how to actually update TOC.Beanery
Ok, works so far - but still I have to assign page numbers statically. For the time being I will drop dynamic TOC from my to-do list. Anyhow, thanks a lot for helping me.Paradisiacal
I don't really understand this code - where are state and dests defined? And how do you retroactively insert this data into the contents page (which might be at the start of the document)?Skiing
The code above can be simplified in newer versions of Prawn. Using Prawn 2.2.2, you can use destinations = pdf.dests.data.children.collect { |child| [child.name, child.value.data] } to get an array containing names and page numbers for each destination.Labana
P
3

I also needed to create a dynamic TOC. I put together a quick spike that needs some clean-up but does pretty much what I want. I didn't include click-able links but they could easily be added. The example also assumes the TOC is being placed on the 2nd page of the document.

The basic strategy I used was to store the TOC in a hash. Each time I add a new section to the document that I want to appear in the TOC I add it to the hash, i.e.

@toc[pdf.page_count] = "the toc text for this section"

Then prior to adding the page numbers to the document I iterate thru the hash:

number_of_toc_entries_per_page = 10
offset = (@toc.count.to_f / number_of_toc_entries_per_page).ceil
@toc.each_with_index do |(key, value), index| 
  pdf.start_new_page if index % number_of_toc_entries_per_page == 0
  pdf.text "#{value}.... page #{key + offset}", size: 38
end

Anyway, the full example is below, hope it helps.

require 'prawn'

class TocTest
  def self.create
    @toc = Hash.new
    @current_section_header_number = 0 # used to fake up section header's
    pdf = Prawn::Document.new

    add_title_page(pdf)
    21.times { add_a_content_page(pdf) }

    fill_in_toc(pdf)

    add_page_numbers(pdf)

    pdf.render_file './output/test.pdf'
  end

  def self.add_title_page(pdf)
    pdf.move_down 200
    pdf.text "This is my title page", size: 38, style: :bold, align: :center
  end

  def self.fill_in_toc(pdf)
    pdf.go_to_page(1)

    number_of_toc_entries_per_page = 10
    offset = (@toc.count.to_f / number_of_toc_entries_per_page).ceil
    @toc.each_with_index do |(key, value), index| 
      pdf.start_new_page if index % number_of_toc_entries_per_page == 0
      pdf.text "#{value}.... page #{key + offset}", size: 38
    end
  end

  def self.add_a_content_page(pdf)
    pdf.start_new_page
    toc_heading = grab_some_section_header_text

    @toc[pdf.page_count] = toc_heading

    pdf.text toc_heading, size: 38, style: :bold
    pdf.text "Here is the content for this section"
    # randomly span a section over 2 pages
    if [true, false].sample
      pdf.start_new_page
      pdf.text "The content for this section spans 2 pages"
    end
  end

  def self.add_page_numbers(pdf)
    page_number_string = 'page <page> of <total>'
    options = {
      at: [pdf.bounds.right - 175, 9], 
      width: 150, 
      align: :right, 
      size: 10,
      page_filter: lambda { |pg| pg > 1 }, 
      start_count_at: 2,
    }
    pdf.number_pages(page_number_string, options)
  end

  def self.grab_some_section_header_text
    "Section #{@current_section_header_number += 1}"
  end
end
Psychotherapy answered 9/4, 2013 at 0:19 Comment(0)
L
1

I built a report generator featuring a clickable table of contents using code and ideas gathered from this discussion. Here is the relevant parts of the code, in case somebody else needs to do the same.

What it does:

  • include Prawn::View to use Prawn's methods without having to prefix them with pdf
  • insert a blank page where the table of contents will be displayed
  • add the document contents, using h1 and h2 helpers for titles
  • the h1 and h2 helpers store the position of headings in the document
  • rewind and generate the actual table of contents
  • indent subsections in the table of contents
  • right-align the dots between toc entry and page number for visual consistency
  • if the table doesn't fit on one page, it adds new pages and increments the relevant page numbers
  • add a PDF outline with the section and subsection titles for bonus points.

Enjoy!

PDF generator

class ReportPdf
  include Prawn::View
  COLOR_GRAY = 'BBBBBB' # Color used for the dots in the table of contents

  def initialize(report)
    @toc = []
    @report = report
    generate_report
  end

  private

  def generate_report
    add_table_of_contents
    add_contents
    update_table_of_contents
    add_outline
  end

  def add_table_of_contents
    # Insert a blank page, which will be filled in later using update_table_of_contents
    start_new_page
  end

  def add_contents
    @report.sections.each do |section|
      h1(section.title, section.anchor)
      section.subsections.each do |subsection|
        h2(subsection.title, subsection.anchor)
        # subsection contents
      end
    end
  end

  def update_table_of_contents
    go_to_page(1) # Rewind to where the table needs to be displayed
    text 'Table of contents', styles_for(:toc_title)
    move_down 20

    added_pages = 0
    @toc.each do |entry|
      unless fits_on_current_page?(entry[:name])
        added_pages += 1
        start_new_page
      end
      entry[:page] += added_pages
      add_toc_line(entry)
      entry[:subsections].each do |subsection_entry|
        unless fits_on_current_page?(subsection_entry[:name])
          added_pages += 1
          start_new_page
        end
        subsection_entry[:page] += added_pages
        add_toc_line(subsection_entry, true)
      end
    end
  end

  def add_outline
    outline.section 'Table of contents', destination: 2
    @toc.each do |entry|
      outline.section entry[:name], destination: entry[:page] do
        entry[:subsections].each do |subsection|
          outline.page title: subsection[:name], destination: subsection[:page]
        end
      end
    end
  end

  def h1(name, anchor)
    add_anchor(anchor, name)
    text name, styles_for(:h1)
  end

  def h2(name, anchor)
    add_anchor(anchor, name, true)
    text name, styles_for(:h2)
  end

  def styles_for(element = :p)
    case element
    when :toc_title then { size: 24, align: :center }
    when :h1 then { size: 20, align: :left }
    when :h2 then { size: 16, align: :left }
    when :p  then { size: 12, align: :justify }
    end
  end

  def add_anchor(name, anchor, is_subsection = false)
    add_dest anchor, dest_xyz(bounds.absolute_left, y + 20)
    if is_subsection
      @toc.last[:subsections] << { anchor: anchor, name: name, page: page_count }
    else
      @toc << { anchor: anchor, name: name, page: page_count, subsections: [] }
    end
  end

  def add_toc_line(entry, is_subsection = false)
    anchor = entry[:anchor]
    name = entry[:name]
    name = "#{Prawn::Text::NBSP * 5}#{name}" if is_subsection
    page_number = entry[:page].to_s
    dots_info = dots_for(name + ' ' + page_number)

    float do
      text "<link anchor='#{anchor}'>#{name}</link>", inline_format: true
    end
    float do
      indent(dots_info[:dots_start], dots_info[:right_margin]) do
        text "<color rgb='#{COLOR_GRAY}'>#{dots_info[:dots]}</color>", inline_format: true, align: :right
      end
    end
    indent(dots_info[:dots_end]) do
      text "<link anchor='#{anchor}'>#{page_number}</link>", inline_format: true, align: :right
    end
  end

  def dots_for(text)
    dot_width  = text_width('.')
    dots_start = text_width(text)
    right_margin = text_width(' ') * 6

    space_for_dots = bounds.width - dots_start - right_margin
    dots = space_for_dots.negative? ? '' : '.' * (space_for_dots / dot_width)
    dots_end = space_for_dots - right_margin
    {
      dots: dots,
      dots_start: dots_start,
      dots_end: dots_end,
      right_margin: right_margin
    }
  end

  def fits_on_current_page?(str)
    remaining_height = bounds.top - bounds.absolute_top + y
    height_of(str) < remaining_height
  end

  def text_width(str, size = 12)
    font(current_font).compute_width_of(str, size: size)
  end

  def current_font
    @current_font ||= font.inspect.split('<')[1].split(':')[0].strip
  end
end

Using the generator

Using Rails, I generate PDFs from a report using the following code:

# app/models/report.rb
class Report < ApplicationRecord
  # Additional methods
  def pdf
    @pdf ||= ReportPdf.new(self)
  end
end

# app/controllers/reports_controller.rb
class ReportsController < ApplicationController
  def show
    respond_to do |format|
      format.html
      format.pdf do
        doc = @report.pdf
        send_data doc.render, filename: doc.filename, disposition: :inline, type: Mime::Type.lookup_by_extension(:pdf)
      end
    end
end
Labana answered 7/5, 2020 at 14:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.