Mead
Native PDF generation for Elixir, in the style of
react-pdf: 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.
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
~PDFtemplates — function components,attr/slotcompile-time validation,:for/:if, slots; powered by the same engine as HEEx. Or buildMead.Nodetrees directly, no templates involved.- Flexbox layout via 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
(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,
.ttccollections. - Elements — text/spans, boxes, tables (fixed tracks, splittable across pages), images (PNG/JPEG/GIF/WebP), SVG, barcodes/QR (via rxing).
- Document features — links, bookmarks/outline, PDF metadata; output via 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
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). 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:
MEAD_BUILD=1 mix deps.compile mead_pdf --force
Fonts
Mead bundles Liberation Sans (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):
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. 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).