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 document 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 configured backend)

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 page-width="400pt" page-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.

Multi-page documents

Content that exceeds the page height is automatically split across multiple pages. No changes to your template are required — just define <row> elements as normal and the renderer inserts page breaks as needed.

# Default — auto page split
AtmlPdf.render(xml, "/tmp/report.pdf")

# Trim to a single page (overflow rows are clipped)
AtmlPdf.render(xml, "/tmp/label.pdf", multiple_page: false)

See examples/multi_page_order.xml for a working 40-item order summary that spans multiple A4 pages.

ATML Language

Document structure

Every ATML template has a single <document> root. Layout is expressed as alternating rows and columns:

<document page-width="400pt" page-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>
  └── <break />                ← forces a new page between rows

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] [--single-page]
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)
--single-page no Trim output to a single page; overflow rows are clipped
# 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

# Multi-page order example (40 items, auto page split)
mix atml_pdf.render examples/multi_page_order.xml --backend ExGutenAdapter

# Trim to single page — overflow rows are clipped
mix atml_pdf.render label.xml /tmp/label.pdf --single-page

# 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

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:

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:

Examples

The examples/ directory contains ready-to-render ATML templates:

File Description
examples/shipping_label.xml Single-page shipping label with logo, barcode, and address
examples/shipping_label_multilingual.xml Shipping label with multilingual text (requires ExGutenAdapter)
examples/multi_page_order.xml 40-item order summary spanning multiple A4 pages
examples/two_page_labels.xml Two shipping labels in one document separated by <break />
mix atml_pdf.render examples/shipping_label.xml
mix atml_pdf.render examples/multi_page_order.xml --backend ExGutenAdapter
mix atml_pdf.render examples/two_page_labels.xml

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 backend calls to produce a PDF; handles coordinate-system flip (top-down layout → PDF bottom-left origin) and automatic page splitting

Documentation

File Description
docs/ATML_language_specs.md Full ATML language specification — element reference, attribute tables, dimension rules, and layout semantics
docs/ADAPTER_IMPLEMENTATION.md Guide for implementing a new PDF backend — behaviour callbacks, contract details, and step-by-step instructions

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