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:
-
The use of the HexaPDF::Composer#text method makes creating the text boxes with styling for the sender and recipient rather easy. Prawn’s bounding boxes are more versatile but also more verbose when doing this task.
-
Use of the
#formatted_text
methods in Prawn and HexaPDF is quite similar which is no coincidence since the idea was taken from Prawn. What HexaPDF doesn’t allow but Prawn does is using HTML-like inline formatting. -
Both examples use the default method for optimizing the size of the output file. The PDF file created by HexaPDF is about 11% smaller than the one from Prawn.
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:
-
The first one shown creates a new document instance and is intended for small scale creation task. The reason for this is that it doesn’t really provide convenient document creation facilities out of the box. Have a look at the Canvas Tutorial to get started.
-
The second one uses the HexaPDF::Composer class which is similar to Prawn’s document class in that it provides convenience methods for creating the contents of a document. Since one can access the document instance, it is also possible to do low-level stuff when needed. This is what one would usually use. Have a look at the Composer Tutorial to get started.
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 ascanvas.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
andcomposer.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 isdoc.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
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:
-
Bounding boxes (
doc.bounding_box
,doc.span
,doc.indent
). This functionality will most probably not be incorporated into HexaPDF in this way due to a different approach in document layouting using the HexaPDF::Composer class and the box system. -
Gradients (
doc.fill_gradient
,doc.stroke_gradient
) -
Soft masks (
doc.soft_mask
) -
Grids (
doc.define_grid
,doc.grid
)
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.