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
# File lib/kramdown/converter/pdf.rb, line 49 def initialize(root, options) super @stack = [] @dests = {} end
Public Instance Methods
Returns false
.
# File lib/kramdown/converter/pdf.rb, line 62 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 57 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 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
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
↑ topProtected Instance Methods
# 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
# File lib/kramdown/converter/pdf.rb, line 404 def abbreviation_options(_el, _opts) {} end
# File lib/kramdown/converter/pdf.rb, line 191 def blockquote_options(_el, _opts) {styles: [:italic]} end
# File lib/kramdown/converter/pdf.rb, line 363 def br_options(_el, _opts) {} end
# File lib/kramdown/converter/pdf.rb, line 279 def codeblock_options(_el, opts) {font: 'Courier', color: '880000', bottom_padding: opts[:size]} end
# File lib/kramdown/converter/pdf.rb, line 355 def codespan_options(_el, _opts) {font: 'Courier', color: '880000'} end
# File lib/kramdown/converter/pdf.rb, line 249 def dd_options(_el, _opts) {} end
# File lib/kramdown/converter/pdf.rb, line 233 def dl_options(_el, _opts) {} end
# File lib/kramdown/converter/pdf.rb, line 241 def dt_options(_el, opts) {styles: (opts[:styles] || []) + [:bold], bottom_padding: 0} end
# 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
# File lib/kramdown/converter/pdf.rb, line 396 def entity_options(_el, _opts) {} end
# 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
# File lib/kramdown/converter/pdf.rb, line 269 def hr_options(_el, opts) {top_padding: opts[:size], bottom_padding: opts[:size]} end
# File lib/kramdown/converter/pdf.rb, line 412 def img_options(_el, _opts) {} end
# File lib/kramdown/converter/pdf.rb, line 225 def li_options(_el, _opts) {} end
# File lib/kramdown/converter/pdf.rb, line 257 def math_options(_el, _opts) {} end
# File lib/kramdown/converter/pdf.rb, line 212 def ol_options(_el, opts) {bottom_padding: opts[:size]} end
# 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
# File lib/kramdown/converter/pdf.rb, line 408 def render_abbreviation(el, opts) text_hash(el.value, opts) end
# File lib/kramdown/converter/pdf.rb, line 195 def render_blockquote(el, opts) @pdf.indent(mm2pt(10), mm2pt(10)) { inner(el, opts) } end
# File lib/kramdown/converter/pdf.rb, line 367 def render_br(_el, opts) text_hash("\n", opts, false) end
# 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
# File lib/kramdown/converter/pdf.rb, line 359 def render_codespan(el, opts) text_hash(el.value, opts) end
# File lib/kramdown/converter/pdf.rb, line 253 def render_dd(el, opts) @pdf.indent(mm2pt(10)) { inner(el, opts) } end
# File lib/kramdown/converter/pdf.rb, line 237 def render_dl(el, opts) inner(el, opts) end
# File lib/kramdown/converter/pdf.rb, line 245 def render_dt(el, opts) render_padded_and_formatted_text(el, opts) end
# File lib/kramdown/converter/pdf.rb, line 349 def render_em(el, opts) inner(el, opts) end
# File lib/kramdown/converter/pdf.rb, line 400 def render_entity(el, opts) text_hash(el.value.char, opts) end
# File lib/kramdown/converter/pdf.rb, line 124 def render_header(el, opts) render_padded_and_formatted_text(el, opts) end
# 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
# File lib/kramdown/converter/pdf.rb, line 229 def render_li(el, opts) inner(el, opts) end
# 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
# 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
# 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
# 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
# File lib/kramdown/converter/pdf.rb, line 375 def render_smart_quote(el, opts) text_hash(smart_quote_entity(el).char, opts) end
# 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
# 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
# File lib/kramdown/converter/pdf.rb, line 323 def render_text(el, opts) text_hash(el.value.to_s, opts) end
# 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
# 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
# File lib/kramdown/converter/pdf.rb, line 104 def root_options(_root, _opts) {font: 'Times-Roman', size: 12, leading: 2} end
# File lib/kramdown/converter/pdf.rb, line 371 def smart_quote_options(_el, _opts) {} end
# File lib/kramdown/converter/pdf.rb, line 335 def strong_options(_el, opts) {styles: (opts[:styles] || []) + [:bold]} end
# File lib/kramdown/converter/pdf.rb, line 289 def table_options(_el, opts) {bottom_padding: opts[:size]} end
# File lib/kramdown/converter/pdf.rb, line 319 def text_options(_el, _opts) {} end
# File lib/kramdown/converter/pdf.rb, line 379 def typographic_sym_options(_el, _opts) {} end
# File lib/kramdown/converter/pdf.rb, line 199 def ul_options(_el, opts) {bottom_padding: opts[:size]} end
Helper methods
↑ topProtected Instance Methods
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 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.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.key?(:top_padding) yield @pdf.move_down(opts[:bottom_padding]) if opts.key?(:bottom_padding) end
Organizational methods
↑ topProtected Instance Methods
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
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
Used in render_root
.
# File lib/kramdown/converter/pdf.rb, line 542 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 534 def setup_document(root) doc = Prawn::Document.new(document_options(root)) doc.extend(PrawnDocumentExtension) doc.converter = self doc end