Migrating from Prawn

This how-to guide provides information and code samples for Prawn users to get familiar with HexaPDF. While Prawn and HexaPDF are different in their respective capabilities, HexaPDF can do many of the things that Prawn can do and can often be used instead of Prawn.

Throughout this guide the following variables are consistently used:

doc
The Prawn document instance.
document
The HexaPDF document instance.
composer
The HexaPDF composer instance.
canvas
The HexaPDF canvas instance of a page.

Code Comparison Example

The following example shows the same document, a very simple invoice, being created in Prawn and in HexaPDF. It doesn’t use all available functionality in both libraries but shows general usage. A short comparison of the two code samples can be found after them.

Here is the Prawn example, generating this result:

require 'prawn'
require 'prawn/table'

doc = Prawn::Document.new(page_size: "A4", margin: [72, 72, 72, 72], compress: true)

doc.font("Helvetica")
doc.font_size(12)

doc.save_graphics_state do
  doc.canvas do
    doc.fill_color("77C3EC")
    doc.fill_rectangle([0, 50], doc.bounds.right, 50)
    doc.fill_rectangle([0, doc.bounds.top], doc.bounds.right, 50)
    doc.fill_color("000000")
  end
end

doc.float do
  doc.bounding_box([doc.bounds.right - 150, doc.cursor], width: 150, height: 100) do
    doc.stroke_bounds
    doc.text_box("Prawn Example Inc.\nGarnish Street 3a\n4567 New South East\nWorld",
                 at: [5, doc.bounds.top - 5], width: 140, height: 90)
  end
end

doc.bounding_box([0, doc.cursor], width: 150, height: 100) do
  doc.stroke_bounds
  doc.text_box("Customer Here\nAvailability Arcarde 1\n 8901 Old North West\nMoon",
               at: [5, doc.bounds.top - 5], width: 140, height: 90)
end

doc.move_down(40)

doc.font_size(24) do
  doc.text("Invoice 1234")
end

doc.move_down(40)

invoice_data = [
  ["Item", "Amount", "Total Price"],
]
1.upto(10) do |i|
  invoice_data << ["Super Dooper #{i}", i, "$ #{10*i}"]
end
invoice_data << ["", "", "$ 450"]

doc.table(invoice_data, width: doc.bounds.width,
          cell_style: {padding: 5, height: 25}, column_widths: [250, 80]) do |table|
  table.row(0).font_style = :bold
  table.row(0).background_color = "EEEEEE"
  table.row(-1).font_style = :bold
  table.row(-1).background_color = "EEEEEE"
  table.column(-2..-1).align = :right
end

doc.move_down(40)

doc.formatted_text(
  [{text: "Please transfer the money to the following bank account:\n"},
   {text: "IBAN: ", styles: [:bold]},
   {text: "AT65 1234 1234 5678 9012 3456, "},
   {text: "BIC: ", styles: [:bold]},
   {text: "ABCDAT12345\n"},
   {text: "Thank you for choosing us!", styles: [:italic], size: 8}],
  align: :center
)

doc.render_file("invoice-prawn.pdf")

And here is the HexaPDF code with its result:

require 'hexapdf'

composer = HexaPDF::Composer.new(skip_page_creation: true)
composer.page_style(:default, page_size: :A4) do |canvas, style|
  box = canvas.context.box(:media)
  canvas.save_graphics_state do
    canvas.fill_color("77C3EC").
      rectangle(0, 0, box.width, 50).
      rectangle(0, box.height - 50, box.width, 50).
      fill
  end
  style.frame = style.create_frame(canvas.context, 72)
end
composer.style(:base, font: "Helvetica", font_size: 12, line_spacing: 1.2)

composer.new_page
composer.text("HexaPDF Example Inc.\nGarnish Street 3a\n4567 New South East\nWorld",
              width: 150, height: 100, padding: 5, border: {width: 1},
              position: :float, align: :right)

composer.text("Customer Here\nAvailability Arcarde 1\n 8901 Old North West\nMoon",
              width: 150, height: 100, padding: 5, border: {width: 1})

composer.text("Invoice 1234", font_size: 24, margin: [40, 0])

invoice_data = [
  ["Item", "Amount", "Total Price"],
]
1.upto(10) do |i|
  invoice_data << ["Super Dooper #{i}", i, "$ #{10*i}"]
end
invoice_data << ["", "", "$ 450"]

composer.table(invoice_data, column_widths: [250, 80], margin: [0, 0, 40]) do |args|
  args[0, 0..-1] = {font: "Helvetica bold", cell: {background_color: "EEE"}}
  args[-1, 0..-1] = {font: "Helvetica bold", cell: {background_color: "EEE"}}
  args[0..-1, 1..-1] = {text_align: :right}
end

composer.formatted_text(
  ["Please transfer the money to the following bank account:\n",
   {text: "IBAN: ", font: "Helvetica bold"},
   "AT65 1234 1234 5678 9012 3456, ",
   {text: "BIC: ", font: "Helvetica bold"},
   "ABCDAT12345\n",
   {text: "Thank you for choosing us!", font: "Helvetica bold", font_size: 8}],
  text_align: :center
)

composer.write("invoice-hexapdf.pdf", optimize: true)

Short comparison:

Creating a Document

Document creation in Prawn and HexaPDF is very similar. The usual flow is to create a document instance at the beginning and to write the result at the end:

# Prawn
doc = Prawn::Document.new
doc.text("Hello World")    # Do something
doc.render_file("hello-prawn.pdf")

# HexaPDF
document = HexaPDF::Document.new
document.pages.add.canvas.
  font("Helvetica", size: 10).
  text("Hello World", at: [100, 500])    # Do something
document.write("hello-hexapdf.pdf")

# HexaPDF Composer
composer = HexaPDF::Composer.new
composer.text("Hello World")    # Do something
composer.write("hello-composer.pdf")

With HexaPDF there are two possible ways when creating a document:

Additionally, there is also the possibility to use a block form when creating a document:

# Prawn
Prawn::Document.generate("hello-prawn.pdf") do |doc|
  # Do something with the document
end

# HexaPDF Composer
HexaPDF::Composer.create("hello-composer.pdf") do |composer|
  # Do something with the composer
end

Working with Graphics and Text

When working with Prawn one is really mostly working with the Prawn::Document instance which is the catch-all object doing everything. It also provides the graphics methods that directly map to PDF operators as well as some convenience methods for more complex tasks, like drawing circles.

When these methods are invoked, they are applied to the content stream of the current page. Once the page is changed to a new page, new invocations apply to the new page.

Another thing to take into account is the default use of a document bounding box in Prawn that influences the position of these operations. To explicitly disable this bounding box one needs to use the Prawn::Document#canvas method.

HexaPDF has an explicit canvas class that is associated with a page. Any operation on such a canvas instance will only ever apply to that single page. And there is no bounding box whatsoever; so all coordinates are relative to the page’s origin at the bottom left. The canvas methods are intentionally low-level as this class should just be a thin layer of convenience methods above the respective PDF operators.

The HexaPDF composer class provides high-level methods for working with text, images, … and is more in line with what one would expect coming from Prawn. The current canvas object can be accessed via HexaPDF::Composer#canvas and through this all graphical operations are available.

The following lists show the HexaPDF equivalents of common operations:

Basic path construction methods directly supported by PDF

doc.move_to, doc.line_to, doc.curve_to, doc.rectangle

canvas.move_to, canvas.line_to, canvas.curve_to, canvas.rectangle canvas.close_subpath canvas.end_path

These methods are basically the same in Prawn and HexaPDF but have slightly different interfaces. E.g. doc.curve_to uses a :bounds argument where as canvas.curve_to allows specifying either of the two bezier points :p1 and/or :p2, for a complete mapping to PDF operators.

Additional path construction methods

doc.line, doc.vertical_line, doc.horizontal_line, doc.curve, doc.rounded_rectangle, doc.polygon, doc.rounded_polygon, doc.circle, doc.ellipse

canvas.line, canvas.polyline, canvas.polygon, canvas.circle, canvas.ellipse, canvas.arc canvas.graphic_object canvas.draw

HexaPDF also supports rounded variants of rectangles and polygons, just provide the radius argument to the methods.

The #arc method works similar to #curve_to but is actually implemented in a separate class as a so called graphic object. These graphic objects provide an easy way to extend the available shapes. Built-in are, for example, implementations for arcs in center and endpoint parameterizations as well as an implementation of solid arcs.

Path painting methods

doc.fill, doc.stroke, doc.fill_and_stroke, doc.close_and_stroke

canvas.fill, canvas.stroke, canvas.fill_stroke, canvas.close_stroke, canvas.close_fill_stroke, canvas.clip_path

The methods practically work the same in Prawn and HexaPDF. Note that Prawn doesn’t have an explicit method for defining a clipping path.

In addition to these methods that directly map to a PDF operator, Prawn also defines helper methods for applying filling or stroking operations to a single shape, like a rectangle. Since HexaPDF doesn’t define such methods, one needs to invoke the appropriate path painting method after drawing the shape(s):

doc.fill_rectangle([100, 100], 200, 50)   # Prawn
canvas.rectangle(100, 100, 200, 50).fill  # HexaPDF
Path property methods

doc.line_width=, doc.cap_style=, doc.join_style=, doc.dash

canvas.line_width, canvas.line_cap_style, canvas.line_join_style, canvas.line_dash_pattern, canvas.miter_limit

Color methods

doc.fill_color, doc.stroke_color

canvas.fill_color, canvas.stroke_color

Prawn supports setting an RGB color using a hex color string and a CMYK color using four values.

HexaPDF supports those two methods as well as color strings of the form ‘RGB’ (in addition to of ‘RRGGBB’), three values for an RGB color, CSS Color Module Level 3 color names and one value for grayscale colors.

Canvas transformation methods

doc.translate, doc.rotate, doc.scale

canvas.transform, canvas.translate, canvas.scale, canvas.rotate, canvas.skew

Text drawing and positioning methods

doc.cursor, doc.move_cursor_to, doc.move_down, doc.move_up, doc.pad_top, doc.pad_bottom, doc.draw_text, doc.text, doc.text_box, doc.formatted_text, doc.formatted_text_box

canvas.begin_text, canvas.end_text, canvas.text, canvas.show_glyphs, canvas.show_glyphs_only, canvas.text_cursor, canvas.move_text_cursor, canvas.text_matrix

composer.x, composer.y, composer.text, composer.formatted_text

Prawn has the notion of a cursor which is the current vertical position on a page. Most operations will be done at the current cursor position, with the horizontal position being the left side of the current bounding box. HexaPDF’s canvas object has no notion of a cursor but the composer has something similar, exposed through the composer.x and composer.y methods. These coordinates indicate the position of the next box placement.

Most of the text drawing methods of Prawn support various options like :character_spacing to style the text itself. It is also possible to use HTML-like inline formatting tags. The only real low-level method for text output is doc.draw_text.

HexaPDF canvas’ methods are intentionally low-level to allow the full spectrum of PDF functionality. One would normally only use canvas.text or the high-level facilities provided by the composer and its associated classes.

Text property methods

doc.font, doc.font_size, doc.default_leading, doc.text_rendering_mode

canvas.font, canvas.font_size, canvas.character_spacing, canvas.horizontal_scaling, canvas.text_rise, canvas.word_spacing, canvas.leading, canvas.text_rendering_mode

While HexaPDF supports text properties on the canvas class using dedicated methods, Prawn mostly supports them through options passed to e.g. doc.text. This way kerning, character spacing, leading and text color can be specified.

When using the HexaPDF::Composer class and its box system, styling of text works through an explicit style object of class HexaPDF::Layout::Style. Such a style object can be applied to a whole text box (e.g. for text alignment, padding, margin, border, …) as well as to text fragments (e.g. for font, text color, character spacing, …).

Other methods

doc.save_graphics_state, doc.restore_graphics_state, doc.transparency, doc.image

canvas.save_graphics_state, canvas.restore_graphics_state, canvas.opacity, canvas.rendering_intent, canvas.image, canvas.xobject, canvas.marked_content_point, canvas.marked_content_sequence, canvas.end_marked_content_sequence

composer.image

Other Prawn Functionality

Tables (doc.table)

This functionality is not part of Prawn itself but part of the official prawn-table gem. Creating tables with Prawn is very easy and straightforward, with advanced functionality also available.

HexaPDF also has a table box implementation which is quite similar:

# Prawn
doc.table([['Cell 1', 'Cell 2'], ['Row 2 Cell 1', 'Row 2 Cell 2']])

# HexaPDF
composer.table([['Cell 1', 'Cell 2'], ['Row 2 Cell 1', 'Row 2 Cell 2']])

Both, Prawn and HexaPDF, support defining cell borders and background colors as well as column and row spans, can selectively apply styling to certain cell ranges, and split the table.

In addition, HexaPDF’s implementation allows any kind of content in a cell while Prawn is limited to text, images or sub-tables.

Column box (doc.column_box)

HexaPDF also has the notion of a column box:

# Prawn
doc.column_box([0, doc.cursor], columns: 2, width: doc.bounds.width) do
  doc.text("Some content here")
end

# HexaPDF
composer.column(columns: 2, gaps: 10) do |column|
  column.text("Some content here")
end

The column box works like any other box. It can use position: :flow for using the shape of the current frame instead of just a rectangular region. And it can make all column heights (roughly) equal if specified to do so.

Repeatable content (doc.repeat)

This can be done in HexaPDF by iterating over the pages, getting their canvas objects and drawing on them:

# Prawn
doc.repeat(:all) do
  doc.draw_text("All pages", at: [0, 0])
end

# HexaPDF
document.pages.each do |page|
  page.canvas.text("All pages", at: [0, 0])
end

Granted, this doesn’t look as nice but it allows for more flexibility. Want to put that repeated content into the background? Use the underlay canvas.

Stamps (doc.create_stamp, doc.stamp, doc.stamp_at)

These are just Form XObjects and those are directly supported by HexaPDF:

# Prawn
doc.create_stamp("pdf") do
  doc.draw_text("PDF software", at: [50, 0])
end
doc.stamp_at("pdf", [200, 100])

# HexaPDF
stamp = document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, 100, 50]})
stamp.canvas.text("PDF software", at: [0, 0])
canvas.xobject(stamp, at: [200, 100])

# HexaPDF Composer
stamp = composer.create_stamp(100, 50) do |canvas|
  canvas.text("PDF software", at: [0, 0])
end
composer.image(stamp)

The HexaPDF::Composer#create_stamp method allows creating such stamps but currently only provides a canvas to draw on (and not a composer-like interface).

Document encryption

Encrypting a document in Prawn (doc.encrypt_document) is possible but should not be done. The reason for this is that it only allows for a very weak encryption scheme (40bit RC4).

In contrast, HexaPDF supports all standard encryption schemes, up to the latest one from PDF 2.0 (AES 256bit):

# Prawn
doc.encrypt_document(user_password: 'foo')

# HexaPDF
document.encrypt(user_password: 'foo')
Document outline

Prawn supports defining a document outline (a.k.a. bookmarks) for a document.

This can also be done by HexaPDF. Additionally, it supports setting the text color for an outline item as well as whether the text should appear in bold and/or italic, and actions instead of destinations are also supported.

# Prawn
5.times { doc.start_new_page }
doc.outline.define do
  section("Section 1", destination: 1) do
    page(title: "Page 2", destination: 2)
    page(title: "Page 3", destination: 3)
    section("Section 1.1") do
      page(title: "Page 3", destination: 4)
    end
  end
end

# HexaPDF
5.times { document.pages.add }
document.outline.add_item("Section 1", destination: 0) do |sec1|
  sec1.add_item("Page 2", destination: document.pages[1])
  sec1.add_item("Page 3", destination: 2)
  sec1.add_item("Section 1.1", text_color: "red", flags: [:bold]) do |sec11|
    sec11.add_item("Page 4", destination: 3)
  end
end

Prawn functionality not yet supported in HexaPDF

There are some things that are not yet supported in HexaPDF via convenience methods:

HexaPDF functionality not supported in Prawn

Since HexaPDF is a full-blown PDF library, it can do many more things than just creating a document, for example:

Advanced boxes

In addition to the column box HexaPDF also supports bullet and numbered lists.

Existing PDF file as template

Granted, there is prawn-template but it is very restricted in which files it can load/work with. Since HexaPDF is a fully-featured PDF library it can load any PDF file (even most damaged ones) as template and add content.

Advanced file compression

HexaPDF is built to create small files by default. The additional optimize: true option when writing a document activates some more features to achieve even better results.

Compared to Prawn the PDF files created by HexaPDF are about 15% to 25% smaller.

Interactive forms

HexaPDF integrates functionality for the creation and pre-rendering of interactive forms a.k.a. AcroForms. This functionality is not yet integrated into the composer but can manually be combined with it.

See the topic “Interactive Forms” and the interactive form example for what’s possible.

Digital signatures

HexaPDF supports adding one or more digital signatures to a document. Such signed documents will be visually flagged in supported PDF readers.

See the topic “Digital Signatures” and HexaPDF::DigitalSignature::Signatures#add for more information on this.