Compatibility

Gamba draws the pages Prawn 2.5, prawn-table 0.2.2 and prawn-svg 0.40.4 draw. Every exception is on this page, declared in the repository and checked by the oracle.

Prawn bugs Gamba fixes

A bug counts only when a specification or Prawn’s own documentation proves it; a mere preference becomes an option. test/divergences.yml lists each one, its proof, the examples that show it, and the exact differences the oracle must report: no more, no fewer.

Prawn’s behaviourProof
Encrypted documents have no /ID in their trailer, so qpdf and strict readers reject themISO 32000-1 §7.5.5, trailer dictionary, ID: “Required if an Encrypt entry is present”
Built-in fonts measure Ÿ (byte 159) as ÿ, so right-aligned or wrapped text holding it overflowsISO 32000-1 Annex D.2: WinAnsiEncoding maps byte 237 octal to Ydieresis, not ydieresis
The faces of one font collection share their width cache, so text in one face is measured with another’s widthsPrawn::Font#eql? compares the file name, family and size, which every face of a .ttc shares; Prawn documents each face as a font of its own
TrueType-flavoured subsetting of CFF fonts changes how their glyphs rasterizeThe subset Prawn embeds (through TTFunk) renders differently from the unmodified font program; Gamba keeps outlines and hints untouched, and renders like the font itself
A transparent block whose content crosses a page break draws the next page opaquePrawn documents transparent as setting the opacity of the content within the block, and ISO 32000-1 §8.4.1 starts each page with the default graphics state, opaque; Gamba sets the opacity of the blocks still open again on the new page, after its header and background
A blend_mode block whose content crosses a page break draws the next page in the normal blend modePrawn documents blend_mode as setting the blend mode, restored after the block, and ISO 32000-1 §8.4.1 starts each page with the default graphics state, blend mode Normal; Gamba sets the blend mode of the blocks still open again on the new page, like their opacity
Colours, line width, dash, cap and join styles set before a block whose content crosses a page break are lost once it ends: what follows draws black, with thin solid linesPrawn documents save_graphics_state as restoring the graphic state after the block, and ISO 32000-1 §8.4.2 has Q restore the state q saved; on the new page the block closes the page’s own state instead, so Gamba writes back the settings in effect before the block
A blend mode set without a block is lost on the next pagePrawn documents blend_mode without a block as setting the blend mode for what follows, and ISO 32000-1 §8.4.1 starts each page with blend mode Normal; Prawn writes the colours and line settings again on a new page, not the blend mode, which Gamba sets again after reopening the blocks still open
A gradient set as the fill or stroke is lost on the next page, which paints with the colour set before the gradientISO 32000-1 §8.6.6.2 makes the gradient the current colour until another replaces it; Prawn writes the current colours again on a new page but never records a gradient, so Gamba records it and paints with it there, while fill_color and stroke_color return what they return in Prawn
text_rendering_mode, character_spacing, word_spacing, horizontal_text_scaling and rise blocks whose text flows onto the next page draw that page without them, though Prawn lays it out with themISO 32000-1 §9.3.1 makes the text state part of the graphics state, which §8.4.1 resets on each page; Gamba writes the text state of the blocks still open on the new page, before its header, again after a block ending there closes the state holding it, and on a page reached through go_to_page, where page numbers and repeaters no longer take the text state of a block left open there
A text_rendering_mode block or a text box drawn after forget_text_rendering_mode! ends with a Tr missing its operand: Poppler drops the text drawn after itISO 32000-1 §9.3.6 gives Tr one operand, the rendering mode; Prawn writes back the forgotten, unknown mode without one, and Gamba writes nothing back, so the text after it draws in the mode the block left
Page numbers, repeaters and other content drawn on a page again, once a block that began there ended on another page, are drawn inside that block: faded, rotated or recoloured on that page onlyISO 32000-1 §8.4.2 has a state saved by q last until its Q, which such a block writes on another page; Gamba closes, on the page go_to_page reaches, the states of blocks that have ended, and keeps those of blocks still open
A transparent or blend_mode block whose content flows onto a page that exists already, reached through go_to_page, draws it there opaque, in the normal blend modePrawn documents both as applying to the content within the block, and ISO 32000-1 §8.4.1 starts each page with the default graphics state; Gamba reopens the blocks still open on that page, on top of what it drew already, as it does on a new page
Colours, line settings, a gradient or a blend mode set before text that flows onto a page that exists already, reached through go_to_page, are lost there: the text goes on in that page's own colours, black by defaultPrawn writes them again on a new page, never on one that exists; Gamba writes back there the settings in effect, as on a new page, while go_to_page called directly, for page numbers and repeaters, keeps the page's own, as in Prawn

SVG web requests

SVG images do not fetch anything from the web unless asked to: svg data, enable_web_requests: true. prawn-svg 0.40 fetches by default, which lets a submitted SVG make the server request any URL. prawn-svg 1.0 plans the same default as Gamba.

Lighter files

The pages are the same; the bytes are not. Gamba writes at least PDF 1.5, with compressed streams, one compressed object stream and a cross-reference stream, where Prawn writes plain objects and an xref table. The oracle checks that each Gamba file passes qpdf --check and weighs no more than Prawn’s. Tools that read PDF files byte for byte, rather than through a PDF parser, see a different file.

Off unless asked for

What would move pixels away from Prawn’s stays off unless asked for:

  • OpenType ligatures and GPOS pair kerning, per font: font "Inter.ttf", features: %i[liga kern];
  • PDF/A-3b and Factur-X: Prawn::Document.new(pdf_a: true), then factur_x.

OpenType features · PDF/A and Factur-X

Loading and versions

  • Gamba raises a LoadError when the prawn gem is in the bundle too: remove prawn, prawn-table and prawn-svg from the Gemfile.
  • Prawn::VERSION is "2.5.0", the API Gamba mirrors, since Prawn extensions check it; Gamba::VERSION is Gamba’s own.
  • The public methods of Prawn 2.5 are listed in test/contract/prawn-2.5.0.yml, generated by script/contract_extract.rb. Gamba accepts every call Prawn accepts on those of its contract; test/contract/decisions.yml settles each method outside the Stable API, with the reason when it is left out.

How it is tested

  1. An oracle. It renders every example of Prawn’s and prawn-table’s manuals and every sample SVG of prawn-svg with Prawn 2.5, prawn-table 0.2.2 and prawn-svg 0.40.4 and with Gamba, each in its own process, and compares their pages, their text and its positions to 0.01 pt, their pixels at 150 dpi in two renderers that share no code (MuPDF and Poppler), their validity (qpdf --check) and their weight. Examples listed in test/oracle_passing.txt must stay identical; one that becomes identical must join the list. test/oracle_examples holds documents of our own for what the manuals do not show.
  2. The oracle against itself. ORACLE_CANDIDATE=prawn bundle exec rake test runs Prawn as the candidate, which proves the comparison reports nothing when nothing differs.
  3. A document fuzzer. Seeded, it writes random documents (text in all its options, boxes, columns, tables, shapes, pages) and compares them the same way. CI adds 300 from a fresh seed on every run; script/fuzz_minimize.rb shrinks a failing one.
  4. An input fuzzer. It corrupts sample PNGs, JPEGs and fonts (flipped bytes, truncations, forged lengths): each must render or raise an error Gamba names, never crash inside a parser, hang or exhaust memory.
  5. veraPDF. PDF/A documents are checked by veraPDF when it is installed.
FUZZ_SEED=42 FUZZ_CASES=500 bundle exec ruby -Itest test/fuzz_test.rb
ruby -Itest script/fuzz_minimize.rb tmp/fuzz/documents/fuzz_0042.rb