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(page_size: :A4, margin: 72)

composer.style(:base, font: "Helvetica", font_size: 12, line_spacing: 1.2)

box = composer.page.box(:media)
composer.canvas.
  fill_color("77C3EC").
  rectangle(0, 0, box.width, 50).
  rectangle(0, box.height - 50, box.width, 50).
  fill

composer.text("HexaPDF Example Inc.\nGarnish Street 3a\n4567 New South East\nWorld",
              width: 150, height: 100, padding: 5, border: {width: 1},
              position: :float, position_hint: :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"]

invoice_data.each_with_index do |row, rindex|
  row.each_with_index do |content, cindex|
    style = {height: 25, padding: 5, border: {width: 1}, margin: [0, -1, -1, 0], valign: :center}
    if rindex == 0 || rindex == invoice_data.length - 1
      style.update({font: ["Helvetica", variant: :bold], background_color: "EEE"})
    end
    case cindex
    when 0 then style.update(width: 250, position: :float)
    when 1 then style.update(width: 80, position: :float, align: :right)
    when 2 then style.update(align: :right)
    end
    composer.text(content.to_s, **style, )
  end
end

composer.draw_box(HexaPDF::Layout::Box.new(height: 40))

composer.formatted_text(
  ["Please transfer the money to the following bank account:\n",
   {text: "IBAN: ", font: ["Helvetica", variant: :bold]},
   "AT65 1234 1234 5678 9012 3456, ",
   {text: "BIC: ", font: ["Helvetica", variant: :bold]},
   "ABCDAT12345\n",
   {text: "Thank you for choosing us!", font: ["Helvetica", variant: :italic], font_size: 8}],
  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 some high-level methods for working with text and 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

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)

There will be a HexaPDF::Composer convenience method for creating such Form XObjects to make it easier.

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')

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:

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 key 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 key topic “Digital Signatures” and HexaPDF::Document::Signatures#add for more information on this.