Free to develop and test

The full-stackPDF library

brevier is written in Python. It makes PDFs from the plainest web tools: HTML, CSS, JSON and Handlebars. Or write the template in Markdown, which AI agents read and write with ease.

pip install brevier
Choose an example
rechnung

rechnung/rechnung.html

<!DOCTYPE html>
<html lang="de">
<head><meta charset="utf-8"><title>Rechnung {{ rechnung.nummer }}</title></head>
<body>
  <p class="absender">{{ firma.name }}, {{ firma.anschrift }}</p>
  <address>{{ kunde.name }}<br>{{ kunde.strasse }}<br>{{ kunde.ort }}</address>

  <h1>Beitragsrechnung {{ rechnung.nummer }}</h1>
  <p>Datum: {{ rechnung.datum | date }}</p>

  <table>
    <thead>
      <tr><th>Pos.</th><th>Leistung</th><th>Betrag</th></tr>
    </thead>
    <tbody>
      {{#each rechnung.positionen}}
      <tr><td>{{ pos }}</td><td>{{ text }}</td><td>{{ betrag | money }}</td></tr>
      {{/each}}
    </tbody>
  </table>
  <p class="summe">Summe {{ rechnung.summe | money }}</p>
</body>
</html>

rechnung/rechnung.css

@page {
  size: A4;
  margin: 25mm 20mm 25mm 25mm;
  @bottom-right {
    content: "Seite " counter(page) " von " counter(pages);
    font: 8pt sans-serif;
  }
}
body { font: 10pt/1.4 sans-serif; color: #16181c; }
.absender { font-size: 7pt; border-bottom: 0.5pt solid #16181c; }
address { font-style: normal; margin: 2mm 0 20mm; }
h1 { font-size: 14pt; }
table { width: 100%; border-collapse: collapse; }
th { text-align: left; border-bottom: 1pt solid #16181c; }
td { padding: 1.5mm 0; border-bottom: 0.5pt solid #d5d9df; }
td:first-child { width: 14mm; }
td:nth-child(2) { width: 34mm; }
td:last-child, th:last-child { text-align: right; }
.summe { text-align: right; font-weight: bold; }

rechnung/rechnung.json

{
  "firma": { "name": "Muster Versicherung AG", "anschrift": "Hauptstraße 1, 10115 Berlin" },
  "kunde": { "name": "Erika Mustermann", "strasse": "Beispielweg 7", "ort": "50667 Köln" },
  "rechnung": {
    "nummer": "R-2026-0142",
    "datum": "2026-09-25",
    "positionen": [
      { "pos": 1, "text": "Berufsunfähigkeitsversicherung", "betrag": 84.2 },
      { "pos": 2, "text": "Haftpflichtversicherung", "betrag": 7.9 },
      { "pos": 3, "text": "Hausratversicherung", "betrag": 12.4 }
    ],
    "summe": 104.5
  }
}

rechnung/rechnung.schema.json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "firma": { "$ref": "#/$defs/Firma" },
    "kunde": { "$ref": "#/$defs/Kunde" },
    "rechnung": { "$ref": "#/$defs/Rechnung" }
  },
  "required": ["firma", "kunde", "rechnung"],
  "$defs": {
    "Firma": {
      "type": "object",
      "properties": {
        "name": { "type": "string" },
        "anschrift": { "type": "string" }
      },
      "required": ["name", "anschrift"]
    },
    "Kunde": {
      "type": "object",
      "properties": {
        "name": { "type": "string" },
        "strasse": { "type": "string" },
        "ort": { "type": "string" }
      },
      "required": ["name", "strasse", "ort"]
    },
    "Rechnung": {
      "type": "object",
      "properties": {
        "nummer": { "type": "string", "title": "Rechnungsnummer" },
        "datum": { "type": "string", "format": "date", "title": "Rechnungsdatum" },
        "positionen": { "type": "array", "items": { "$ref": "#/$defs/Position" } },
        "summe": { "type": "number", "format": "decimal", "title": "Gesamtbetrag" }
      },
      "required": ["nummer", "datum", "positionen", "summe"]
    },
    "Position": {
      "type": "object",
      "properties": {
        "pos": { "type": "integer" },
        "text": { "type": "string" },
        "betrag": { "type": "number", "format": "decimal" }
      },
      "required": ["pos", "text", "betrag"]
    }
  }
}

rechnung/rechnung.pdf

Page 1 of the invoice, as brevier rendered it from these files.
rechnung

rechnung/rechnung.md

---
style: rechnung.css
---

{{ firma.name }}, {{ firma.anschrift }}

An: {{ kunde.name }}, {{ kunde.strasse }}, {{ kunde.ort }}

# Beitragsrechnung {{ rechnung.nummer }}

Datum: {{ rechnung.datum | date }}

| Pos. | Leistung | Betrag |
| --- | --- | ---: |
{{#each rechnung.positionen}}
| {{ pos }} | {{ text }} | {{ betrag | money }} |
{{/each}}

**Summe {{ rechnung.summe | money }}**

rechnung/rechnung.css

@page {
  size: A4;
  margin: 25mm 20mm 25mm 25mm;
  @bottom-right {
    content: "Seite " counter(page) " von " counter(pages);
    font: 8pt sans-serif;
  }
}
body { font: 10pt/1.4 sans-serif; color: #16181c; }
.absender { font-size: 7pt; border-bottom: 0.5pt solid #16181c; }
address { font-style: normal; margin: 2mm 0 20mm; }
h1 { font-size: 14pt; }
table { width: 100%; border-collapse: collapse; }
th { text-align: left; border-bottom: 1pt solid #16181c; }
td { padding: 1.5mm 0; border-bottom: 0.5pt solid #d5d9df; }
td:first-child { width: 14mm; }
td:nth-child(2) { width: 34mm; }
td:last-child, th:last-child { text-align: right; }
.summe { text-align: right; font-weight: bold; }

rechnung/rechnung.json

{
  "firma": { "name": "Muster Versicherung AG", "anschrift": "Hauptstraße 1, 10115 Berlin" },
  "kunde": { "name": "Erika Mustermann", "strasse": "Beispielweg 7", "ort": "50667 Köln" },
  "rechnung": {
    "nummer": "R-2026-0142",
    "datum": "2026-09-25",
    "positionen": [
      { "pos": 1, "text": "Berufsunfähigkeitsversicherung", "betrag": 84.2 },
      { "pos": 2, "text": "Haftpflichtversicherung", "betrag": 7.9 },
      { "pos": 3, "text": "Hausratversicherung", "betrag": 12.4 }
    ],
    "summe": 104.5
  }
}
src/main/java

src/main/java/RechnungService.java

import com.brevier.Brevier;

public class RechnungService {

    public byte[] pdf(Firma firma, Rechnung rechnung, Kunde kunde) {
        return Brevier.template("rechnung")
                      .render(firma, rechnung, kunde);
    }
}
app

app/rechnung.py

import brevier


def pdf(firma, rechnung, kunde) -> bytes:
    return brevier.template("rechnung").render(firma, rechnung, kunde)

Everything a business document needs

brevier handles the print rules, so you can focus on your data.

  • HTML and CSS templates

    The page is HTML with CSS. No proprietary format, no drawing code.

  • Markdown for simple documents

    Write a letter in Markdown with front matter. brevier eject turns it into the HTML it compiles to.

  • Data binding

    Handlebars syntax binds JSON into the template: values, loops and conditions.

  • Templates checked against a schema

    A misspelt field fails the check before anything prints.

  • from-code: start from your classes

    Reads Java classes or Python models and writes a schema, a template and sample data.

  • The visible middle

    --emit-html writes the exact HTML the engine received, and --format html renders a web view of the same document.

  • German and English hyphenation

    Compound words break at their joins: Berufsunfähigkeits-versicherung, not Beruf-sunfähigkeit. Companies can add their own words.

  • Tables that span pages

    Declared column widths and a header row that repeats on every page.

  • Übertrag

    The running total is carried to the bottom of each page and to the top of the next.

  • Headers, footers and page counters

    Seite 3 von 12, in one editable block, on every page.

  • Contracts and fine print

    Numbered clauses, a table of contents with page numbers, and small print that stays readable. No heading is left alone at the bottom of a page.

  • Every feature, with its status

    Open the feature list

Brevier is German for a prayer book: the small book you keep close and use every day. That is what brevier wants to be for developers who must write code that makes PDFs. I had to do that job myself, with tools that made it hard. So I built brevier to make PDF generation easy for developers.

Liviu IonesiAuthor of brevier

Built on what you already know

HTML, CSS, JSON and your own code. Nothing new to learn.

  • HTML for the page

    The template is an HTML page. Handlebars fills it with your data: values, loops and conditions.

    developer.mozilla.org
  • CSS for the look

    CSS sets the fonts and colours, and the printed page too: size, margins, headers, footers and page numbers.

    CSS @page
  • JSON for the data

    Your data comes in as JSON. A JSON Schema checks every field before anything prints.

    json-schema.org
  • Your own code

    Send the objects you already have from Java, Python and other languages. brevier turns them into the data.

    See the clients

Install anywhere

Install brevier with pip, a one-line script or Docker. It runs on Linux, macOS and Windows.

Call it from your language
Choose how to install

For Python 3 users. pip needs the Pango library on your system.

  1. Install brevier
    pip install brevier
  2. Check that it works
    brevier --version
  3. Render the example from the top of this page
    brevier render rechnung.html --data rechnung.json -o rechnung.pdf

The script installs brevier in its own folder and puts the brevier command in ~/.local/bin. On macOS it also installs Pango.

  1. Install brevier
    curl -fsSL https://get.brevier.dev | sh
  2. Check that it works
    brevier --version
  3. Render the example from the top of this page
    brevier render rechnung.html --data rechnung.json -o rechnung.pdf

The PowerShell script installs Pango through MSYS2, then installs brevier for your user and adds it to your PATH.

  1. Install brevier
    iwr -useb https://get.brevier.dev/ps1 | iex
  2. Check that it works
    brevier --version
  3. Render the example from the top of this page
    brevier render rechnung.html --data rechnung.json -o rechnung.pdf

Nothing to install on the machine but Docker. The image runs brevier in the folder you mount at /work.

  1. Install brevier
    docker pull brevier/brevier:latest
  2. Check that it works
    docker run --rm brevier/brevier --version
  3. Render the example from the top of this page
    docker run --rm -v "$PWD:/work" brevier/brevier render rechnung.html --data rechnung.json -o rechnung.pdf