# PDF and HTML documents | DRE

> The html and pdf formats: a branded document from a Jinja HTML + CSS template, with no browser.

You're reading the docs for DRE 0.8 (v0.8.1). [This page for 0.11](https://getdre.com/docs/plugin-document/)

# PDF and HTML documents

PDF and HTML documents from an HTML + CSS template: the `html` format and `pdf` format, from the `document` package. Declare the package once in `dependencies.yml`; DRE installs it on demand:

dependencies.yml

```yaml
plugins:
  - document
```

### `html` format: options

Set in a report’s `output:` (or `format_options.html` in `dre_project.yml`).

Option

Type

Default

Notes

`max_rows`

integer

`50000`

The most rows across the output’s queries; more fails the run.

`embed_assets`

boolean

`true`

Inline images, stylesheets and fonts as data URIs, so the file works wherever it’s delivered.

### `pdf` format: options

Set in a report’s `output:` (or `format_options.pdf` in `dre_project.yml`).

Option

Type

Default

Notes

`max_rows`

integer

`50000`

The most rows across the output’s queries; more fails the run.

`page_size`

string

`A4`

Paper size, used when the template’s CSS sets no `@page` size. One of `A3`, `A4`, `A5`, `Letter`, `Legal`.

`orientation`

string

`portrait`

Used when the template’s CSS sets no `@page` size. One of `portrait`, `landscape`.

`margins`

string

`20mm`

Page margins as in CSS, like `20mm` or `15mm 20mm`; used when the CSS sets no `@page` margin.

`author`

string

The PDF’s author metadata.

`subject`

string

The PDF’s subject metadata.

`password`

string

An open password: the PDF is encrypted with AES-256 and opens only with it; Jinja like `{{ each.pdf_password }}`

A client statement, a board pack or a compliance summary: one HTML + CSS template, filled with the report’s query results, written as an `.html` file or laid out into a `.pdf`. The PDF is made inside the plugin: no browser, no Playwright, no conversion service and no network call. _New in DRE 0.6._ The package needs DRE 0.6.0 or later.

## A statement

reports/clients/statement/statement.yml

```yaml
queries: [summary, holdings]
vars:
  client: Acme Pty Ltd
sets:
  - {name: acme}
  - {name: beta, vars: {client: Beta Ltd}}
output:
  format: pdf
  template:
    file: statement.html          # found next to this YAML (see "Finding a template")
  author: Acme Funds
```

reports/clients/statement/statement.html

```html
{% extends "base.html" %}
{% block title %}Statement for {{ var('client') }}{% endblock %}
{% block body %}
<h1>{{ var('client') }}</h1>
<p>Balance on {{ run.date | date('d mmmm yyyy') }}:
   <strong>{{ results.summary.first.balance | currency('AUD', 2) }}</strong></p>
{{ table(results.holdings) }}
{% include "terms.html" %}
{% endblock %}
```

```html
<!-- reports/clients/base.html: shared by every report in the folder -->
<!doctype html>
<html lang="en-AU">
<head>
  <title>{% block title %}{% endblock %}</title>
  <link rel="stylesheet" href="assets/brand.css">
  <style>
    @page { size: A4; margin: 20mm 15mm;
            @bottom-center { content: "Page " counter(page) " of " counter(pages); } }
  </style>
</head>
<body>
  <img src="assets/logo.svg" alt="Acme">
  {% block body %}{% endblock %}
</body>
</html>
```

`dre run statement --set all` writes `statement.pdf` for each Set. Change `format: pdf` to `format: html` for a web page from the same template. With no `template:`, the output is a plain document with a table per query.

## What a template reads

Everything a [message](https://getdre.com/docs/v0.8/messages/#what-a-message-template-reads) template reads, the same way:

`results.<query>`

each query of the output, in YAML order: `value`, `first.<column>`, `rows`, `row_count`, `columns`, `column_types` (each column’s type, `{amount: Decimal128(18, 2)}`), `sets[n]`

`var('name')`

every resolved var: `--var`, the schedule’s, the Set’s, the report’s, its folders’ and the project’s, whether or not a query uses it

`run.*`, `target.name`

the report, Set, date, timezone and the rest of [`run`](https://getdre.com/docs/v0.8/templates/)

`locale`

the run’s [`locale:`](https://getdre.com/docs/v0.8/templates/#locale) tag, for `<html lang>`

Values keep their types: numbers stay numbers, dates are DRE dates (shown in the run’s timezone), nulls are `none`. Unlike messages, `rows` holds every row: a document is never cut short (see `max_rows`). `env_var()` isn’t available: a document is data that’s delivered.

Filters and functions:

-   The [number filters](https://getdre.com/docs/v0.8/templates/#numbers-number-percent-signed-currency-compact): `number`, `percent`, `signed`, `currency` and `compact`, in the run’s `locale:`, as in messages.
-   `date(format)`: a date or timestamp in an Excel-style format (`dd/mm/yyyy`, `d mmm yyyy`, `yyyy-mm-dd hh:mm`); default `yyyy-mm-dd`.
-   `row.fmt('amount')` (on a row of `rows`, or `first`) shows a value in its column’s YAML `format`, the [xlsx format codes](https://getdre.com/docs/v0.8/plugin-xlsx/#column-formats) (`$#,##0.00`, `d mmm yyyy`); a column with no format is shown as `table()` shows it. `row.amount` stays the raw value, for comparisons and arithmetic.
-   `table(results.<query>)` writes a styled `<table class="dre-table">` of the query’s rows, each value shown with its column’s YAML `format` (the [xlsx format codes](https://getdre.com/docs/v0.8/plugin-xlsx/#column-formats), such as `$#,##0.00` or `d mmm yyyy`). `table(results.q, ['fund', 'amount'])` picks and orders the columns. Style it with CSS on `.dre-table`.
-   The date functions (`date()`, `period()`, …) of [every template](https://getdre.com/docs/v0.8/templates/).

Values are HTML-escaped, so a client called `A&B <Ltd>` shows as written; `| safe` inserts deliberate markup. A value that isn’t there (`results.holdings.first.amout`) fails the run, naming the template line and the queries and columns there are, so a statement never goes out with a silent blank. Project macros (`macros/`) aren’t available here: share layout with `{% include %}` and `{% extends %}`.

## Finding a template

`template.file`, `{% include %}`, `{% extends %}` and `{% import %}` find a file the same way:

-   A name with `/` is a path from the project root, taken exactly: `templates/statement.html`.
-   A bare file name is looked for next to the report’s YAML (for an include: next to the including template), then in each parent folder up to the project root; the nearest wins.
-   Failing that, anywhere in the project, where it must be unique. Two matches are an error that lists both; name one by its path.

Never outside the project, and never a URL. The same rule finds an [xlsx](https://getdre.com/docs/v0.8/plugin-xlsx/#start-from-a-complete-workbook-project) template. A Set can override `template.file` like any other output key, so one client can get a different layout.

## Images, stylesheets and fonts

`<img src>`, `<link rel="stylesheet" href>` and CSS `url()` are relative to the template that writes them (a shared `base.html` links its logo next to itself), and must be inside the project. `http(s)://` is refused: rendering is offline and gives the same document every time. PNG, JPEG, GIF and SVG images work, and so does inline `<svg>`, so a logo or a simple chart made by a macro appears in the PDF.

An `html` output is one self-contained file: its images, stylesheets and fonts are inlined as data URIs, so it still shows its logo after email, S3 or SFTP delivery. `embed_assets: false` keeps them as links, for a file published next to its assets.

The PDF uses only fonts it’s given, never the machine’s, so it looks the same everywhere. The plugin bundles **IBM Plex Sans** (regular, bold, italic) and **IBM Plex Mono**, under the SIL Open Font License. Add your own with `@font-face`, from a `.ttf` or `.otf` file in the project:

```css
@font-face { font-family: Brand; src: url(fonts/Brand-Regular.ttf); }
@font-face { font-family: Brand; src: url(fonts/Brand-Bold.ttf); font-weight: bold; }
body { font-family: Brand, sans-serif; }
```

A `font-family` that’s neither bundled nor declared falls back to IBM Plex Sans, with a warning naming it. Fonts are subset: the PDF holds only the characters it uses.

Text inside an SVG is the exception: it’s drawn with the machine’s fonts. For a logo that looks the same everywhere, convert its text to outlines (paths) in your design tool, or write the name in the html next to an icon-only SVG.

## Page setup

Set the page in CSS, as for print: `@page { size: A4 landscape; margin: 15mm }`, page-margin boxes for running headers and footers (`@top-right`, `@bottom-center` with `counter(page)` and `counter(pages)`), and `break-before: page`, `break-after`, `break-inside: avoid`. A table that runs over several pages repeats its `<thead>` on each. The `page_size`, `orientation` and `margins` options apply only where the CSS sets nothing.

The PDF’s title comes from `<title>`, its language from `<html lang>`, `author` and `subject` from the options, and its creation date is the run’s `run.now`, so the same data makes the same file.

## How big a document can be

`max_rows` (default 50,000) caps the rows across every query of one document. More fails the run with a hint, before anything is laid out: aggregate in SQL, filter, or raise it. A message instead keeps its first `max_rows` rows; a document fails, because it must never be cut short. A single table of more than about 30,000 rows is more than the layout engine handles: split it, or aggregate. Laying out 5,000 table rows takes about a second.

## Checks before a run

`dre validate` (and `dre run`, before any query) parses the template and every file it includes by a literal name, checks its images and stylesheets exist, its filters exist, and that each `results.<name>` is a query of the output, with a “did you mean” hint. An include whose name is computed (`{% include "terms/" ~ var('client') ~ ".html" %}`) gets a warning that it isn’t checked until the run.

## Designing a template

A template takes many tweaks. Save the results once, then re-render from them, with no query and no connection, in about a second:

```bash
dre run statement --set acme --no-deliver --output-json design   # runs the queries once
dre run statement --set acme --from-results design               # each tweak: re-render only
```

`--from-results` reads the results `--output-json` saved (`design/<report>/<binding>/`, see [the manifest page](https://getdre.com/docs/v0.8/manifest/)), renders every output of the report (xlsx and messages too), and implies `--no-deliver`. It fails when a query has no saved result, and warns when a query’s SQL changed since they were saved.

### What the PDF engine draws differently

The PDF is laid out by [fulgur](https://github.com/fulgur-rs/fulgur), not a browser, so check a design in the PDF itself. One difference: a box’s borders share one colour, its top border’s. For a coloured line on one side only, set the colour for every side and the width for that side:

```css
header { border: 0 solid #e8873a; border-bottom-width: 2pt; }
```

## Password-protected PDFs

`password:` on a `pdf` output encrypts the file after it’s laid out: it opens only with that password. AES-256 (PDF 2.0), with a random owner password nobody keeps, so the encryption can’t be lifted with a blank owner password. _New in `document` 1.1.0._

```yaml
output:
  - name: statement
    format: pdf
    template: {file: statement.html}
    for_each: {query: members, key: member_id}
    password: "{{ each.pdf_password }}"        # or "{{ env_var('STATEMENT_PW') }}"
```

-   The password is Jinja like other output values: `each.*` for one per row, `env_var()`, `var()`. A templated password needs DRE 0.7.0 or later; a literal one works on 0.6.
-   An empty password is an error, never an unencrypted file. `html` takes no password: zip it with a password on the destination instead ([zip packs](https://getdre.com/docs/v0.8/password-protected-outputs/)).
-   Keep passwords to ASCII letters, digits and punctuation: macOS Preview and Safari can’t open a PDF whose password has other characters (Acrobat, Chrome, Edge and Firefox can). The run warns.
-   Mark the password’s column in [`secret_columns:`](https://getdre.com/docs/v0.8/password-protected-outputs/#keep-passwords-out-of-dres-records) so it never shows in DRE’s logs, results or previews, and send the password by another route than the file.
-   Only an open password: no print, copy or edit restrictions (readers honour those voluntarily).

## Delivery

File destinations send a `.pdf` as `application/pdf` and an `.html` as `text/html`; an email attachment opens in the recipient’s mail client. To email the document, attach the output to a message with [`attach:`](https://getdre.com/docs/v0.8/messages/#a-headline-plus-the-file).

[Previous  
Databricks](https://getdre.com/docs/v0.8/plugin-databricks/)[Next  
DuckDB](https://getdre.com/docs/v0.8/plugin-duckdb/)
