---
title: "pdfmake alternative: the same invoice as a definition — quario"
description: "pdfmake builds a PDF from a document object your code assembles. quario renders one JSON definition to PDF, Excel and Word. Both side by side, honestly."
url: "https://getquario.com/compare/pdfmake"
---

# pdfmake alternative

pdfmake is a good library. It’s MIT-licensed, and it runs in Node and in the browser. For years it
has been the usual answer to “a table that spans pages”. You describe the document as one object of
paragraphs, tables, and columns. pdfmake lays it out on top of PDFKit, repeats header rows after a
page break, and fills in page numbers through a footer function.

What it doesn’t do is know anything about your data. Before pdfmake sees a row, your code has looped
over the records and multiplied the quantities. It has summed the totals, too, and formatted every
amount as a string. That assembly code is the part teams usually go looking to replace, and it’s what
this page is about.

## The same invoice, twice

One invoice, three lines, a subtotal, 21% VAT, a total, and “page 1 of 1” in the footer. First with
pdfmake 0.3 in Node:

```ts title="invoice-pdfmake.ts"
import pdfmake from 'pdfmake'

type Line = { item: string; qty: number; unitPrice: number }
const { invoice } = (await getInvoice()) as { invoice: { number: string; lines: Line[] } }

const eur = new Intl.NumberFormat('en-IE', { style: 'currency', currency: 'EUR' })
const round = (n: number) => Math.round(n * 100) / 100
const right = (text: string, bold = false) => ({ text, bold, alignment: 'right' })

const subtotal = invoice.lines.reduce((sum, l) => sum + l.qty * l.unitPrice, 0)
const vat = subtotal * 0.21

pdfmake.addFonts({ Roboto: { normal: 'fonts/Roboto-Regular.ttf', bold: 'fonts/Roboto-Medium.ttf' } })
pdfmake.setLocalAccessPolicy((path: string) => path.startsWith('fonts/'))

const doc = {
  pageSize: 'A4',
  pageMargins: [54, 54, 54, 54],
  defaultStyle: { fontSize: 10 },
  footer: (currentPage: number, pageCount: number) => ({
    text: `Invoice ${invoice.number} · page ${currentPage} of ${pageCount}`,
    alignment: 'right', fontSize: 8, margin: [54, 0],
  }),
  content: [
    { text: `Invoice ${invoice.number}`, fontSize: 22, bold: true, margin: [0, 0, 0, 18] },
    {
      layout: 'headerLineOnly',
      table: {
        headerRows: 1,                                 // repeated after a page break
        widths: ['*', 'auto', 'auto', 'auto'],
        body: [
          [{ text: 'Item', bold: true }, right('Qty', true), right('Unit', true), right('Amount', true)],
          ...invoice.lines.map((l) => [
            l.item,
            right(String(l.qty)),
            right(eur.format(l.unitPrice)),
            right(eur.format(round(l.qty * l.unitPrice))),
          ]),
          [{ ...right('Subtotal'), colSpan: 3 }, {}, {}, right(eur.format(round(subtotal)))],
          [{ ...right('VAT 21%'), colSpan: 3 }, {}, {}, right(eur.format(round(vat)))],
          [{ ...right('Total due', true), colSpan: 3 }, {}, {}, right(eur.format(round(subtotal + vat)), true)],
        ],
      },
    },
  ],
}

await pdfmake.createPdf(doc).write('invoice.pdf')
```

That’s a fair pdfmake program, and it works. Read it again for where the invoice actually lives. The
columns are in `body`, the arithmetic is in `reduce`, the money is in `Intl.NumberFormat`, and the
footer is a function. Add a column and you touch the widths, the header row, the row mapping and
every `colSpan`.

Now the same invoice as a quario definition. This is the file the card on the
[guides page](https://getquario.com/guides.md) renders:

```json title="invoice.report.json"
{
  "data": "$.invoice.lines[*]",
  "aggregates": { "subtotal": "sum:=@.qty * @.unitPrice" },
  "style": { "size": 10 },
  "header": [
    { "type": "text",
      "value": "Invoice {{ $.input.invoice.number }}",
      "style": { "size": 22, "bold": true, "spaceAfter": 18 } }
  ],
  "detail": {
    "header": { "style": { "bold": true,
      "borderBottomWidth": 0.5, "borderBottomStyle": "solid", "borderBottomColor": "#1e1f22" } },
    "columns": [
      { "header": "Item", "value": "{{ @.item }}", "width": 46 },
      { "header": { "value": "Qty", "style": { "align": "right" } }, "value": "{{ @.qty }}",
        "style": { "align": "right" }, "width": 12 },
      { "header": { "value": "Unit", "style": { "align": "right" } }, "value": "{{ @.unitPrice }}",
        "style": { "align": "right", "format": "currency" }, "width": 20 },
      { "header": { "value": "Amount", "style": { "align": "right" } }, "value": "{{ round(@.qty * @.unitPrice, 2) }}",
        "style": { "align": "right", "format": "currency" }, "width": 22 }
    ],
    "total": {
      "style": { "align": "right" },
      "rows": [
        { "cells": [
          { "value": "Subtotal", "span": 3 },
          { "value": "{{ round($.subtotal, 2) }}", "style": { "format": "currency" } }
        ] },
        { "cells": [
          { "value": "VAT 21%", "span": 3 },
          { "value": "{{ round($.subtotal * 0.21, 2) }}", "style": { "format": "currency" } }
        ] },
        { "style": { "bold": true,
            "borderTopWidth": 0.5, "borderTopStyle": "solid", "borderTopColor": "#1e1f22" },
          "cells": [
            { "value": "Total due", "span": 3 },
            { "value": "{{ round($.subtotal * 1.21, 2) }}", "style": { "format": "currency" } }
          ] }
      ]
    }
  },
  "page": {
    "footer": [
      { "type": "text",
        "value": "Invoice {{ $.input.invoice.number }} · page {{ page.number }} of {{ page.total }}",
        "style": { "size": 8, "align": "right" } }
    ]
  }
}
```

And the code that renders it:

```ts title="invoice-quario.ts"
import { writeFileSync } from 'node:fs'
import { quario } from 'quario'
import { pdf } from '@quario/pdf'
import definition from './invoice.report.json' with { type: 'json' }

const report = quario({ locale: 'en-IE', currency: 'EUR' }).report(definition)

const data = await getInvoice()                    // your JSON, from an API or DB
const bytes = await report.render(pdf({ page: { size: 'A4', margin: 54 } }), data)

writeFileSync('invoice.pdf', bytes)
```

The loop is `"data": "$.invoice.lines[*]"`. The subtotal is an aggregate the engine computes, and
`round(…, 2)` sits on the cell that shows it. The amounts stay numbers, and `"format": "currency"`
presents them from the locale and currency you hand `quario()`. The footer reads `page.number` and
`page.total` without a callback. Nothing in the definition is code, so it’s JSON you store, version
and review like any other data.

## What changes when you switch

**The document becomes data.** A pdfmake document object with a footer function can’t round-trip
through JSON, and the amounts in it are already strings. A quario definition is plain JSON. It
can sit in a database, arrive from an editor, or come from an AI agent, and it still renders safely.
Expressions parse into closures. There’s no `eval`, no `new Function` and no code generation, so a
definition someone else wrote can’t run anything. [How that holds →](https://getquario.com/docs/diving-deeper/security.md)

**One definition reaches four document shapes.** pdfmake writes PDF. The same quario definition
renders to an HTML fragment with `@quario/html`, a PDF with `@quario/pdf`, an Excel workbook with
`@quario/xlsx` and a Word document with `@quario/docx`:

```ts title="every-shape.ts"
import { quario } from 'quario'
import { html } from '@quario/html'
import { pdf } from '@quario/pdf'
import { xlsx } from '@quario/xlsx'
import { docx } from '@quario/docx'

const report = quario({ locale: 'en-IE', currency: 'EUR' }).report(definition)

const page = await report.render(html(), data)
const paper = await report.render(pdf({ page: { size: 'A4' } }), data)
const book = await report.render(xlsx(), data)
const word = await report.render(docx({ page: { size: 'A4' } }), data)
```

In the workbook the Amount column is still numbers, under a currency number format, so the customer’s
accountant can sum it. A pdfmake export to Excel is a second program.

**Pagination stays automatic.** Both libraries repeat a table’s header rows after a page break.
quario also keeps group headers with their first rows and keeps the last row with its totals, without
a declaration in the definition. The [PDF target](https://getquario.com/docs/diving-deeper/pdf-target.md) covers what it
promises and what it does best-effort.

**The bytes repeat.** The PDF target writes no creation timestamp. The same definition and data
produce a byte-identical file, which is a fair thing to test against.

## When to keep pdfmake

Switch for the reasons above, and only for them. pdfmake is the better choice when:

- **The project is open source, or the budget is zero.** pdfmake is MIT. quario is commercial past
evaluation.
- **The document isn’t a report.** A brochure, a letter or a one-off layout with nested columns and
lists is where pdfmake’s document object is at home. A report definition is for documents whose
shape comes from rows of data.
- **You need PDF features quario doesn’t claim**, such as a generated table of contents or tiling
patterns.
- **The existing code works and nothing else needs the document.** If nobody asks for the same
report in Excel or Word and the layout rarely changes, a rewrite buys you little.

Reach for quario when the document is data-driven, such as an invoice, a statement, a payslip, or a
sales report. It earns its place when the document has to exist as a PDF and a spreadsheet. It also
fits when someone other than your code writes the layout.

## FAQ

### Does quario run in the browser like pdfmake?

Yes. `@quario/pdf` renders in the browser as well as in Node and edge runtimes, with no headless
browser anywhere. The definition you render on the server is the one the browser renders.

### Do I still need font files?

pdfmake reads the TTF files you register with `addFonts` in Node, or a virtual file system of them
in the browser. quario’s PDF target draws Latin text in the PDF standard faces and needs no font
files for it. For full Unicode or your own face, hand `pdf({ fonts })` TrueType bytes and install the
optional `fontkit` peer. The target then embeds a subset of the glyphs it used. The
[PDF target](https://getquario.com/docs/diving-deeper/pdf-target.md#embedding-fonts-for-full-unicode) covers it.

### Can I migrate one document at a time?

Yes. quario is a dependency, not a framework. Render the invoice through a definition and leave the
rest of your pdfmake documents where they are.

### Can I use quario for free?

Evaluating it’s free, with no time limit. You get the full engine and need no key, and evaluation
output carries a watermark. Shipping it in an application or running it in your business needs a
license for each production deployment. Its users, agents, and renders are never counted, and replicas
of one service count once. [See pricing →](https://getquario.com/pricing.md)

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://getquario.com/"},{"@type":"ListItem","position":2,"name":"Comparisons","item":"https://getquario.com/compare"},{"@type":"ListItem","position":3,"name":"pdfmake alternative","item":"https://getquario.com/compare/pdfmake"}]}
```
