Packages

Native PDF generation for Elixir: react-pdf-style ~PDF templates, flexbox layout, and a pure-Rust render backend. No browser, no external binaries.

Current section

Files

Jump to
mead_pdf README.md
Raw

README.md

# Mead
[![Hex.pm](https://img.shields.io/hexpm/v/mead_pdf.svg)](https://hex.pm/packages/mead_pdf)
[![Docs](https://img.shields.io/badge/hexdocs-mead__pdf-blue.svg)](https://hexdocs.pm/mead_pdf)
[![CI](https://github.com/jakeprem/mead/actions/workflows/ci.yml/badge.svg)](https://github.com/jakeprem/mead/actions/workflows/ci.yml)
Native PDF generation for Elixir, in the style of
[react-pdf](https://react-pdf.org/): you write `~PDF` templates (HEEx-flavored
function components with `attr`/`slot` validation), a flexbox engine lays them
out, and a pure-Rust backend draws the PDF. No headless browser, no
wkhtmltopdf, no LaTeX — just a NIF.
```elixir
defmodule MyApp.Report do
use Mead.Component
attr :customer, :string, required: true
attr :rows, :list, required: true
def report(assigns) do
~PDF"""
<view gap="12">
<text font_size="24" font_weight="bold">Quarterly Report</text>
<text color="#5A6178">Prepared for {@customer}. All figures in USD.</text>
<table>
<tr repeat background="#283C86">
<td padding="6"><text color="#FFFFFF">Service</text></td>
<td padding="6"><text color="#FFFFFF" text_align="right">Amount</text></td>
</tr>
<tr :for={{service, amount} <- @rows}>
<td padding="6"><text>{service}</text></td>
<td padding="6"><text text_align="right">{amount}</text></td>
</tr>
</table>
</view>
"""
end
def footer(assigns) do
~PDF"""
<text font_size="8" color="#8A90A5" text_align="right">
Page <page_number/> of <page_count/>
</text>
"""
end
end
pdf =
Mead.render(MyApp.Report.report(%{customer: "ACME", rows: rows}),
margin: 50,
footer: MyApp.Report.footer(%{}),
title: "Quarterly Report"
)
File.write!("report.pdf", pdf)
```
Content that outgrows a page flows onto the next one: tables split with their
header rows repeating (`repeat`), orphan/widow control and
`break_before="avoid"` keep the breaks sensible, and header/footer chrome with
live page counters is painted on every page.
## Features
- **`~PDF` templates** — function components, `attr`/`slot` compile-time
validation, `:for`/`:if`, slots; powered by the same engine as HEEx. Or
build `Mead.Node` trees directly, no templates involved.
- **Flexbox layout** via [Taffy](https://github.com/DioxusLabs/taffy):
`flex_direction`, `gap`, grow/shrink/basis, percent and point dimensions,
padding/margin/border, `background`, `border_radius`.
- **Pagination** — orphans/widows, `break_before`, keep-with-next, repeating
table header rows, per-page header/footer with `<page_number/>` /
`<page_count/>`, safety cap for runaway documents.
- **Real text** — shaping via [Parley](https://github.com/linebender/parley)
(BiDi and complex scripts, dictionary-based line breaking for
Thai/Lao/Khmer/Burmese/Japanese), rich-text spans, font fallback chains, a
lazy per-script fallback pool, `.ttc` collections.
- **Elements** — text/spans, boxes, tables (fixed tracks, splittable across
pages), images (PNG/JPEG/GIF/WebP), SVG, barcodes/QR
(via [rxing](https://github.com/rxing-core/rxing)).
- **Document features** — links, bookmarks/outline, PDF metadata; output via
[krilla](https://github.com/LaurenzV/krilla) with font subsetting.
- **Fast and deterministic** — one NIF call per document; sessions reuse
parsed fonts and warm shaping caches across renders; the same input renders
identically on every machine.
## Installation
```elixir
def deps do
[
{:mead_pdf, "~> 0.1.0"}
]
end
```
Precompiled NIF binaries are downloaded at compile time for common platforms
(Linux glibc/musl, macOS, Windows, on x86_64 and aarch64 — see the
[release artifacts](https://github.com/jakeprem/mead/releases)). No Rust
toolchain needed.
To compile the NIF from source instead (unsupported platform, or you just
prefer to), add `{:rustler, "~> 0.38"}` to your deps and set `MEAD_BUILD=1`:
```shell
MEAD_BUILD=1 mix deps.compile mead_pdf --force
```
## Fonts
Mead bundles [Liberation Sans](https://github.com/liberationfonts/liberation-fonts)
(regular/bold/italic/bold-italic, SIL OFL) so rendering works out of the box —
including in slim container images that ship no fonts at all. Fonts are
subset into the output, so PDFs stay small.
Declare your own fonts per family, with an optional fallback chain and a
lazy pool for broad script coverage (e.g. the Noto family):
```elixir
session =
Mead.Session.new(
Mead.FontSet.new(
%{
"Inter" => [
%{path: "priv/fonts/Inter-Regular.ttf"},
%{path: "priv/fonts/Inter-Bold.ttf", weight: 700}
],
"Noto Sans SC" => [%{path: "priv/fonts/NotoSansSC-Regular.otf"}]
},
fallback: ["Noto Sans SC"],
pool: ["/usr/share/fonts/google-noto"]
)
)
Mead.render(doc, session: session)
```
Reuse the session for repeated rendering — fonts parse once and shaping
caches stay warm.
## Performance
`bench/readme_bench.exs` renders a 9-page tabular report (repeating table
header, page footer with counters) through a warm session:
| | |
|---|---|
| Single-threaded | **14 ms/doc** median (p99 20 ms) |
| Concurrent (12 schedulers) | **~420 docs/sec** |
Measured on an AMD Ryzen 5 7640U laptop (Linux, 12 threads); run the script
yourself for numbers on your hardware. Rendering runs on dirty schedulers,
so it won't starve the rest of your application.
## Documentation
Full documentation is on [HexDocs](https://hexdocs.pm/mead_pdf). The
`examples/` directory renders a realistic invoice, a template-driven report,
and more (`just example invoice`).
## License
MIT. The Rust dependency tree is entirely permissive
(MIT/Apache-2.0/BSD/Zlib/ISC, plus Unicode-3.0 for the ICU4X segmentation
data compiled into the binary). Bundled Liberation Sans fonts are under the
SIL Open Font License (see `priv/fonts/LiberationSans-LICENSE`).