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

Shipping label rendered by atml_pdf

Installation

Add atml_pdf to your dependencies in mix.exs:

def deps do
  [
    {:atml_pdf, "~> 1.0"}
  ]
end

Quick Start

Command line

Render a template file to PDF directly from the shell:

# 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

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

{: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:

<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>

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
fillfill Consume all remaining space; split equally among fill siblings
fitfit Shrink-wrap to content size

Spacing (padding)

<!-- all sides -->

<!-- top+bottom | left+right -->

<!-- top | right | bottom | left -->

<!-- per-side override -->

Borders

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.

<document font-family="Helvetica" font-size="8pt" font-weight="normal">
  <row>
<!-- inherits family and weight -->

      <row>
<!-- inherits family and 12pt size -->

        </col>
      </row>
    </col>
  </row>
</document>
Attribute Values Default
font-family any font name "Helvetica"
font-size<n>pt8pt
font-weightnormal | boldnormal

Alignment

Attribute Values Default
text-alignleft | center | rightleft
vertical-aligntop | center | bottomtop

Images (<img>)

<img> must be a direct child of <col>. Three src formats are supported:

<!-- 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:

Barcodes

Generate a barcode PNG with Barlix, encode it as a data URI, and pass it as an <img src>:

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)
<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:

{: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)
# 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):

# 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):

# 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:

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`

@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`

@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

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