Skip to content
You're reading the docs for DRE 0.6 (v0.6.1). This page for 0.11

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
plugins:
- document

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.

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.

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.

reports/clients/statement/statement.yml
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
{% 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 %}
<!-- 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.

Everything a message 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
locale the run’s 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: 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 ($#,##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, 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.

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 %}.

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 template. A Set can override template.file like any other output key, so one client can get a different layout.

<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:

@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.

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.

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.

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.

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

Terminal window
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), 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.

The PDF is laid out by 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:

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

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:.