Packages

Elixir NIF wrapper for sghtmltopdf, an HTML-to-PDF renderer built on Servo components (html5ever, Stylo, Taffy) — no headless browser required

Current section

Files

Jump to
ex_htmltopdf README.md
Raw

README.md

# ExHtmltopdf
HTML → PDF for Elixir, natively. An Elixir NIF wrapper for
[sghtmltopdf](https://github.com/waka/sghtmltopdf), a PDF rendering engine
written in Rust on Servo components (html5ever, Stylo, Taffy).
- **No headless browser.** No Chrome, no wkhtmltopdf binary, no ports or
System.cmd — the engine runs in-process on a dirty scheduler.
- **Modern CSS.** Flexbox, Grid, custom properties, `@page` rules, repeating
table headers across page breaks.
- **Precompiled.** Ships prebuilt NIFs for macOS/Linux (arm64 + x86_64) via
`rustler_precompiled` — users need no Rust toolchain.
- **Fast.** Upstream benchmarks ~21× faster than wkhtmltopdf and ~53× faster
than headless Chrome on large documents.
```elixir
{:ok, pdf} = ExHtmltopdf.render("<h1>Hello</h1><p>from Elixir</p>")
:ok =
ExHtmltopdf.render_to_file(html, "invoice.pdf",
page_size: "A4",
margin_top: "20mm",
footer_center: "Page [page] of [topage]"
)
```
## Installation
```elixir
def deps do
[
{:ex_htmltopdf, "~> 0.1"}
]
end
```
## Usage
```elixir
# To a binary (send it, store it, no temp files):
{:ok, pdf} = ExHtmltopdf.render(html, page_size: "A4")
# Raising variant:
pdf = ExHtmltopdf.render!(html)
# Straight to a file — written atomically (temp file + rename), a failed
# render never leaves a truncated PDF behind:
:ok = ExHtmltopdf.render_to_file(html, "out.pdf", grayscale: true)
```
### Options
sghtmltopdf exposes one option surface — its CLI flags — shared by the CLI,
HTTP server, Ruby gem, and this library. Options are the flag names as
underscored atoms; values follow a few simple rules
(see `ExHtmltopdf.Options`):
```elixir
page_size: "A4" # --page-size A4
orientation: "landscape" # --orientation landscape
margin_top: "20mm" # --margin-top 20mm (all four sides available)
grayscale: true # bare flag; false/nil omits it
dpi: 300
zoom: 1.25
title: "Q3 Report" # PDF metadata (also author/subject/keywords)
header_center: "Confidential" # simple headers/footers (+ _left/_right)
footer_center: "Page [page] of [topage]"
header_html: "header.html" # full HTML headers/footers
toc: true # table of contents
cover: "cover.html"
user_style_sheet: "print.css" # may repeat: pass a list
minimum_font_size: 9
encoding: "shift_jis"
base_url: "https://example.com/" # resolve relative <img>/<link> paths —
# rendering from a string has no document
# directory, so without this (or an inline
# <base href>) relative assets resolve to
# nothing, silently (see below)
font: "/fonts/NotoSansJP.ttf" # embed fonts; .ttc faces:
font: %{path: "/fonts/Hiragino.ttc", index: 1}
gothic_font: "/fonts/NotoSansJP.ttf" # pin CSS generic families
```
See the [upstream documentation](https://waka.github.io/sghtmltopdf/en/) for
the full flag list — anything the CLI accepts works here, including new flags
after a dependency bump, with no wrapper changes.
Unsupported wkhtmltopdf options (JavaScript execution, forms, …) return a
clear `{:error, %ExHtmltopdf.Error{kind: :usage}}` explaining why, exactly
like the CLI does.
### Errors
Render functions return `{:error, %ExHtmltopdf.Error{kind: kind, message:
message}}` (bang variants raise it). `kind` mirrors the CLI's exit-code
classes: `:usage` (bad options), `:input` (missing file/font, unwritable
output), `:render` (engine constraint), `:timeout`, plus `:panic` for a
contained native bug (the VM survives; please report upstream).
Messages are upstream's text verbatim — often Japanese (clap's parse errors
are English). Match on `kind`, never on `message`.
### Missing assets, warnings, and runaway documents
Three operational behaviors worth knowing before production:
- **Missing assets degrade silently by default.** A broken `<img>`, dead
`@import`, or unreadable font still returns `{:ok, pdf}` — upstream prints
a warning to raw stderr (bypassing `Logger`; not suppressible via options)
and renders without the asset. Pass `load_media_error_handling: "abort"`
to get `{:error, %Error{}}` instead.
- **Renders are uninterruptible.** A render occupies one dirty CPU scheduler
until the engine finishes; the BEAM cannot kill a running NIF, and
`Task.shutdown` abandons the caller without stopping the work. Bound your
concurrency (e.g. a fixed-size `Task.Supervisor` pool) — a deadline option
is planned (PORTING.md phase 3).
- **Concurrent `render_to_file/3` calls are safe**, including to the same
path (atomic per-call temp + rename; last writer wins).
## Rendering untrusted HTML
Two engine policies matter when the HTML isn't yours:
- **Remote assets are off by default.** `<img src="https://…">` and remote
stylesheets are only fetched with `allow_remote_assets: true` (upstream
guards fetches against SSRF/private addresses).
- **Local file access is ON by default** (upstream CLI compatibility):
`<img src="/etc/anything">` and `file://` references read from the host
filesystem. For untrusted input, pass `disable_local_file_access: true`,
or `allow: ["/safe/dir"]` to restrict reads to specific directories.
```elixir
ExHtmltopdf.render(untrusted_html, disable_local_file_access: true)
```
## Phoenix
Render any HTML your app can produce — a controller action for PDF invoices:
```elixir
def invoice(conn, %{"id" => id}) do
html =
Phoenix.Template.render_to_string(MyAppWeb.InvoiceHTML, "show", "html",
invoice: Invoices.get!(id)
)
{:ok, pdf} = ExHtmltopdf.render(html, page_size: "A4")
conn
|> put_resp_content_type("application/pdf")
|> put_resp_header("content-disposition", ~s(attachment; filename="invoice-#{id}.pdf"))
|> send_resp(200, pdf)
end
```
## Development
Requires a Rust toolchain. `EXHTMLTOPDF_BUILD=1` forces building the NIF
from source instead of downloading a precompiled one:
```bash
EXHTMLTOPDF_BUILD=1 mix test # or: just test
just fmt # mix format + cargo fmt
```
The upstream checkout is expected at `../sghtmltopdf`; `UPDATE_PROCEDURE.md`
documents how to bump the pinned revision, and `PORTING.md` the project's
design and staged plan.
## License
MIT. sghtmltopdf itself is MIT (© yo_waka).