Guide
PrawnX answers to Prawn’s API: installing it is swapping gems. This page covers what it brings on top.
Installation
# Gemfile
gem "prawnx" # instead of prawn, prawn-table and prawn-svg
PrawnX needs Ruby 3.3 or later. It depends on css_parser, matrix and rexml, which only its SVG support loads.
Remove prawn, prawn-table and prawn-svg from the Gemfile: PrawnX refuses to load next to the prawn gem, with a LoadError saying so, rather than mixing two engines in one process.
Drop-in usage
Code written for Prawn runs as it is. PrawnX provides require "prawn", require "prawn/table", require "prawn-svg" and require "prawn/measurement_extensions", and the Prawn namespace: Prawn::Document, Prawn::Table, Prawn::Errors, Prawn::SVG.
require "prawn"
Prawn::Document.generate("invoice.pdf", page_size: "A4", info: {Title: "Invoice"}) do
font "DejaVuSans.ttf"
text "Invoice INV-2026-0001", size: 14, style: :bold
table [%w[SKU Qty Price], %w[A-1 2 $10.00]], header: true
number_pages "<page>/<total>", at: [bounds.right - 50, 0], width: 50, align: :right
end
Prawn::VERSION answers "2.5.0", the version of the API PrawnX mirrors, because Prawn extensions check it. PrawnX’s own version is PrawnX::VERSION.
The contract is Prawn 2.5’s public methods, listed in test/contract/prawn-2.5.0.yml: PrawnX accepts every call Prawn accepts on those, and test/contract/decisions.yml records, method by method, what is in and why the rest is left out.
Tables
The API of prawn-table 0.2.2, built in: table, make_table, cell, make_cell, with their cell styles, headers repeated across pages, column widths, row and column spans, and subtables. No other gem and no require are needed.
table [%w[SKU Description Qty], *lines], header: true, width: bounds.width, cell_style: {padding: 3} do
row(0).font_style = :bold
column(2).align = :right
end
SVG
The API of prawn-svg 0.40.4, built in: svg data, options loads SVG support on first use.
svg File.read("logo.svg"), width: 120, position: :right
| Option | What it does |
|---|---|
at, position, vposition | Where the image goes, as in Prawn’s image |
width, height | Its size; the other dimension follows the aspect ratio |
enable_web_requests | Lets the SVG fetch images from http and https URLs. Off by default, where prawn-svg 0.40 has it on: see Compatibility |
enable_file_requests_with_root | Lets the SVG read local files under this directory |
cache_images | Keeps fetched images between calls |
fallback_font_name | The font used when the SVG names one the document does not know |
color_mode | :rgb or :cmyk |
language | The language <switch> elements match against |
log_warnings | Prints the warnings collected while reading the SVG |
Fonts
Prawn’s fonts, unchanged in use: the 14 standard PDF fonts, TrueType files (.ttf), collections (.ttc) and .dfont, font_families.update, and fallback_fonts. OpenType fonts with CFF outlines (.otf) embed like TrueType ones, and render like the font itself.
A parsed font is shared by every document of the process, and every thread, until its file changes: a server reads Inter.ttf once, not once per PDF.
OpenType features
Ligatures and GPOS pair kerning move glyphs away from where Prawn puts them, so they stay off unless asked for, per font:
font "Inter.ttf", features: %i[liga kern]
font_families.update("Inter" => {normal: {file: "Inter.ttf", features: %i[liga kern]}})
liga applies the font’s standard ligatures (GSUB); kern applies its GPOS pair kerning.
Images
JPEG images are embedded as they are; PNG images in every variant Prawn reads, alpha included, are decoded. The same image placed twice in a document is embedded once.
A corrupt or hostile file raises Prawn::Errors::UnsupportedImageType or a named font error, never a crash inside a parser: PNG data inflates no further than its header announces. An input fuzzer checks it on flipped bytes, truncations and forged lengths.
PDF/A and Factur-X
Prawn::Document.new(pdf_a: true, info: {Title: "Invoice INV-2026-0001"}) do
font "Inter.ttf"
# …
factur_x File.read("factur-x.xml"), profile: :en16931
end
pdf_a: true writes a PDF/A-3b document (ISO 19005-3): XMP metadata matching the info hash, an sRGB output intent, a file identifier, and annotations flagged for printing. What PDF/A forbids raises Prawn::Errors::PdfAViolation: a standard PDF font, which cannot be embedded, and encryption.
factur_x(xml, profile:, filename:, description:) embeds the CII XML of a Factur-X invoice into a PDF/A-3 document, with the relationship the specification gives each profile. PrawnX does not build that XML: it embeds the one the invoicing system produced.
| Argument | Values |
|---|---|
profile | :minimum, :basic_wl, :basic, :en16931 (default), :extended |
filename | "factur-x.xml" by default |
description | "Factur-X invoice" by default |
The test suite checks PDF/A documents with veraPDF when it is installed (brew install verapdf).
Output
Whatever does not move a pixel is on by default. Every document is written as PDF 1.5: streams compressed, every other object packed into one compressed object stream, and a compressed cross-reference stream instead of the xref table (ISO 32000-1 §7.5.7, §7.5.8).
Servers that fork
TrueType and SVG support load on first use, so a document of standard fonts never pays for them. Before a server forks its workers, load them once, so every worker shares that memory instead of loading its own copy:
# config/puma.rb, before_fork, or an initializer
PrawnX.eager_load!
Comparing an app’s PDFs
script/capture_pdfs.rb writes every PDF an app’s test suite renders, named after its test. Run the suite under Prawn, then under PrawnX, and compare both directories with Oracle.pdf_differences:
PRAWNX_CAPTURE_DIRECTORY=tmp/prawn RUBYOPT=-r/path/to/prawnx/script/capture_pdfs.rb bin/rails test