README.md

# 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](docs/img/demo.jpg)

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

```elixir
{: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.

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

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

- `<document>` → `<row>` and `<break>` children only
- `<row>` → `<col>` children only
- `<col>` → text, `<img>`, or `<row>` (mixed content allowed)
- A `<col>` cannot be a direct child of another `<col>`
- `<break>` has no attributes or children; ignored in single-page mode

### 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] [--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 |

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

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

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

## 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`)
- `:multiple_page` - When `false`, trims output to a single page and clips overflow rows. Defaults to `true`.
- `: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`)
- `:multiple_page` - When `false`, trims output to a single page and clips overflow rows. Defaults to `true`.
- `:compress` - Enable PDF compression (backend-specific)

## 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 />` |

```bash
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`](docs/ATML_language_specs.md) | Full ATML language specification — element reference, attribute tables, dimension rules, and layout semantics |
| [`docs/ADAPTER_IMPLEMENTATION.md`](docs/ADAPTER_IMPLEMENTATION.md) | Guide for implementing a new PDF backend — behaviour callbacks, contract details, and step-by-step instructions |

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