soli-pdf

Quickstart

This walkthrough renders a small invoice in three files: a template, the data, and a Ruby script. It takes about five minutes. It assumes the gem is installed.

#1. Write the template

A template is a JSON object. content is the list of elements laid out from top to bottom, and ${...} placeholders read from the data. Save this as invoice.template.json:

{
  "fonts": ["titillium"],
  "footer": [
    { "type": "paragraph", "value": "Page #PAGE# of #PAGES#",
      "options": { "alignment": "center", "fontSize": 8, "color": "7c808a" } }
  ],
  "content": [
    { "type": "paragraph", "value": "${company.name}",
      "options": { "fontSize": 22, "fontWeight": "bold", "color": "0f766e" } },
    { "type": "paragraph", "value": "Invoice ${invoice.number} · ${invoice.date}",
      "options": { "fontSize": 11, "spacing": 18 } },

    { "type": "table", "data": "items",
      "header_columns": [
        { "text": "Description", "width": 301, "fontWeight": "bold" },
        { "text": "Qty", "width": 60, "alignment": "right", "fontWeight": "bold" },
        { "text": "Amount", "width": 120, "alignment": "right", "fontWeight": "bold" }
      ],
      "rows": [ [
        { "text": "${name}", "width": 301 },
        { "text": "${qty}", "width": 60, "alignment": "right" },
        { "text": "${amount}", "width": 120, "alignment": "right" }
      ] ],
      "options": { "header": { "fillColor": "0f766e", "textColor": "ffffff" },
                   "stripe": "f1f5f9", "padding_x": 6, "padding_y": 6 } },

    { "type": "move", "y": 12 },
    { "type": "paragraph", "value": "Total due: ${invoice.total}",
      "options": { "alignment": "right", "fontSize": 14, "fontWeight": "bold" } }
  ]
}

A few things to notice:

  • The column widths add up to 481 pt. That is the width of an A4 page (595 pt) minus the default margins of 20 mm on each side.
  • The table has "data": "items". Its single template row repeats once for each entry in the items array, and ${name} resolves against that entry.
  • #PAGE# and #PAGES# are filled in after pagination, so they are correct on every page.

#2. Build the data

The data is a plain Hash. Format numbers and dates in Ruby: the template prints what you give it.

data = {
  'company' => { 'name' => 'Atelier Nord' },
  'invoice' => { 'number' => 'AN-2025-0042', 'date' => '28 Nov 2025', 'total' => '€4,800.00' },
  'items' => [
    { 'name' => 'Brand identity system', 'qty' => '1', 'amount' => '€2,400.00' },
    { 'name' => 'Signage fabrication (6 panels)', 'qty' => '6', 'amount' => '€1,830.00' },
    { 'name' => 'On-site installation', 'qty' => '1', 'amount' => '€570.00' }
  ]
}

#3. Render

require 'json'
require 'soli/pdf'

template = JSON.parse(File.read('invoice.template.json'))

Soli::PDF.render_to_file('invoice.pdf',
  template: template,
  data: data,
  title: 'Invoice AN-2025-0042',
  author: 'Atelier Nord'
)

puts Soli::PDF.last_warnings   # anything the renderer skipped

Open invoice.pdf. That's it.

Tip Paste the template above into the playground, and the data as JSON, to iterate on the layout without running Ruby.

#What just happened

  1. render_to_file called Soli::PDF.render, which serialised the template and the data to JSON.
  2. It started the render_pdf binary, downloaded on the first run. The template went through a temporary file, and the data went through standard input.
  3. The renderer laid out the page and returned the PDF bytes on standard output. The gem wrote them to invoice.pdf.

If a placeholder had no value, for example a typo like ${invoice.nubmer}, the render still succeeds: the placeholder renders empty and a warning shows up in Soli::PDF.last_warnings. A template that isn't valid (an unknown element type, broken JSON) raises Soli::PDF::RenderError instead. See Errors & warnings.

#Keep going