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
# 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