# atml_pdf
An Elixir library that parses **ATML** — an XML-based format for defining label layouts — and renders the result to PDF.
## Overview
ATML describes a single label as a tree of rows and columns. The library runs a three-stage pipeline:
```
ATML XML string
→ AtmlPdf.Parser (XML → element structs)
→ AtmlPdf.Layout (resolve dimensions, font inheritance)
→ AtmlPdf.Renderer (element tree → PDF via the pdf library)
```
## Demo

## Installation
Add `atml_pdf` to your dependencies in `mix.exs`:
```elixir
def deps do
[
{:atml_pdf, "~> 1.0"}
]
end
```
## Quick Start
### Command line
Render a template file to PDF directly from the shell:
```bash
# Output written next to the template (label.pdf)
mix atml_pdf.render label.xml
# Explicit output path
mix atml_pdf.render label.xml /tmp/label.pdf
```
### Render to a file
```elixir
xml = """
<document width="400pt" height="200pt" font-family="Helvetica" font-size="8pt">
<row height="fill">
<col width="fill" vertical-align="center" text-align="center"
font-size="14pt" font-weight="bold">
SHIPPING LABEL
</col>
</row>
</document>
"""
:ok = AtmlPdf.render(xml, "/tmp/label.pdf")
```
### Render to binary
```elixir
{:ok, binary} = AtmlPdf.render_binary(xml)
# binary is a valid PDF you can send over HTTP, write to S3, etc.
```
## ATML Language
### Document structure
Every ATML template has a single `<document>` root. Layout is expressed as alternating rows and columns:
```xml
<document width="400pt" height="600pt" font-family="Helvetica" font-size="8pt">
<row height="60pt" border-bottom="solid 1pt #000000">
<col width="80pt" vertical-align="center" padding="4pt">
<img src="/assets/logo.png" width="60pt" height="40pt" />
</col>
<col width="fill" vertical-align="center" font-size="14pt" font-weight="bold"
text-align="center">
SHIPPING LABEL
</col>
</row>
<row height="fill" border-bottom="solid 1pt #000000">
<col width="50%" padding="6pt" border-right="solid 1pt #000000">
<row height="fit"><col font-weight="bold" font-size="7pt">SENDER</col></row>
<row height="fill"><col padding-top="4pt">John Doe, 123 Street</col></row>
</col>
<col width="fill" padding="6pt">
<row height="fit"><col font-weight="bold" font-size="7pt">RECIPIENT</col></row>
<row height="fill"><col padding-top="4pt">Jane Smith, 456 Avenue</col></row>
</col>
</row>
<row height="28pt">
<col text-align="center" vertical-align="center"
font-size="11pt" font-weight="bold">
VN-123456789-SG
</col>
</row>
</document>
```
### Nesting rules
```
<document>
└── <row>
└── <col>
├── text
├── <img>
└── <row> ← nest rows inside cols to subdivide further
└── <col>
```
- `<document>` → `<row>` children only
- `<row>` → `<col>` children only
- `<col>` → text, `<img>`, or `<row>` (mixed content allowed)
- A `<col>` cannot be a direct child of another `<col>`
### Dimensions
| Value | Example | Meaning |
|---|---|---|
| Points | `100pt` | Fixed size (1 pt = 1/72 inch) |
| Pixels | `120px` | Fixed size (1 px = 0.75 pt) |
| Percentage | `50%` | Relative to parent container |
| `fill` | `fill` | Consume all remaining space; split equally among `fill` siblings |
| `fit` | `fit` | Shrink-wrap to content size |
### Spacing (padding)
```xml
padding="4pt" <!-- all sides -->
padding="4pt 8pt" <!-- top+bottom | left+right -->
padding="2pt 4pt 2pt 4pt" <!-- top | right | bottom | left -->
padding-top="4pt" <!-- per-side override -->
```
### Borders
```xml
border="solid 1pt #000000"
border-bottom="dashed 1pt #cccccc"
border-right="dotted 2px #aaaaaa"
border-top="none"
```
Format: `<style> <width> <color>` where style is `solid`, `dashed`, or `dotted`,
width is `<n>pt` or `<n>px`, and color is `#rrggbb` or `#rgb`.
### Fonts
Font attributes cascade from `<document>` down through all descendants. A child
overrides only the attribute it declares; the rest continue to inherit.
```xml
<document font-family="Helvetica" font-size="8pt" font-weight="normal">
<row>
<col font-size="12pt"> <!-- inherits family and weight -->
<row>
<col font-weight="bold"> <!-- inherits family and 12pt size -->
</col>
</row>
</col>
</row>
</document>
```
| Attribute | Values | Default |
|---|---|---|
| `font-family` | any font name | `"Helvetica"` |
| `font-size` | `<n>pt` | `8pt` |
| `font-weight` | `normal` \| `bold` | `normal` |
### Alignment
| Attribute | Values | Default |
|---|---|---|
| `text-align` | `left` \| `center` \| `right` | `left` |
| `vertical-align` | `top` \| `center` \| `bottom` | `top` |
### Images (`<img>`)
`<img>` must be a direct child of `<col>`. Three `src` formats are supported:
```xml
<!-- Local file path -->
<img src="/path/to/logo.png" width="60pt" height="40pt" />
<!-- Standard data URI (browser / tool default) -->
<img src="data:image/png;base64,iVBORw0KGgo..." width="60pt" height="40pt" />
<!-- Legacy base64 prefix -->
<img src="base64:iVBORw0KGgo..." width="60pt" height="40pt" />
```
Supported MIME types in data URIs: `image/png`, `image/jpeg`, `image/gif`,
`image/webp`.
**Scaling behaviour:**
- One axis fixed, other `fit` → proportional scaling
- Both fixed → stretch to fill (no aspect ratio preservation)
- Both `fit` → intrinsic size
### Barcodes
Generate a barcode PNG with [Barlix](https://hex.pm/packages/barlix), encode it
as a data URI, and pass it as an `<img src>`:
```elixir
barcode_src =
"VN-123456789-SG"
|> Barlix.Code128.encode!()
|> Barlix.PNG.print(xdim: 2, height: 40, margin: 4)
|> then(fn {:ok, iodata} ->
"data:image/png;base64," <> Base.encode64(IO.iodata_to_binary(iodata))
end)
```
```xml
<img src="data:image/png;base64,..." width="300pt" height="40pt" />
```
Barlix supports Code39, Code93, Code128, ITF, EAN13, and UPC-E. Add it to your
deps:
```elixir
{:barlix, "~> 0.6"}
```
## Mix Task
`mix atml_pdf.render` renders an ATML template file to a PDF file without
writing any Elixir code.
```
mix atml_pdf.render TEMPLATE [OUTPUT] [--backend BACKEND]
```
| Argument | Required | Description |
|---|---|---|
| `TEMPLATE` | yes | Path to the ATML XML template file |
| `OUTPUT` | no | Destination PDF path. Defaults to the template path with `.pdf` extension |
| `--backend` | no | PDF backend: `PdfAdapter` (default) or `ExGutenAdapter` (UTF-8) |
```bash
# Minimal — output written as label.pdf in the same directory
mix atml_pdf.render label.xml
# Explicit output path
mix atml_pdf.render templates/label.xml /tmp/output.pdf
# Use ExGuten backend for UTF-8 / multilingual text
mix atml_pdf.render label.xml /tmp/label.pdf --backend ExGutenAdapter
# Absolute paths work too
mix atml_pdf.render /data/templates/label.xml /data/output/label.pdf
```
Exit codes: `0` on success, `1` on any error (missing file, parse failure,
render failure).
## Backend Configuration
atml_pdf uses a pluggable backend system for PDF generation. This allows you to switch between different PDF libraries based on your needs.
### Available Backends
| Backend | Description | UTF-8 Support | Status |
|---|---|---|---|
| `AtmlPdf.PdfBackend.PdfAdapter` | Default backend using the `pdf` hex package. Supports WinAnsi encoding only. | ❌ ASCII + Latin-1 | ✅ Stable |
| `AtmlPdf.PdfBackend.ExGutenAdapter` | ExGuten backend with full UTF-8 support and immutable API. | ✅ Full Unicode | ✅ Available |
### Configuration
**Application-level configuration** (affects all render calls):
```elixir
# config/config.exs
config :atml_pdf,
pdf_backend: AtmlPdf.PdfBackend.PdfAdapter # Default (WinAnsi only)
# Or use ExGuten for UTF-8 support
config :atml_pdf,
pdf_backend: AtmlPdf.PdfBackend.ExGutenAdapter
```
**Runtime override** (per-document):
```elixir
# Use PdfAdapter (WinAnsi encoding)
AtmlPdf.render(xml, path, backend: AtmlPdf.PdfBackend.PdfAdapter)
# Use ExGuten (UTF-8 support)
AtmlPdf.render(xml, path, backend: AtmlPdf.PdfBackend.ExGutenAdapter)
# Or when rendering to binary
{:ok, binary} = AtmlPdf.render_binary(xml, backend: AtmlPdf.PdfBackend.ExGutenAdapter)
```
### Font registration (ExGutenAdapter)
Every `.ttf` file in `priv/fonts/` is registered automatically at startup
using its filename stem as the font name. The bundled fonts are:
| File | Registered as | Coverage |
|---|---|---|
| `NotoSans-Regular.ttf` | `"NotoSans"`, `"NotoSans-Regular"` | Latin, Vietnamese, extended Latin |
| `NotoSansThai-Regular.ttf` | `"NotoSansThai-Regular"` | Thai script |
Drop any additional `.ttf` into `priv/fonts/` and it becomes available as a
`font-family` value in ATML — no code changes required.
For fonts outside `priv/fonts/`, register them via application config:
```elixir
config :atml_pdf, :fonts, [
{"NotoSansCJK-Regular", "/usr/share/fonts/NotoSansCJK-Regular.ttf"}
]
```
All registered TTF fonts are automatically included in the fallback glyph chain,
so characters not covered by the primary font are rendered by the first fallback
that supports them.
### Character encoding
| Backend | Encoding | Supports |
|---|---|---|
| `PdfAdapter` | WinAnsi (ASCII + Latin-1) | English, Western European, common symbols |
| `ExGutenAdapter` | Full UTF-8 | All of the above + Vietnamese, Thai, CJK, Cyrillic, Greek, emoji |
# Or per-document
AtmlPdf.render(xml, path, backend: AtmlPdf.PdfBackend.ExGutenAdapter)
```
## API Reference
### `AtmlPdf.render/3`
```elixir
@spec render(String.t(), Path.t(), keyword()) :: :ok | {:error, String.t()}
```
Parses `template`, resolves layout, and writes the PDF to `path`. Returns `:ok`
on success or `{:error, reason}` on failure.
**Options:**
- `:backend` - PDF backend module (defaults to application config or `PdfAdapter`)
- `:compress` - Enable PDF compression (backend-specific)
### `AtmlPdf.render_binary/2`
```elixir
@spec render_binary(String.t(), keyword()) :: {:ok, binary()} | {:error, String.t()}
```
Same as `render/3` but returns `{:ok, binary}` instead of writing to disk.
**Options:**
- `:backend` - PDF backend module (defaults to application config or `PdfAdapter`)
- `:compress` - Enable PDF compression (backend-specific)
## Pipeline Modules
| Module | Responsibility |
|---|---|
| `AtmlPdf.Parser` | Parses ATML XML into `%Document{}` / `%Row{}` / `%Col{}` / `%Img{}` structs |
| `AtmlPdf.Layout` | Resolves `fill`, `fit`, `%`, `pt`, `px` dimensions; propagates font inheritance; applies min/max constraints |
| `AtmlPdf.Renderer` | Walks the resolved tree and issues `Pdf.*` calls to produce a PDF process; handles coordinate-system flip (top-down layout → PDF bottom-left origin) |
## Development
```bash
# Install dependencies
mix deps.get
# Compile
mix compile
# Run tests
mix test
# Run tests with coverage
mix test --cover
# Format
mix format
# Check formatting (CI)
mix format --check-formatted
```
## License
MIT