Gamba is a Ruby library that writes PDF files: invoices, reports, labels, anything you can lay out as text, tables, images and shapes. It is a faster rewrite of Prawn, the most widely used PDF library for Ruby, together with its two companion gems, prawn-table and prawn-svg.
Code written for Prawn 2.5 runs on Gamba unchanged and draws the same pages, down to the pixel. The difference is speed and size: a one-page invoice renders about five times faster and the file is a quarter lighter (see Performance).
Website and guide: https://levups.github.io/gamba/
Gamba needs Ruby 3.3 or later. In your Gemfile, replace prawn, prawn-table and prawn-svg with one line:
gem "gamba"Gamba refuses to load if the prawn gem is in the same bundle, so remove it. Your existing require "prawn",
require "prawn/table" and require "prawn-svg" lines keep working, and pdf.table and pdf.svg are available
without them.
require "prawn"
Prawn::Document.generate("hello.pdf") do
text "Hello, World!"
table [%w[SKU Qty], %w[A-1 2]]
endThis writes hello.pdf with a line of text and a two-row table. Everything else Prawn documents works the same
way: fonts, colors, images, columns, page numbers, links, encryption. Prawn's manual applies as it is, and
the guide covers what Gamba adds.
The constant is still Prawn, so you don't have to rename anything. Prawn::VERSION reports 2.5.0, the
version of the API Gamba reproduces, for code that checks it. Gamba's own version is Gamba::VERSION.
Anything that would change how a page looks compared with Prawn is off until you ask for it.
Ligatures and kerning from OpenType fonts, per font:
font "Inter.ttf", features: %i[liga kern]PDF/A-3b archival files, and Factur-X electronic invoices (the French and German standard that embeds the invoice data as XML inside the PDF). Gamba embeds the XML your invoicing system produced; it does not build it.
Prawn::Document.new(pdf_a: true, info: {Title: "Invoice INV-2026-0001"}) do
font "Inter.ttf" # PDF/A needs embedded fonts: the 14 standard ones raise, and so does encryption
# ...
factur_x File.read("factur-x.xml"), profile: :en16931 # or minimum, basic_wl, basic, extended
endMerging existing PDFs, such as carrier labels or supplier invoices, one page after another. This replaces the combine_pdf gem for that use:
Gamba.combine([label, other_label], page_size: [288, 432], orientation: :portrait)Loading font and SVG support once, before a server such as Puma or Unicorn forks its workers, so they share it in memory:
Gamba.eager_load!Gamba.combine takes the bytes of each PDF and returns the bytes of the merged one. It reads the cross-reference
formats PDF writers use, including incremental updates, and files encrypted with an owner password only (RC4,
AES-128 and AES-256). A file that needs a password to open raises Prawn::Errors::EncryptedPDF, and a damaged one
it cannot repair raises Prawn::Errors::MalformedPDF.
Pages are copied as they were encoded, and resources several files share, like a carrier's fonts and logo, are
written once. With page_size, each page is scaled from its lower left corner to fit, keeping its proportions.
With orientation, a page the other way round turns a quarter counterclockwise, as combine_pdf does. A page's
/Rotate is applied to its content, where combine_pdf left it to the viewer. Outlines, forms and tagged structure
are not carried over.
Applications often put user content into PDFs: an uploaded logo, a font, a name typed in a form, a PDF to attach. Any of it can be built to attack the server that renders it.
- SVG images fetch nothing from the web unless you pass
enable_web_requests: true. prawn-svg 0.40 fetched by default, so a submitted SVG could make your server request any URL. prawn-svg 1.0 plans the same default as Gamba. - An SVG reads only what you allow. Base64
data:URLs of PNG, JPEG and SVG images always load;httpandhttpsURLs needenable_web_requests: true; files needenable_file_requests_with_root, and only regular files inside that directory load, symbolic links in it followed. Any other URL, path or scheme is skipped with a warning, and an entity the SVG declares from a file or URL is never read. - An SVG can't make Gamba loop or run out of stack through references (
<use>,<tref>, gradients) or deep nesting. An SVG it loads with<use>or<image>is checked the same way, SVG images stop 16 levels inside one another, and they draw within the budget of the SVG that embeds them. - Fonts an SVG declares with
@font-facedisappear once it is drawn, and one that is not a font is left out. In prawn-svg, an uploaded SVG declaring a "Helvetica" font changed the font of the application's own text. - A damaged or malicious PNG, JPEG, TrueType or OpenType file either renders or raises an error Gamba names
(
Prawn::Errors::UnsupportedImageType,Prawn::Images::JPG::FormatError,Gamba::TrueType::Error, orPrawn::Errors::CannotFitwhen a damaged font makes a glyph wider than the page). A PNG inflates no further than the size its header gives. CI corrupts sample files on every run to check this. Gamba.combinestops at 64 levels of nested arrays and dictionaries and at 256 MB of decompressed data, and ignores references that lead back to themselves, so a crafted PDF can't send it into a loop or fill memory.- Text with inline formatting (
inline_format: true) is parsed in linear time, whatever the markup. - The
/IDof an encrypted PDF depends on its passwords, so it reveals nothing about the content to someone without them.
Inline formatting still turns <link href="..."> into a clickable link. Escape user text before you put it inside
markup. A word with no space to break at, thousands of characters long, takes time that grows with the square of its
length, as in Prawn: limit the length of what users type.
To report a vulnerability, see SECURITY.md.
Before switching, you can compare what your app produces under both libraries. script/capture_pdfs.rb saves
every PDF your test suite renders, named after the test. Run the suite once with Prawn and once with Gamba:
GAMBA_CAPTURE_DIRECTORY=tmp/prawn RUBYOPT=-r/path/to/gamba/script/capture_pdfs.rb bin/rails testThen compare the two directories with Oracle.pdf_differences, from Gamba's test support.
bundle exec rake bench renders each document in bench/documents with Prawn 2.5 and with Gamba, in 7 fresh
YJIT processes each, taking turns. Each process times 20 renders, and the table shows the median of the 7, on
Ruby 4.0.9:
| Document | Prawn | Gamba | Allocations, Prawn → Gamba | Weight, Prawn → Gamba |
|---|---|---|---|---|
| Invoice, one page | 22.1 ms | 4.5 ms | 163,881 → 31,366 | 38,766 → 28,992 bytes |
| Report, 101 pages | 559.4 ms | 216.0 ms | 3,350,853 → 2,089,008 | 207,815 → 164,924 bytes |
| Catalog, 1,000 table rows | 544.9 ms | 265.6 ms | 3,979,977 → 2,078,343 | 147,731 → 134,082 bytes |
Files are lighter because Gamba compresses streams and packs objects together, which PDF 1.5 allows and Prawn doesn't do. Charts and peak memory are on the benchmarks page.
The test suite renders every example of Prawn's and prawn-table's manuals, and every sample SVG of prawn-svg, once
with the original gems and once with Gamba. For each pair it compares the text and its position to 0.01 pt, the
pixels at 150 dpi in two unrelated renderers (MuPDF and Poppler), whether qpdf --check accepts the file, and its
size. We call this the oracle.
Gamba does not copy Prawn's bugs. When Prawn crashes, writes an invalid PDF or contradicts its own documentation, Gamba fixes it, and the compatibility page lists each fix with its proof.
Prawn runs from its own bundle, oracle/Gemfile, so the oracle can compare the two. The tests need mutool,
pdftoppm and qpdf (brew install mupdf-tools poppler qpdf), and Prawn's manual and sample data, which
rake corpus:fetch downloads into corpus/cache. Without that cache, the comparison tests are skipped.
bundle install && BUNDLE_GEMFILE=oracle/Gemfile bundle install
bundle exec rake corpus:fetch
bundle exec rake test
bundle exec rubocopOther commands:
ORACLE_CANDIDATE=prawn bundle exec rake test # Prawn against itself, to check the oracle
FUZZ_SEED=42 FUZZ_CASES=500 bundle exec ruby -Itest test/fuzz_test.rb # random documents, compared with Prawn
INPUT_FUZZ_SEED=42 INPUT_FUZZ_CASES=400 bundle exec ruby -Itest test/input_fuzz_test.rb # corrupted images and fonts
bundle exec rake bench # writes docs/benchmarks.jsonruby -Itest script/fuzz_minimize.rb tmp/fuzz/documents/fuzz_0042.rb shrinks a failing fuzz document. PDF/A
files are also checked by veraPDF when it is installed (brew install verapdf).
The files that define what "compatible" means:
test/oracle_passing.txt: examples that must stay identical to Prawn's. One that becomes identical joins it.test/divergences.yml: Prawn bugs Gamba fixes, with the specification that proves each one and the exact differences the oracle must find.test/oracle_examples: documents of our own, for cases the manuals don't show.test/contract/prawn-2.5.0.yml: Prawn's public methods, generated byscript/contract_extract.rb. Every call Prawn accepts must keep working; exceptions are settled intest/contract/decisions.yml.
See CONTRIBUTING.md before opening a pull request.
Pull requests leave CHANGELOG.md alone. A release starts by writing its notes under ## [Unreleased], from the
pull requests merged since the last tag, without committing them. Then, from an up-to-date main with green CI:
bin/release minor --dry-run # shows the notes and the changes, writes nothing
bin/release minor # or patch, major, or an exact version such as 1.2.0main takes no direct push, so bin/release dates the notes and bumps the version on a release/vX.Y.Z branch,
opens a pull request that merges itself once CI is green, and waits for it. It then pushes a signed vX.Y.Z tag on
the merge commit (bin/release --tag X.Y.Z does it if you stopped waiting). The tag starts the Release workflow,
which publishes the gem to RubyGems through trusted publishing and creates the GitHub release with the notes and
the .gem.
By Level UP Solutions. Gamba is derived from Prawn, PDF::Core, TTFunk and prawn-table, by Gregory Brown and their
contributors, and is licensed like them: under Matz's original terms for Ruby, or the GPLv2, or the GPLv3. Its SVG
support comes from prawn-svg, by Roger Nesbitt, under the MIT license. See LICENSE.