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
OptionWhat it does
at, position, vpositionWhere the image goes, as in Prawn’s image
width, heightIts size; the other dimension follows the aspect ratio
enable_web_requestsLets 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_rootLets the SVG read local files under this directory
cache_imagesKeeps fetched images between calls
fallback_font_nameThe font used when the SVG names one the document does not know
color_mode:rgb or :cmyk
languageThe language <switch> elements match against
log_warningsPrints 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.

ArgumentValues
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