soli-pdf

Starter quote

The starter invoice's skeleton with its own identity and a valid-until strip.

A good second template to read: the same structure as the starter invoice, different colours and a different call to action.

Data-bound table Colour band 1 page
Page 1
Page 1 of 1
render_quote.rb ruby
require 'json'
require 'soli/pdf'

template = JSON.parse(File.read('quote.template.json'))
data     = JSON.parse(File.read('quote.data.json'))

pdf = Soli::PDF.render(
  template: template,
  data: data,
  title: 'Quote Q-2025-0117'
)
File.binwrite('quote.pdf', pdf)

Run it yourself

  1. Add the gem to your Gemfile — see Installation.
  2. Download quote.template.json and quote.data.json.
  3. Run ruby render_quote.rb.
quote.template.json json
{
  "fonts": ["titillium"],
  "options": { "header_height": 0 },
  "footer": [
    { "type": "paragraph", "value": "${company.name} · ${company.email} · Page #PAGE# of #TOTAL_PAGE#", "options": { "alignment": "center", "fontSize": 8 } }
  ],
  "content": [
    {
      "type": "table",
      "header_columns": [
        {
          "content": [
            { "type": "text", "value": "${company.name}", "fontSize": 19, "fontWeight": "bold" },
            { "type": "text", "value": "Estimate", "fontSize": 10 }
          ],
          "width": 300, "alignment": "left",
          "borderSides": { "top": "false", "bottom": "false", "left": "false", "right": "false" }
        },
        {
          "text": "QUOTE", "width": 181, "fontSize": 26, "fontWeight": "bold", "alignment": "right",
          "borderSides": { "top": "false", "bottom": "false", "left": "false", "right": "false" }
        }
      ],
      "rows": [],
      "options": { "header": { "fillColor": "1F2430", "textColor": "E7A33E" }, "padding_x": 18, "padding_y": 18 }
    },
    {
      "type": "table",
      "header_columns": [
        {
          "text": "Quote ${quote.number}   ·   Prepared for ${customer.name}   ·   Valid until ${quote.valid_until}",
          "width": 481, "fontSize": 10, "fontWeight": "bold", "alignment": "center",
          "borderSides": { "top": "false", "bottom": "false", "left": "false", "right": "false" }
        }
      ],
      "rows": [],
      "options": { "header": { "fillColor": "FBEFD6", "textColor": "7A5310" }, "padding_x": 12, "padding_y": 9 }
    },
    { "type": "move", "y": 24 },
    {
      "type": "table",
      "data": "items",
      "header_columns": [
        { "text": "SCOPE", "width": 300, "fontSize": 9, "fontWeight": "bold", "alignment": "left",  "borderSides": { "top": "false", "left": "false", "right": "false", "bottom": "true" } },
        { "text": "QTY",   "width": 50,  "fontSize": 9, "fontWeight": "bold", "alignment": "center","borderSides": { "top": "false", "left": "false", "right": "false", "bottom": "true" } },
        { "text": "RATE",  "width": 70,  "fontSize": 9, "fontWeight": "bold", "alignment": "right", "borderSides": { "top": "false", "left": "false", "right": "false", "bottom": "true" } },
        { "text": "EST.",  "width": 81,  "fontSize": 9, "fontWeight": "bold", "alignment": "right", "borderSides": { "top": "false", "left": "false", "right": "false", "bottom": "true" } }
      ],
      "rows": [
        [
          { "text": "${name}",   "width": 300, "fontSize": 10, "alignment": "left",   "borderSides": { "top": "false", "left": "false", "right": "false", "bottom": "true" }, "borderColor": "EAE7E0" },
          { "text": "${qty}",    "width": 50,  "fontSize": 10, "alignment": "center", "borderSides": { "top": "false", "left": "false", "right": "false", "bottom": "true" }, "borderColor": "EAE7E0" },
          { "text": "${rate}",   "width": 70,  "fontSize": 10, "alignment": "right",  "borderSides": { "top": "false", "left": "false", "right": "false", "bottom": "true" }, "borderColor": "EAE7E0" },
          { "text": "${amount}", "width": 81,  "fontSize": 10, "alignment": "right",  "borderSides": { "top": "false", "left": "false", "right": "false", "bottom": "true" }, "borderColor": "EAE7E0" }
        ]
      ],
      "options": { "header": { "fillColor": "F4F1EA", "textColor": "1F2430", "borderColor": "1F2430" }, "padding_x": 6, "padding_y": 7 }
    },
    { "type": "move", "y": 12 },
    {
      "type": "table",
      "header_columns": [],
      "rows": [
        [
          { "text": "", "width": 271, "borderSides": { "top": "false", "left": "false", "right": "false", "bottom": "false" } },
          { "text": "Estimated total", "width": 130, "fontSize": 14, "fontWeight": "bold", "alignment": "right", "borderSides": { "top": "true", "left": "false", "right": "false", "bottom": "false" }, "borderColor": "E7A33E" },
          { "text": "${quote.total}", "width": 80, "fontSize": 14, "fontWeight": "bold", "alignment": "right", "borderSides": { "top": "true", "left": "false", "right": "false", "bottom": "false" }, "borderColor": "E7A33E" }
        ]
      ],
      "options": { "padding_x": 6, "padding_y": 7 }
    },
    { "type": "move", "y": 22 },
    {
      "type": "paragraph",
      "value": "${quote.note}",
      "options": { "fontSize": 10, "alignment": "left" }
    }
  ]
}
quote.data.json json
{
  "data": {
    "company": { "name": "Atelier Nord", "email": "studio@ateliernord.fr" },
    "customer": { "name": "Maison Lumière" },
    "quote": {
      "number": "Q-2025-0117",
      "valid_until": "31 Dec 2025",
      "total": "€4,800",
      "note": "This estimate is indicative and excludes shipping. Acceptance by reply email converts it into a firm order; a 30% deposit schedules fabrication."
    },
    "items": [
      { "name": "Brand identity system", "qty": 1, "rate": "€2,400", "amount": "€2,400" },
      { "name": "Signage fabrication (6 panels)", "qty": 6, "rate": "€180", "amount": "€1,080" },
      { "name": "On-site installation", "qty": 1, "rate": "€520", "amount": "€520" }
    ]
  }
}