# class Kramdown::Converter::Pdf

Converts an element tree to a PDF using the prawn PDF library.

This basic version provides a nice starting point for customizations but can also be used directly.

There can be the following two methods for each element type: render_TYPE(el, opts) and TYPE_options(el, opts) where `el` is a kramdown element and `opts` an hash with rendering options.

The render_TYPE(el, opts) is used for rendering the specific element. If the element is a span element, it should return a hash or an array of hashes that can be used by the formatted_text method of Prawn::Document. This method can then be used in block elements to actually render the span elements.

The rendering options are passed from the parent to its child elements. This allows one to define general options at the top of the tree (the root element) that can later be changed or amended.

Currently supports the conversion of all elements except those of the following types:

    :html_element, :img, :footnote

### Public Class Methods

new(root, options) click to toggle source
Calls superclass method Kramdown::Converter::Base.new

    # File lib/kramdown/converter/pdf.rb, line 47
    def initialize(root, options)
      super
      @stack = []
      @dests = {}
    end

### Public Instance Methods

apply_template_after?() click to toggle source

Returns `false`.

    # File lib/kramdown/converter/pdf.rb, line 60
    def apply_template_after?
      false
    end

apply_template_before?() click to toggle source

PDF templates are applied before conversion. They should contain code to augment the converter object (i.e. to override the methods).

    # File lib/kramdown/converter/pdf.rb, line 55
    def apply_template_before?
      true
    end

convert(el, opts = {}) click to toggle source

Invoke the special rendering method for the given element `el`.

A PDF destination is also added at the current location if th element has an ID or if the element is of type :header and the :auto_ids option is set.

    # File lib/kramdown/converter/pdf.rb, line 71
    def convert(el, opts = {})
      id = el.attr['id']
      id = generate_id(el.options[:raw_text]) if !id && @options[:auto_ids] && el.type == :header
      if !id.to_s.empty? && !@dests.has_key?(id)
        @pdf.add_dest(id, @pdf.dest_xyz(0, @pdf.y))
        @dests[id] = @pdf.dest_xyz(0, @pdf.y)
      end
      send(DISPATCHER_RENDER[el.type], el, opts)
    end

### Protected Instance Methods

inner(el, opts) click to toggle source

Render the children of this element with the given options and return the results as array.

Each time a child is rendered, the `TYPE_options` method is invoked (if it exists) to get the specific options for the element with which the given options are updated.

    # File lib/kramdown/converter/pdf.rb, line 87
    def inner(el, opts)
      @stack.push([el, opts])
      result = el.children.map do |inner_el|
        options = opts.dup
        options.update(send(DISPATCHER_OPTIONS[inner_el.type], inner_el, options))
        convert(inner_el, options)
      end.flatten.compact
      @stack.pop
      result
    end

## Element rendering methods

↑ top

### Protected Instance Methods

a_options(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 332
    def a_options(el, opts)
      hash = {:color => '000088'}
      if el.attr['href'].start_with?('#')
        hash[:anchor] = el.attr['href'].sub(/\A#/, '')
      else
        hash[:link] = el.attr['href']
      end
      hash
    end

abbreviation_options(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 397
    def abbreviation_options(el, opts)
      {}
    end

blockquote_options(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 180
    def blockquote_options(el, opts)
      {:styles => [:italic]}
    end

br_options(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 356
    def br_options(el, opts)
      {}
    end

codeblock_options(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 268
    def codeblock_options(el, opts)
      {
        :font => 'Courier', :color => '880000',
        :bottom_padding => opts[:size]
      }
    end

codespan_options(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 348
    def codespan_options(el, opts)
      {:font => 'Courier', :color => '880000'}
    end

dd_options(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 238
    def dd_options(el, opts)
      {}
    end

dl_options(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 222
    def dl_options(el, opts)
      {}
    end

dt_options(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 230
    def dt_options(el, opts)
      {:styles => (opts[:styles] || []) + [:bold], :bottom_padding => 0}
    end

em_options(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 320
    def em_options(el, opts)
      if opts[:styles] && opts[:styles].include?(:italic)
        {:styles => opts[:styles].reject {|i| i == :italic}}
      else
        {:styles => (opts[:styles] || []) << :italic}
      end
    end

entity_options(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 389
    def entity_options(el, opts)
      {}
    end

header_options(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 116
    def header_options(el, opts)
      size = opts[:size] * 1.15**(6 - el.options[:level])
      {
        :font => "Helvetica", :styles => (opts[:styles] || []) + [:bold],
        :size => size, :bottom_padding => opts[:size], :top_padding => opts[:size]
      }
    end

hr_options(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 258
    def hr_options(el, opts)
      {:top_padding => opts[:size], :bottom_padding => opts[:size]}
    end

img_options(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 405
    def img_options(el, opts)
      {}
    end

li_options(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 214
    def li_options(el, opts)
      {}
    end

math_options(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 246
    def math_options(el, opts)
      {}
    end

ol_options(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 201
    def ol_options(el, opts)
      {:bottom_padding => opts[:size]}
    end

p_options(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 128
    def p_options(el, opts)
      bpad = (el.options[:transparent] ? opts[:leading] : opts[:size])
      {:align => :justify, :bottom_padding => bpad}
    end

render_a(el, opts)
Alias for: render_em
render_abbreviation(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 401
    def render_abbreviation(el, opts)
      text_hash(el.value, opts)
    end

render_blockquote(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 184
    def render_blockquote(el, opts)
      @pdf.indent(mm2pt(10), mm2pt(10)) { inner(el, opts) }
    end

render_br(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 360
    def render_br(el, opts)
      text_hash("\n", opts, false)
    end

render_codeblock(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 275
    def render_codeblock(el, opts)
      with_block_padding(el, opts) do
        @pdf.formatted_text([text_hash(el.value, opts, false)], block_hash(opts))
      end
    end

render_codespan(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 352
    def render_codespan(el, opts)
      text_hash(el.value, opts)
    end

render_dd(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 242
    def render_dd(el, opts)
      @pdf.indent(mm2pt(10)) { inner(el, opts) }
    end

render_dl(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 226
    def render_dl(el, opts)
      inner(el, opts)
    end

render_dt(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 234
    def render_dt(el, opts)
      render_padded_and_formatted_text(el, opts)
    end

render_em(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 342
    def render_em(el, opts)
      inner(el, opts)
    end

Also aliased as: render_strong, render_a
render_entity(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 393
    def render_entity(el, opts)
      text_hash(el.value.char, opts)
    end

render_header(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 124
    def render_header(el, opts)
      render_padded_and_formatted_text(el, opts)
    end

render_hr(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 262
    def render_hr(el, opts)
      with_block_padding(el, opts) do
        @pdf.stroke_horizontal_line(@pdf.bounds.left + mm2pt(5), @pdf.bounds.right - mm2pt(5))
      end
    end

render_li(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 218
    def render_li(el, opts)
      inner(el, opts)
    end

render_math(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 250
    def render_math(el, opts)
      if el.options[:category] == :block
        @pdf.formatted_text([{:text => el.value}], block_hash(opts))
      else
        {:text => el.value}
      end
    end

render_ol(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 205
    def render_ol(el, opts)
      with_block_padding(el, opts) do
        el.children.each_with_index do |li, index|
          @pdf.float { @pdf.formatted_text([text_hash("#{index+1}.", opts)]) }
          @pdf.indent(mm2pt(6)) { convert(li, opts) }
        end
      end
    end

render_p(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 133
    def render_p(el, opts)
      if el.children.size == 1 && el.children.first.type == :img
        render_standalone_image(el, opts)
      else
        render_padded_and_formatted_text(el, opts)
      end
    end

render_root(root, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 108
    def render_root(root, opts)
      @pdf = setup_document(root)
      inner(root, root_options(root, opts))
      create_outline(root)
      finish_document(root)
      @pdf.render
    end

render_smart_quote(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 368
    def render_smart_quote(el, opts)
      text_hash(smart_quote_entity(el).char, opts)
    end

render_standalone_image(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 141
    def render_standalone_image(el, opts)
      img = el.children.first
      line = img.options[:location]

      if img.attr['src'].empty?
        warning("Rendering an image without a source is not possible#{line ? " (line #{line})" : ''}")
        return nil
      elsif img.attr['src'] !~ /\.jpe?g$|\.png$/
        warning("Cannot render images other than JPEG or PNG, got #{img.attr['src']}#{line ? " on line #{line}" : ''}")
        return nil
      end

      img_dirs = (@options[:image_directories] || ['.']).dup
      begin
        img_path = File.join(img_dirs.shift, img.attr['src'])
        image_obj, image_info = @pdf.build_image_object(open(img_path))
      rescue
        img_dirs.empty? ? raise : retry
      end

      options = {:position => :center}
      if img.attr['height'] && img.attr['height'] =~ /px$/
        options[:height] = img.attr['height'].to_i / (@options[:image_dpi] || 150.0) * 72
      elsif img.attr['width'] && img.attr['width'] =~ /px$/
        options[:width] = img.attr['width'].to_i / (@options[:image_dpi] || 150.0) * 72
      else
        options[:scale] =[(@pdf.bounds.width - mm2pt(20)) / image_info.width.to_f, 1].min
      end

      if img.attr['class'] =~ /\bright\b/
        options[:position] = :right
        @pdf.float { @pdf.embed_image(image_obj, image_info, options) }
      else
        with_block_padding(el, opts) do
          @pdf.embed_image(image_obj, image_info, options)
        end
      end
    end

render_strong(el, opts)
Alias for: render_em
render_table(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 285
    def render_table(el, opts)
      data = []
      el.children.each do |container|
        container.children.each do |row|
          data << []
          row.children.each do |cell|
            if cell.children.any? {|child| child.options[:category] == :block}
              line = el.options[:location]
              warning("Can't render tables with cells containing block elements#{line ? " (line #{line})" : ''}")
              return
            end
            cell_data = inner(cell, opts)
            data.last << cell_data.map {|c| c[:text]}.join('')
          end
        end
      end
      with_block_padding(el, opts) do
        @pdf.table(data, :width => @pdf.bounds.right) do
          el.options[:alignment].each_with_index do |alignment, index|
            columns(index).align = alignment unless alignment == :default
          end
        end
      end
    end

render_text(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 316
    def render_text(el, opts)
      text_hash(el.value.to_s, opts)
    end

render_typographic_sym(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 376
    def render_typographic_sym(el, opts)
      str = if el.value == :laquo_space
              ::Kramdown::Utils::Entities.entity('laquo').char +
                ::Kramdown::Utils::Entities.entity('nbsp').char
            elsif el.value == :raquo_space
              ::Kramdown::Utils::Entities.entity('raquo').char +
                ::Kramdown::Utils::Entities.entity('nbsp').char
            else
              ::Kramdown::Utils::Entities.entity(el.value.to_s).char
            end
      text_hash(str, opts)
    end

render_ul(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 192
    def render_ul(el, opts)
      with_block_padding(el, opts) do
        el.children.each do |li|
          @pdf.float { @pdf.formatted_text([text_hash("•", opts)]) }
          @pdf.indent(mm2pt(6)) { convert(li, opts) }
        end
      end
    end

root_options(root, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 104
    def root_options(root, opts)
      {:font => 'Times-Roman', :size => 12, :leading => 2}
    end

smart_quote_options(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 364
    def smart_quote_options(el, opts)
      {}
    end

strong_options(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 328
    def strong_options(el, opts)
      {:styles => (opts[:styles] || []) + [:bold]}
    end

table_options(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 281
    def table_options(el, opts)
      {:bottom_padding => opts[:size]}
    end

text_options(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 312
    def text_options(el, opts)
      {}
    end

typographic_sym_options(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 372
    def typographic_sym_options(el, opts)
      {}
    end

ul_options(el, opts) click to toggle source

    # File lib/kramdown/converter/pdf.rb, line 188
    def ul_options(el, opts)
      {:bottom_padding => opts[:size]}
    end

## Helper methods

↑ top

### Protected Instance Methods

block_hash(opts) click to toggle source

Helper function that returns a hash with valid options for the prawn text_box extracted from the given options.

    # File lib/kramdown/converter/pdf.rb, line 612
    def block_hash(opts)
      hash = {}
      [:align, :valign, :mode, :final_gap, :leading, :fallback_fonts,
       :direction, :indent_paragraphs].each do |key|
        hash[key] = opts[key] if opts.has_key?(key)
      end
      hash
    end

render_padded_and_formatted_text(el, opts) click to toggle source

Render the children of the given element as formatted text and respect the top/bottom padding (see with_block_padding).

    # File lib/kramdown/converter/pdf.rb, line 592
    def render_padded_and_formatted_text(el, opts)
      with_block_padding(el, opts) { @pdf.formatted_text(inner(el, opts), block_hash(opts)) }
    end

text_hash(text, opts, squeeze_whitespace = true) click to toggle source

Helper function that returns a hash with valid “formatted text” options.

The `text` parameter is used as value for the :text key and if `squeeze_whitespace` is `true`, all whitespace is converted into spaces.

    # File lib/kramdown/converter/pdf.rb, line 600
    def text_hash(text, opts, squeeze_whitespace = true)
      text = text.gsub(/\s+/, ' ') if squeeze_whitespace
      hash = {:text => text}
      [:styles, :size, :character_spacing, :font, :color, :link,
       :anchor, :draw_text_callback, :callback].each do |key|
        hash[key] = opts[key] if opts.has_key?(key)
      end
      hash
    end

with_block_padding(el, opts) { || ... } click to toggle source

Move the prawn document cursor down before and/or after yielding the given block.

The :top_padding and :bottom_padding options are used for determinig the padding amount.

    # File lib/kramdown/converter/pdf.rb, line 584
    def with_block_padding(el, opts)
      @pdf.move_down(opts[:top_padding]) if opts.has_key?(:top_padding)
      yield
      @pdf.move_down(opts[:bottom_padding]) if opts.has_key?(:bottom_padding)
    end

## Organizational methods

↑ top

### Protected Instance Methods

create_outline(root) click to toggle source

Create the PDF outline from the header elements in the TOC.

    # File lib/kramdown/converter/pdf.rb, line 546
    def create_outline(root)
      toc = ::Kramdown::Converter::Toc.convert(root).first

      text_of_header = lambda do |el|
        if el.type == :text
          el.value
        else
          el.children.map {|c| text_of_header.call(c)}.join('')
        end
      end

      add_section = lambda do |item, parent|
        text = text_of_header.call(item.value)
        destination = @dests[item.attr[:id]]
        if !parent
          @pdf.outline.page(:title => text, :destination => destination)
        else
          @pdf.outline.add_subsection_to(parent) do
            @pdf.outline.page(:title => text, :destination => destination)
          end
        end
        item.children.each {|c| add_section.call(c, text)}
      end

      toc.children.each do |item|
        add_section.call(item, nil)
      end
    end

document_options(root) click to toggle source

Return a hash with options that are suitable for Prawn::Document.new.

Used in setup_document.

    # File lib/kramdown/converter/pdf.rb, line 515
    def document_options(root)
      {
        :page_size => 'A4', :page_layout => :portrait, :margin => mm2pt(20),
        :info => {
          :Creator => 'kramdown PDF converter',
          :CreationDate => Time.now
        },
        :compress => true, :optimize_objects => true
      }
    end

finish_document(root) click to toggle source

Used in render_root.

    # File lib/kramdown/converter/pdf.rb, line 541
    def finish_document(root)
      # no op
    end

setup_document(root) click to toggle source

Create a Prawn::Document object and return it.

Can be used to define repeatable content or register fonts.

Used in render_root.

    # File lib/kramdown/converter/pdf.rb, line 531
    def setup_document(root)
      doc = Prawn::Document.new(document_options(root))
      doc.extend(PrawnDocumentExtension)
      doc.converter = self
      doc
    end