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
# File lib/kramdown/converter/pdf.rb, line 46 def initialize(root, options) super @stack = [] @dests = {} end
Returns false
.
# File lib/kramdown/converter/pdf.rb, line 59 def apply_template_after? false end
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 54 def apply_template_before? true end
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 70 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
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 86 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
# File lib/kramdown/converter/pdf.rb, line 331 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
# File lib/kramdown/converter/pdf.rb, line 396 def abbreviation_options(el, opts) {} end
# File lib/kramdown/converter/pdf.rb, line 179 def blockquote_options(el, opts) {:styles => [:italic]} end
# File lib/kramdown/converter/pdf.rb, line 355 def br_options(el, opts) {} end
# File lib/kramdown/converter/pdf.rb, line 267 def codeblock_options(el, opts) { :font => 'Courier', :color => '880000', :bottom_padding => opts[:size] } end
# File lib/kramdown/converter/pdf.rb, line 347 def codespan_options(el, opts) {:font => 'Courier', :color => '880000'} end
# File lib/kramdown/converter/pdf.rb, line 237 def dd_options(el, opts) {} end
# File lib/kramdown/converter/pdf.rb, line 221 def dl_options(el, opts) {} end
# File lib/kramdown/converter/pdf.rb, line 229 def dt_options(el, opts) {:styles => (opts[:styles] || []) + [:bold], :bottom_padding => 0} end
# File lib/kramdown/converter/pdf.rb, line 319 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
# File lib/kramdown/converter/pdf.rb, line 388 def entity_options(el, opts) {} end
# File lib/kramdown/converter/pdf.rb, line 115 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
# File lib/kramdown/converter/pdf.rb, line 257 def hr_options(el, opts) {:top_padding => opts[:size], :bottom_padding => opts[:size]} end
# File lib/kramdown/converter/pdf.rb, line 404 def img_options(el, opts) {} end
# File lib/kramdown/converter/pdf.rb, line 213 def li_options(el, opts) {} end
# File lib/kramdown/converter/pdf.rb, line 245 def math_options(el, opts) {} end
# File lib/kramdown/converter/pdf.rb, line 200 def ol_options(el, opts) {:bottom_padding => opts[:size]} end
# File lib/kramdown/converter/pdf.rb, line 127 def p_options(el, opts) bpad = (el.options[:transparent] ? opts[:leading] : opts[:size]) {:align => :justify, :bottom_padding => bpad} end
# File lib/kramdown/converter/pdf.rb, line 400 def render_abbreviation(el, opts) text_hash(el.value, opts) end
# File lib/kramdown/converter/pdf.rb, line 183 def render_blockquote(el, opts) @pdf.indent(mm2pt(10), mm2pt(10)) { inner(el, opts) } end
# File lib/kramdown/converter/pdf.rb, line 359 def render_br(el, opts) text_hash("\n", opts, false) end
# File lib/kramdown/converter/pdf.rb, line 274 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
# File lib/kramdown/converter/pdf.rb, line 351 def render_codespan(el, opts) text_hash(el.value, opts) end
# File lib/kramdown/converter/pdf.rb, line 241 def render_dd(el, opts) @pdf.indent(mm2pt(10)) { inner(el, opts) } end
# File lib/kramdown/converter/pdf.rb, line 225 def render_dl(el, opts) inner(el, opts) end
# File lib/kramdown/converter/pdf.rb, line 233 def render_dt(el, opts) render_padded_and_formatted_text(el, opts) end
# File lib/kramdown/converter/pdf.rb, line 341 def render_em(el, opts) inner(el, opts) end
# File lib/kramdown/converter/pdf.rb, line 392 def render_entity(el, opts) text_hash(el.value.char, opts) end
# File lib/kramdown/converter/pdf.rb, line 123 def render_header(el, opts) render_padded_and_formatted_text(el, opts) end
# File lib/kramdown/converter/pdf.rb, line 261 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
# File lib/kramdown/converter/pdf.rb, line 217 def render_li(el, opts) inner(el, opts) end
# File lib/kramdown/converter/pdf.rb, line 249 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
# File lib/kramdown/converter/pdf.rb, line 204 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
# File lib/kramdown/converter/pdf.rb, line 132 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
# File lib/kramdown/converter/pdf.rb, line 107 def render_root(root, opts) @pdf = setup_document(root) inner(root, root_options(root, opts)) create_outline(root) finish_document(root) @pdf.render end
# File lib/kramdown/converter/pdf.rb, line 367 def render_smart_quote(el, opts) text_hash(smart_quote_entity(el).char, opts) end
# File lib/kramdown/converter/pdf.rb, line 140 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
# File lib/kramdown/converter/pdf.rb, line 284 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
# File lib/kramdown/converter/pdf.rb, line 315 def render_text(el, opts) text_hash(el.value.to_s, opts) end
# File lib/kramdown/converter/pdf.rb, line 375 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
# File lib/kramdown/converter/pdf.rb, line 191 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
# File lib/kramdown/converter/pdf.rb, line 103 def root_options(root, opts) {:font => 'Times-Roman', :size => 12, :leading => 2} end
# File lib/kramdown/converter/pdf.rb, line 363 def smart_quote_options(el, opts) {} end
# File lib/kramdown/converter/pdf.rb, line 327 def strong_options(el, opts) {:styles => (opts[:styles] || []) + [:bold]} end
# File lib/kramdown/converter/pdf.rb, line 280 def table_options(el, opts) {:bottom_padding => opts[:size]} end
# File lib/kramdown/converter/pdf.rb, line 311 def text_options(el, opts) {} end
# File lib/kramdown/converter/pdf.rb, line 371 def typographic_sym_options(el, opts) {} end
# File lib/kramdown/converter/pdf.rb, line 187 def ul_options(el, opts) {:bottom_padding => opts[:size]} end
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.has_key?(key) end hash end
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
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.has_key?(key) end hash end
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.has_key?(:top_padding) yield @pdf.move_down(opts[:bottom_padding]) if opts.has_key?(:bottom_padding) end
Create the PDF outline from the header elements in the TOC.
# File lib/kramdown/converter/pdf.rb, line 545 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
Return a hash with options that are suitable for Prawn::Document.new.
Used in setup_document.
# File lib/kramdown/converter/pdf.rb, line 514 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
Used in render_root.
# File lib/kramdown/converter/pdf.rb, line 540 def finish_document(root) # no op end
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 530 def setup_document(root) doc = Prawn::Document.new(document_options(root)) doc.extend(PrawnDocumentExtension) doc.converter = self doc end