Migrating from Prawn / An imperative drawing API
Migrating off Prawn, from bounding boxes to normal flow
A Ruby drawing library organised around bounding boxes and a text cursor, with a real table extension, where the box structure is the closest thing to a layout you have.
What ports and what does not
The data layer. The bounding boxes do not port, but they are the most useful thing in the old code when writing the markup, because a bounding_box call states a position and a width explicitly and that is a container with a width. Prawn's table extension is genuinely tabular, so tables convert more directly here than from any other drawing API: the column widths, the header repeat and the cell styling all have counterparts.
What the call becomes
Prawn::Document.generate or a new document plus render becomes a POST to /v1/pdf with html. The page_size and page_layout options become options.format and options.landscape, and the margin option becomes options.margin. repeat(:all), which Prawn used to draw content on every page, becomes options.header_html and options.footer_html or a CSS margin box. number_pages becomes the substituted class names in the footer.
Side by side
The old shape and the new one, with the parts that have no counterpart called out in comments rather than quietly omitted.
# Before
Prawn::Document.generate("invoice.pdf", page_size: "A4",
margin: [50, 40, 50, 40]) do |pdf|
pdf.repeat(:all) do
pdf.bounding_box([0, pdf.bounds.top + 20], width: pdf.bounds.width) do
pdf.text "Invoice", size: 12, style: :bold
end
end
pdf.table(rows, header: true, column_widths: [220, 80, 80])
pdf.number_pages "<page> of <total>", at: [0, 0], align: :center, size: 8
end
# After
html = ApplicationController.render(
template: "invoices/show", layout: "pdf", assigns: { rows: rows }
)
conn.post("https://api.pdfpipe.xyz/v1/pdf") do |req|
req.headers["Authorization"] = "Bearer #{ENV.fetch('PDFPIPE_KEY')}"
req.headers["Content-Type"] = "application/json"
req.body = {
html: html,
options: {
format: "A4",
margin: { top: "50pt", bottom: "50pt", left: "40pt", right: "40pt" },
header_html: '<div style="font:bold 12pt sans-serif">Invoice</div>',
footer_html: '<div style="font-size:8pt;text-align:center">' \
'<span class="pageNumber"></span> of ' \
'<span class="totalPages"></span></div>',
},
}.to_json
end
# column_widths are in points and points are valid CSS, so they
# transcribe. header: true becomes a thead, which repeats on every page.
# The margin array is [top, right, bottom, left]: check the order.The CSS delta, in both directions
What the old engine accepted that a current one does not, and what it refused that a current one wants. The second direction is the one people forget, and it is where the value of the migration is.
- Nothing to migrate. bounding_box calls give you container positions and widths in points, which transcribe directly into CSS.
- Prawn's table extension maps unusually well: column_widths become column widths, header: true becomes a thead that repeats, and cell styling becomes selectors.
- The text cursor and move_down become normal flow and margins.
- repeat(:all) becomes the header and footer options, which is a narrowing: repeat could draw anything anywhere on every page, and the header and footer are two fixed regions.
What changes without anything erroring
The dangerous list. Each of these produces a different document and no diagnostic, so none of them are caught by a test that only checks the render succeeded.
- Everything visual, because it is a rewrite.
- Page count.
- Anything repeat(:all) drew outside the top and bottom of the page, which has no counterpart at all.
- Fonts, from font_families registration to @font-face.
What to diff before cutting over
Diff extracted text, then check the margin array order, which Prawn takes as top, right, bottom, left and which is easy to transcribe wrongly into named sides.
The one thing that always breaks
repeat(:all) blocks that drew something other than a header or footer. A watermark, a side rule, a confidentiality mark down the edge: all of them were one construct in Prawn and there is no single construct for them here. Each becomes a different CSS technique, and the ones that were not at the top or bottom of the page are the ones nobody notices are missing.
Frequently asked
Do I have to rewrite my templates to leave Prawn?
Effectively yes, and it is better to plan for that than to discover it. The old tool did not have HTML templates to port, so the document has to be expressed as markup for the first time. The value in the old code is the data layer underneath the drawing, and that part survives untouched.
Can I run both for a while?
Yes, and it is the safest way to do it. Put both behind one internal function that takes your data and returns bytes, switch on an environment variable, and run the new path on real traffic while the old one still serves. You get a diff on real documents rather than on fixtures, and you keep a way back that does not involve a deploy.
What about the documents already generated?
Nothing here changes them. They are files that already exist. What is worth deciding before the cutover is whether a regenerated document has to match the original byte for byte or merely say the same thing, because for anything with a legal or audit character the answer is usually to keep the original file rather than to be able to reproduce it.
Other migrations
The nearest neighbours first, then others that started from the same kind of tool, because the model matters more than the language.
Migrating off ReportLab, where there are no templates to port
A Python library with a low-level canvas and a higher-level flowable system, where nothing in the layout is markup and the document has to be written as HTML for the first time.
Migrating off FPDF, from Cell coordinates to markup
A minimal drawing library built around Cell and MultiCell calls, where the layout is arithmetic and the arithmetic is the only specification of the document that exists.
Migrating off PDFKit for Node, from a stream of calls to markup
A Node drawing library with a chainable API and a streaming output model, where the document is a sequence of calls and the stream is the part that changes shape.
Migrating off jsPDF, from text at coordinates to a document
A JavaScript drawing library whose documents are sequences of text and shape calls at coordinates, frequently run in the browser, so the migration moves the render to a server as well as changing its shape.
Migrating off iText, and the licensing question underneath it
A mature PDF library with a document model and a low-level API, where the technical migration is ordinary and the reason for it is frequently the licence rather than the code.
Every migration, grouped by what you are coming from
The full list, sorted by the model the old tool used rather than by its name.
What this API actually does
One page per option and endpoint that exists, which is what the mappings above point at.
Render one of your existing documents through the playground before changing any code. That comparison is the whole of the risk assessment for this migration.