Render a deterministic invoice PDF from Elixir data, preview it inline, and download the same bytes.
Setup
rendro_dep =
if System.get_env("RENDRO_LIVEBOOK_LOCAL") == "1" do
{:rendro, path: System.get_env("RENDRO_LIVEBOOK_PATH") || File.cwd!()}
else
{:rendro, "~> 1.0"}
end
Mix.install([
rendro_dep,
{:jason, "~> 1.4"},
{:jsv, "~> 0.19"},
{:yaml_elixir, ">= 2.11.0 and < 3.0.0"},
{:kino, "~> 0.16"}
])Invoice Data
This mirrors the realistic invoice fixture Rendro ships at
priv/examples/invoice/acme-phoenix-saas/invoice.json: a full invoice anatomy with
an issuer, a customer, a due date and terms, a handful of distinct line items, and
Decimal-faithful totals. Line-item prices are bare numbers (the recipe's legacy
line contract); the summary totals are Decimal and are validated to reconcile
against the line items.
invoice = %{
id: "INV-CMP-2026-001",
date: ~D[2026-06-11],
due_date: ~D[2026-07-11],
terms: "Net 30",
issuer: %{name: "Rendro Systems", address: "100 Deterministic Way, Portland, OR 97205"},
customer: %{name: "Acme Phoenix SaaS", address: "42 LiveView Lane, Austin, TX 78701"},
items: [
%{name: "Platform subscription - Team plan", qty: 1, price: 3600},
%{name: "Additional seats", qty: 12, price: 79},
%{name: "Priority support add-on", qty: 1, price: 480}
],
totals: %{
subtotal: Decimal.new("5028.00"),
tax: Decimal.new("402.24"),
total: Decimal.new("5430.24")
}
}Render
doc = Rendro.Recipes.Invoice.document(invoice)
{:ok, pdf} = Rendro.render(doc, deterministic: true)
if binary_part(pdf, 0, 5) != "%PDF-" do
raise "expected Rendro to render a PDF"
end
sha256 = :crypto.hash(:sha256, pdf) |> Base.encode16(case: :lower)
%{
bytes: byte_size(pdf),
sha256: sha256
}Apply a preset
This exact Invoice/Swiss/#2C6BED/light path uses the same invoice data and keeps
font registration explicit. To try another canonical choice, edit the
preset, accent, and mode values below. Presets are working starting points,
not a design-quality, accessibility, PDF/UA, WCAG, or print guarantee. A
dark-mode experiment is screen-oriented. Screen-oriented; not a print, accessibility, PDF/UA, or WCAG claim.
# rendro-theme-snippet:start
preset = :swiss
theme =
Rendro.Theme.preset(preset, accent: {44, 107, 237}, mode: :light)
document =
invoice
|> Rendro.Recipes.Invoice.document(theme: theme)
|> Rendro.Theme.Presets.register_fonts(preset)
# rendro-theme-snippet:endthemed_document = document
{:ok, themed_pdf} = Rendro.render(themed_document, deterministic: true)
if binary_part(themed_pdf, 0, 5) != "%PDF-" do
raise "expected Rendro to render a themed PDF"
end
themed_sha256 = :crypto.hash(:sha256, themed_pdf) |> Base.encode16(case: :lower)
%{
bytes: byte_size(themed_pdf),
sha256: themed_sha256
}themed_encoded = Base.encode64(themed_pdf)
Kino.HTML.new("""
<iframe
title="Rendro themed invoice PDF preview"
src="data:application/pdf;base64,#{themed_encoded}"
style="width: 100%; height: 640px; border: 1px solid #d1d5db;">
</iframe>
""")Kino.Download.new(fn -> themed_pdf end, filename: "rendro-invoice-swiss.pdf")Preview
encoded = Base.encode64(pdf)
Kino.HTML.new("""
<iframe
title="Rendro invoice PDF preview"
src="data:application/pdf;base64,#{encoded}"
style="width: 100%; height: 640px; border: 1px solid #d1d5db;">
</iframe>
""")Download
Kino.Download.new(fn -> pdf end, filename: "rendro-invoice.pdf")Realistic Example Library
Rendro also ships the realistic demonstration fixtures used by the Rendered Gallery
under priv/examples/ — one per family (invoice, statement, receipt, certificate,
payslip, ticket). They are loaded internally through the Rendro.Examples helper
and coerced to each recipe's shape by Rendro.ExamplesData. Both are
@moduledoc false internal helpers for the shipped demonstration set. Here is the
invoice fixture rendered through that internal path:
raw = Rendro.Examples.load!("invoice/acme-phoenix-saas/invoice.json")
fixture_invoice = Rendro.ExamplesData.transform_invoice(raw)
fixture_doc = Rendro.Recipes.Invoice.document(fixture_invoice)
{:ok, fixture_pdf} = Rendro.render(fixture_doc, deterministic: true)
%{
bytes: byte_size(fixture_pdf),
sha256: :crypto.hash(:sha256, fixture_pdf) |> Base.encode16(case: :lower)
}These fixtures are deterministic demonstrations of each family — not a claim of
visual polish, and Rendro makes no accessibility-standard claim about the rendered
output. In your own app you build the recipe data map directly (as in the section
above) and call the recipe's document/2.
Phoenix Handoff
The controller shape is intentionally schematic and is not executed in this notebook.
defmodule MyAppWeb.InvoiceController do
use MyAppWeb, :controller
def show(conn, %{"id" => id}) do
invoice = Billing.get_invoice!(id)
doc = Rendro.Recipes.Invoice.document(invoice)
{:ok, pdf} = Rendro.render(doc, deterministic: true)
conn
|> put_resp_content_type("application/pdf")
|> put_resp_header("content-disposition", "inline; filename=invoice-#{id}.pdf")
|> send_resp(200, pdf)
end
endNext
- Recipes guide:
guides/recipes.md - Comparison guide:
guides/comparison.md - Self-rendered manual:
assets/rendro/manual.pdf