Packages
Pure Elixir utilities for PDF inspection, metadata, tokenizing, merging, page transforms, splitting, stamping, forms, attachments, outlines, text extraction, and native HTML/CSS rendering.
Current section
Files
Jump to
Current section
Files
docs/html-to-pdf-examples.md
# Create PDFs from HTML
Use `NativeElixirPdfUtilities.HtmlToPdf` to render HTML into PDF bytes or a file.
The examples below reuse the alias from the first example. Supply your own
files where paths are shown. See [HTML and CSS support](html-to-pdf-compatibility.md)
for supported features and options.
## Render HTML
`render/2` returns PDF bytes:
```elixir
alias NativeElixirPdfUtilities.HtmlToPdf
html = "<h1>Invoice</h1><p>Amount due: 200.00</p>"
{:ok, pdf} = HtmlToPdf.render(html, page_size: :a4, margin: "18mm")
File.write!("/tmp/invoice.pdf", pdf)
```
### Add a styled table
```elixir
html = """
<style>
table { width: 100%; border-collapse: collapse; }
th, td { border: 1pt solid #cccccc; padding: 5pt; text-align: left; }
</style>
<table>
<thead><tr><th>Item</th><th>Amount</th></tr></thead>
<tbody><tr><td>Printing</td><td>200.00</td></tr></tbody>
</table>
"""
{:ok, pdf} = HtmlToPdf.render(html, margin: "18mm")
```
### Paginate a table with row groups
Use one `<thead>` for repeating column headings and one `<tbody>` for each
group of related rows. Apply `break-inside: avoid` to the row groups, then render
the entire table in one call. Do not split the rows into page-sized batches.
This runnable example has a small Printing group and a Delivery group with
60 rows. The Delivery group is deliberately too tall for one A4 page.
```elixir
alias NativeElixirPdfUtilities.HtmlToPdf
delivery_rows =
Enum.map_join(1..60, "", fn number ->
"<tr><td>Delivery</td><td>Package #{number}</td><td>1</td></tr>"
end)
html = """
<style>
table { width: 100%; border-collapse: collapse; font-size: 10pt; }
th, td { border: 1pt solid #cccccc; padding: 6pt; text-align: left; }
tbody { break-inside: avoid; }
</style>
<table>
<thead>
<tr><th>Group</th><th>Item</th><th>Quantity</th></tr>
</thead>
<tbody>
<tr><td>Printing</td><td>Brochures</td><td>200</td></tr>
<tr><td>Printing</td><td>Posters</td><td>20</td></tr>
</tbody>
<tbody>
#{delivery_rows}
</tbody>
</table>
"""
{:ok, pdf} = HtmlToPdf.render(html, page_size: :a4, margin: "18mm")
File.write!("/tmp/grouped-table.pdf", pdf)
```
The Printing rows stay together. The Delivery group spans multiple pages,
with Group, Item, and Quantity repeated above its rows and a bottom border
on each page. Package numbers remain in order from 1 to 60.
See [table page breaks](html-to-pdf-compatibility.md#table-page-breaks) for
sizing rules and limitations.
## Render a file
`render_file/3` reads the HTML and writes the PDF:
```elixir
:ok =
HtmlToPdf.render_file(
"priv/static/templates/invoice.html",
"/tmp/invoice.pdf",
page_size: :a4,
margin: "18mm",
stylesheets: [{:file, "priv/static/templates/invoice.css"}],
base_url: "priv/static"
)
```
Use `{:css, css}` for inline CSS and `{:file, path}` for a stylesheet file.
The renderer does not infer `:base_url` from the input file's directory. Reading
and writing errors use the [diagnostic contract](diagnostics.md).
## Add headers, footers, and page numbers
Use `:page_furniture` for header and footer templates. Leave enough page
margin for them:
```elixir
{:ok, pdf} =
HtmlToPdf.render(
"<h1>Account statement</h1><p>Statement content...</p>",
margin: "18mm",
page_furniture: [
header: [
default: "<div style=\"font-size: 8pt\">Account statement</div>",
first: false
],
footer:
"<div style=\"font-size: 8pt; text-align: right\">" <>
"Page {{page}} of {{pages}}</div>"
]
)
```
Use `:first`, `:odd`, `:even`, and `:default` variants when pages need
different headers or footers. A `false` or `nil` variant disables that template on the
matching page. To number an existing PDF, use [Stamp.page_numbers/2](https://github.com/Cees-Kettenis/native_elixir_pdf_utilities/blob/main/docs/pdf-stamping.md#page-numbers).
## Load local images
Set `:base_url` when the document refers to local images or fonts. Referenced
paths must stay beneath that directory.
```elixir
{:ok, pdf} =
HtmlToPdf.render(
"""
<div style="width: 90mm; border: 1pt solid #111; padding: 4mm">
<img src="images/product.png" alt="Product" style="width: 22mm">
<p>001764 - DEFAULT</p>
</div>
""",
base_url: "priv/static"
)
```
Map document references to bytes to avoid filesystem reads for those assets,
or to trusted files to authorize specific paths:
```elixir
HtmlToPdf.render(html,
assets: %{
"product-image" => {:bytes, png_bytes},
"report-font" => {:file, "priv/fonts/report-sans.ttf"}
}
)
```
The HTML or CSS can use those exact references in `src` or `url(...)`.
For caller-managed asset loading, supply a resolver:
```elixir
resolver = fn
%{reference: "company-logo", kind: :image} -> {:ok, png_bytes}
_request -> :not_found
end
{:ok, pdf} =
HtmlToPdf.render(~s(<img src="company-logo" style="width: 30mm">),
asset_resolver: resolver
)
```
See [Local files and assets](html-to-pdf-compatibility.md#assets-and-local-files)
for the path and callback rules.
## Register a font
Pass a static TrueType font when output must use the same face on every host:
```elixir
{:ok, pdf} =
HtmlToPdf.render(
~s(<p style="font-family: 'Report Sans'">Café</p>),
fonts: [%{family: "Report Sans", path: "priv/fonts/report-sans.ttf"}],
system_font_discovery: false
)
```
Use `data: ttf_bytes` instead of `path:` for an in-memory font. Font fallback
and embedding restrictions are covered in [Fonts and text](html-to-pdf-compatibility.md#fonts-and-text).
A document can also load a font beneath `:base_url` with `@font-face`:
```elixir
{:ok, pdf} =
HtmlToPdf.render(
"""
<style>
@font-face {
font-family: "Report Sans";
src: url("fonts/report-sans.ttf") format("truetype");
}
body { font-family: "Report Sans"; }
</style>
<p>Café</p>
""",
base_url: "priv/static"
)
```
## Set PDF metadata
```elixir
{:ok, pdf} =
HtmlToPdf.render(
"<title>Monthly statement</title><p>Statement content</p>",
metadata: [
author: "Finance Operations",
subject: "Customer statement",
keywords: ["statement", "monthly"],
creation_date: Date.utc_today()
]
)
```
The first non-empty HTML `<title>` becomes the PDF title unless
`metadata[:title]` is set. To change metadata in an existing PDF, use
[Info.put/2](pdf-information.md#updating-information).
## Create bookmarks from headings
Visible HTML headings can become nested items in the PDF viewer's bookmarks
panel:
```elixir
{:ok, pdf} =
HtmlToPdf.render(
"""
<h1>Annual report</h1>
<h2>Financial results</h2>
<h3>Revenue</h3>
<h3>Expenses</h3>
""",
outlines: :headings
)
```
See [PDF outlines and bookmarks](pdf-outlines.md) for exact outline input,
automatic detection in existing PDFs, and preservation behavior.
## Render static form values
Form controls become visible, non-editable PDF content:
```elixir
html = """
<div>
<input type="text" value="Amira Tan">
<input type="checkbox" checked>
<select><option selected>Approved</option></select>
<textarea>Documents verified</textarea>
<button type="button">Record application</button>
</div>
"""
{:ok, pdf} = HtmlToPdf.render(html)
```
## Handle an error
Rendering uses the library's shared diagnostic result:
```elixir
require Logger
case HtmlToPdf.render(html) do
{:ok, pdf} ->
File.write!("/tmp/document.pdf", pdf)
{:error, {reason, diagnostic}} ->
Logger.warning(
"PDF render failed with #{reason}: #{diagnostic.message}"
)
end
```
Use the reason for program flow and the diagnostic for logs or template fixes.
See [Diagnostics](diagnostics.md) for the full contract.