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

Constants

VERSION

Public Class Methods

new(root, options) click to toggle source
Calls superclass method
# File lib/kramdown/converter/pdf.rb, line 49
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 62
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 57
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 73
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.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 89
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 339
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 404
def abbreviation_options(_el, _opts)
  {}
end
blockquote_options(_el, _opts) click to toggle source
# File lib/kramdown/converter/pdf.rb, line 191
def blockquote_options(_el, _opts)
  {styles: [:italic]}
end
br_options(_el, _opts) click to toggle source
# File lib/kramdown/converter/pdf.rb, line 363
def br_options(_el, _opts)
  {}
end
codeblock_options(_el, opts) click to toggle source
# File lib/kramdown/converter/pdf.rb, line 279
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 355
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 249
def dd_options(_el, _opts)
  {}
end
dl_options(_el, _opts) click to toggle source
# File lib/kramdown/converter/pdf.rb, line 233
def dl_options(_el, _opts)
  {}
end
dt_options(_el, opts) click to toggle source
# File lib/kramdown/converter/pdf.rb, line 241
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 327
def em_options(_el, opts)
  if 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 396
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 269
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 412
def img_options(_el, _opts)
  {}
end
li_options(_el, _opts) click to toggle source
# File lib/kramdown/converter/pdf.rb, line 225
def li_options(_el, _opts)
  {}
end
math_options(_el, _opts) click to toggle source
# File lib/kramdown/converter/pdf.rb, line 257
def math_options(_el, _opts)
  {}
end
ol_options(_el, opts) click to toggle source
# File lib/kramdown/converter/pdf.rb, line 212
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 408
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 195
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 367
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 283
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 359
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 253
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 237
def render_dl(el, opts)
  inner(el, opts)
end
render_dt(el, opts) click to toggle source
# File lib/kramdown/converter/pdf.rb, line 245
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 349
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 400
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 273
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 229
def render_li(el, opts)
  inner(el, opts)
end
render_math(el, opts) click to toggle source
# File lib/kramdown/converter/pdf.rb, line 261
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 216
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 375
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("Requested to render images that are potentially not a JPEG or PNG. "\
            "The image might not be present if it's not in one of these formats. " \
            "Got #{img.attr['src']}#{line ? " on line #{line}" : ''}")
  end

  img_dirs = @options.fetch(:image_directories, []) + ["."]
  path_or_url = img.attr["src"]
  begin
    image_obj, image_info = @pdf.build_image_object(open(path_or_url))
  rescue StandardError
    raise if img_dirs.empty?
    path_or_url = File.join(img_dirs.shift, img.attr["src"])
    retry
  end

  options = {}
  if img.attr['class'] =~ /\balign\-left\b/
    options[:position] = :left
  elsif img.attr['class'] =~ /\balign\-right\b/
    options[:position] = :right
  else
    options[:position] = :center
  end

  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 293
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 323
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 383
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 203
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 371
def smart_quote_options(_el, _opts)
  {}
end
strong_options(_el, opts) click to toggle source
# File lib/kramdown/converter/pdf.rb, line 335
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 289
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 319
def text_options(_el, _opts)
  {}
end
typographic_sym_options(_el, _opts) click to toggle source
# File lib/kramdown/converter/pdf.rb, line 379
def typographic_sym_options(_el, _opts)
  {}
end
ul_options(_el, opts) click to toggle source
# File lib/kramdown/converter/pdf.rb, line 199
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 611
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.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 591
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 599
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.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 583
def with_block_padding(_el, opts)
  @pdf.move_down(opts[:top_padding]) if opts.key?(:top_padding)
  yield
  @pdf.move_down(opts[:bottom_padding]) if opts.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 547
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 518
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 542
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 534
def setup_document(root)
  doc = Prawn::Document.new(document_options(root))
  doc.extend(PrawnDocumentExtension)
  doc.converter = self
  doc
end