Metadata-Version: 2.5
Name: klartex
Version: 0.18.0
Summary: PDF generation via LaTeX — structured data in, professional documents out
Project-URL: Homepage, https://klartex.se
Project-URL: Repository, https://github.com/swedev/klartex
License-Expression: MIT
License-File: LICENSE
Keywords: document-generation,invoice,jinja2,json-schema,latex,pdf
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Topic :: Office/Business
Classifier: Topic :: Printing
Classifier: Topic :: Text Processing :: Markup :: LaTeX
Requires-Python: >=3.12
Requires-Dist: jinja2>=3.1.0
Requires-Dist: jsonschema>=4.23.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: typer>=0.15.0
Provides-Extra: dev
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: pytest-xdist>=3.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Provides-Extra: serve
Requires-Dist: fastapi>=0.115; extra == 'serve'
Requires-Dist: uvicorn[standard]>=0.34; extra == 'serve'
Description-Content-Type: text/markdown

> **English version:** [README.en.md](README.en.md)

# Klartex

PDF-generering via LaTeX — strukturerad data in, professionella dokument ut.

[klartex.se](https://klartex.se) · [PyPI](https://pypi.org/project/klartex/) · [GitHub](https://github.com/swedev/klartex)

Klartex tar JSON-data + mallnamn och producerar PDF via XeLaTeX. Kan användas som Python-bibliotek eller CLI-verktyg.

## Mallar

| Mall | Beskrivning |
|------|-------------|
| `_block` | Universell blockmotor — agenten komponerar dokumentet fritt |
| `protokoll` | Mötesprotokoll med dagordning, beslut och justerare |
| `faktura` | Faktura med rader, moms och betalningsinformation |
| `kvitto` | Kvitto med enkel radlista, betalsätt och totalbelopp |
| `resultatrakning` | Resultaträkning med jämförelseår och noter |
| `balansrakning` | Balansräkning med tillgångar och skulder/eget kapital |
| `budgetrapport` | Budgetrapport med kontokoder, budget och utfall |
| `sie-exportrapport` | Läsbar PDF av SIE4-bokföringsdata |

## Installation

```bash
# Som globalt CLI-verktyg
pipx install klartex

# Eller i ett projekt
pip install klartex
```

Kräver Python ≥ 3.12 och XeLaTeX.

```bash
# macOS
brew install --cask mactex

# Debian/Ubuntu
sudo apt install texlive-xetex texlive-fonts-recommended \
  texlive-latex-extra texlive-latex-recommended texlive-science texlive-plain-generic
```

Paketuppsättningen för Debian/Ubuntu är en snabb approximation av renderingsmiljön. Den exakta listan över TeX Live-paket som behövs finns i `.github/tl_packages` — det är vad CI installerar, och med BasicTeX eller en minimal TeX Live räcker `tlmgr install $(grep -v '^#' .github/tl_packages)`. `texlive-xetex` ensamt räcker inte — bland annat `ulem` (i `texlive-plain-generic`), `tcolorbox` och `siunitx` behövs för att rendera.

### Färdig renderingsmiljö (containerimage)

Den miljö klartex släpps mot publiceras som `ghcr.io/swedev/klartex-base`: full TeX Live, en garanterad teckensnittsuppsättning och den Python-runtime som behövs för att installera paketet. Tjänster som renderar med klartex bygger vidare på den i stället för att återskapa apt-listan.

Garanterade familjer — det `page_template.font` och `page_template.header_font` kan sättas till utan att veta något om maskinen som renderar:

Arial, Courier New, Georgia, Times New Roman, Trebuchet MS, Verdana, EB Garamond, IBM Plex Mono, IBM Plex Sans, IBM Plex Serif, Inter, Lato, Noto Sans, Noto Serif, Open Sans, Roboto.

Den auktoritativa listan är schemabeskrivningarna för `font` och `header_font` (`klartex schema _block`); imagebygget faller om någon familj saknas. Andra fontspec-namn fungerar bara där teckensnittet råkar vara installerat.

Ett teckensnitt utanför listan kan i stället följa med anropet: `font` och `header_font` tar också ett objekt med filnamn — `{"file": "Inter-Regular.ttf", "bold": "Inter-Bold.ttf", "italic": "Inter-Italic.ttf", "bold_italic": "Inter-BoldItalic.ttf"}`. Filerna slås upp i `asset_dir` (över `klartex serve`: i anropets `assets`), och bara `file` krävs — ett snitt vars fil inte skickats med renderas i det ordinarie snittet. Filnamnet är ett rent filnamn som slutar på `.ttf` eller `.otf`, utan understreck eller andra LaTeX-tecken.

```dockerfile
FROM ghcr.io/swedev/klartex-base:<tagg>@sha256:<digest>
```

Pinna alltid tagg **och** manifest-digest — det finns ingen `latest`-tagg. Imagen byggs av `.github/workflows/base-image.yml` från `docker/Dockerfile.base`, och hela testsviten körs inuti den färdigbyggda amd64-imagen innan något publiceras — en image som klartex inte renderar i når aldrig registret.

Samma image är också releasegrind: `.github/workflows/publish.yml` kör hela testsviten inuti den pinnade imagen innan paketet byggs, så varje version som publiceras på PyPI har passerat i renderingsmiljön.

## Användning

### Som Python-bibliotek

```python
from klartex import render

pdf_bytes = render("protokoll", data)
```

### Som CLI

```bash
# Rendera (block engine är default)
klartex -d data.json

# Pipe JSON via stdin
cat data.json | klartex

# Med explicit mall
klartex -d data.json -t protokoll

# Med egen sidmall (hela sidan i en fil)
klartex -d data.json --page-template sidmall.tex.jinja

# Med egen sidmall (en slot i taget)
klartex -d data.json --header-template sidhuvud.tex.jinja

# Lista mallar
klartex templates

# Visa JSON Schema för en mall
klartex schema protokoll
```

### Som HTTP-tjänst (`klartex serve`)

Samma renderare bakom en liten HTTP-yta: `POST /render` (JSON in, PDF ut) och `GET /health`. Ligger bakom extran `serve`.

```bash
pip install 'klartex[serve]'
klartex serve --host 127.0.0.1 --port 8000
```

Mall, data och eventuella mallkällor — `page_template_source` för hela sidan, `header_source`/`footer_source` per slot — går i samma JSON-objekt. Assets följer med som base64 och skrivs till en temporärkatalog som lever precis så länge anropet gör det.

```json
{
  "template": "_block",
  "data": {"body": [{"type": "heading", "text": "Hej"}]},
  "header_source": "\\fancyhead[R]{\\includegraphics[height=1cm]{logo.png}}",
  "assets": {"logo.png": "<base64>"}
}
```

Svaret är `application/pdf`, eller ett fel vars `detail.type` är `input_error`, `validation_error`, `payload_too_large`, `render_error` eller `overloaded`. Schema- och blockfel bär dessutom `detail.path` — en lista som `["body", 1, "items", 0, "text"]` som pekar ut noden som fallerade.

| Miljövariabel | Default | Betydelse |
|---------------|---------|-----------|
| `KLARTEX_MAX_CONCURRENT` | `2` | Samtidiga xelatex-körningar. Fler samtidiga anrop får `503` med `Retry-After`. |
| `KLARTEX_MAX_BODY_MB` | `80` | Största begäran som läses. Kontrollen sker på `Content-Length` innan kroppen läses, så gränsen gäller den storlek anroparen uppger. |

Tjänsten har varken autentisering eller rate limiting — den är ett kompileringslager och ska stå bakom en anropare som äger båda. Därför binder den till `127.0.0.1` om inget annat anges. Ett `latex`-block i indata kör godtycklig LaTeX i renderingsprocessen; kör tjänsten avskild från allt som inte tål det.

### Renderingstjänsten som image

Varje release publicerar också `ghcr.io/swedev/klartex-render:X.Y.Z` — samma pinnade bas som releasegrinden testar i, med releasens wheel-paket installerat. Taggen är alltid lika med klartex-versionen, och det finns ingen `latest`: pinna den version som motsvarar din `klartex==`-pin.

```bash
docker run --rm -p 127.0.0.1:8000:8000 \
  --read-only --tmpfs /tmp --tmpfs /home/render \
  ghcr.io/swedev/klartex-render:X.Y.Z
```

Imagen kör som icke-root och binder till `0.0.0.0` inuti containern — publicera porten bara på det nät anroparen finns på.

## Sidmallar (Page Templates)

En sidmall består av två oberoende delar: **header** (sidhuvud) och **footer** (sidfot). Varje del väljs för sig — en färdig variant, ett objekt med uppgifterna som ska stå där, eller `null` för tomt. Strukturerade inställningar fortsätter gälla för den del som är fördefinierad, även när den andra delen har egen LaTeX.

| Slot | Variant | Innehåll |
|------|---------|----------|
| `header` | `letterhead` | Organisationsuppgifter till vänster, logotyp till höger |
| `header` | `logo` | Enbart logotyp till höger |
| `header` | `null` | Tomt sidhuvud — sidhuvudets utrymme återtas |
| `footer` | `pagenumber` | Sidnummer centrerat, valfritt med dokumenttiteln före (`title`) |
| `footer` | `columns` | Flerkolumnsfot med företags-, kontakt- och betalningsuppgifter (`fields`) |
| `footer` | `null` | Tom sidfot |

En del som utelämnas får ytans default: blockmotorn har tomt sidhuvud och sidnummerfoten, recepten letterhead-sidhuvudet och sidnummerfoten med dokumenttiteln före sidnumret (`footer: {"variant": "pagenumber", "title": true}`).

```json
"page_template": {
  "header": {
    "variant": "letterhead",
    "fields": {
      "org_name": "Min Förening",
      "address": "Storgatan 1, 123 45 Stad",
      "web": "minforening.se",
      "email": "styrelsen@minforening.se",
      "logo": "logo.pdf"
    }
  },
  "footer": {
    "variant": "columns",
    "fields": {
      "company": "Min Förening",
      "org_number": "802000-0000",
      "bankgiro": "1234-5678"
    }
  }
}
```

```json
"page_template": { "header": "logo", "footer": null }
```

Objektformen av `letterhead` kräver `fields.org_name` — namnet är det som sidhuvudet byggs runt, och utan det skulle övriga uppgifter inte skrivas ut. Ett sidhuvud helt utan uppgifter anges som variantnamnet självt (`"header": "letterhead"`). `logo` är ett filnamn utan LaTeX-specialtecken (`\ # $ % & _ { } ~ ^`).

Utöver sloten finns inställningar på dokumentnivå — `font`, `header_font`, `diff_style` och `margins` — som gäller oavsett om en slot har egen LaTeX, plus `page_numbers` och `first_page_header`.

### Marginaler

`margins` anger avståndet från papperskanten till **brödtexten**, en nyckel per sida. Varje nyckel är valfri och verkar för sig; värdet är ett LaTeX-mått med utskriven enhet (`cm`, `mm`, `pt`, `in`).

```json
"page_template": {
  "margins": { "top": "3.4cm", "bottom": "2cm", "left": "3cm", "right": "3cm" }
}
```

Chromet anpassar sig efter måtten i stället för tvärtom: `top` mäts till första textraden, så med ett sidhuvud står bandet kvar där det står och glappet mellan sidhuvud och text växer eller krymper — därför måste `top` överstiga 2,1 cm, där bandet slutar. Är sidhuvudet tomt (eller saknar innehåll) återtas dess utrymme och vilket positivt `top` som helst fungerar. `bottom` mäts till sista textraden och sidfoten hänger under den, så lämna plats åt den — ett litet värde klipper foten. `left` och `right` flyttar även sidhuvudets och sidfotens band, som följer textbredden.

En slot med egen LaTeX som sätter sin egen geometri vinner över `margins`, precis som den gör över `font`.

### Egen sidmall

Rå LaTeX skickas som fil eller text, inte i JSON. En fil kan äga hela sidan, eller en slot i taget:

```bash
klartex -d data.json --page-template sidmall.tex.jinja
klartex -d data.json --header-template sidhuvud.tex.jinja
klartex -d data.json --header-template sidhuvud.tex.jinja --footer-template sidfot.tex.jinja
```

```python
render("_block", data, page_template_source=Path("sidmall.tex.jinja").read_text())
render("_block", data, header_source=Path("sidhuvud.tex.jinja").read_text())
```

`--page-template` äger båda slotarna och kan inte kombineras med slot-flaggorna. Slot-filerna måste ligga i samma katalog — den katalogen blir mallkatalogen som filer hittas relativt till.

Utan mallflagga letar klartex själv: först `<data-filens-stam>.tex.jinja` bredvid datafilen, sedan `page_template.tex.jinja` i arbetsmappen. Hittas en sådan fil används den som helsidesmall, och sökvägen skrivs på stderr. En slot-flagga stänger av autodetekteringen.

Inställningarna på dokumentnivå i `data.page_template` (`font`, `header_font`, `diff_style`, `margins`) gäller oavsett form och skrivs ut före mallens egen LaTeX, så mallens `\geometry` och `\setmainfont` vinner. `header` och `footer` i JSON läses inte när en helsidesmall är satt.

En mallfil definierar sitt eget chrome:

```latex
\definecolor{brandprimary}{HTML}{2E5A1C}
\definecolor{brandsecondary}{HTML}{555555}
\renewcommand{\orgname}{Min Förening}
\fancyhead[L]{\fontsize{6pt}{9pt}\selectfont\textbf{\orgname}}
\fancyhead[R]{\includegraphics[height=0.855cm]{logo.pdf}}
```

```latex
\makeatletter
\fancyfoot[C]{%
    \kx@setlang%
    \fontsize{6pt}{9pt}\selectfont\color{brandsecondary}%
    \doctitle\ \textbullet\ \kx@page\ \thepage\ \kx@of\ \pageref{LastPage}%
}
\makeatother
```

Dessa makron är kontraktet mellan sidmallen och dokumentklassen och kan skrivas om i preamblens toppnivå: `\orgname`, `\orgaddress`, `\orgwebsite`, `\orgemail`, `\orgphone`, `\brandlogo`. Klassen definierar dem tomma, så använd `\renewcommand`. Sidhuvudets utrymme återtas i slutet av preamblen om `\orgname` och `\brandlogo` båda är tomma — ett värde som sätts senare (t.ex. i `\AtBeginDocument`) hinner inte med det testet.

Delarna skrivs ut i fast ordning: inställningar på dokumentnivå, sidhuvud, sidfot, återtaget utrymme. En egen slot bör därför inte röra den andra slotens `\fancyhead`/`\fancyfoot`-celler.

Var logotyper och andra filer hittas skiljer sig mellan de två ytorna:

- **CLI med filbaserad sidmall** (`--page-template`, `--header-template`, `--footer-template`, samt en autodetekterad mall): filer hittas relativt till mallfilens egen katalog, med arbetsmappen som fallback. En mall och dess logotyper kan därmed ligga samlade i t.ex. en `Branding/`-mapp och användas från vilken arbetsmapp som helst. För en symlänkad fil gäller målets katalog.
- **API med `page_template_source`, `header_source` eller `footer_source`**: parametrarna tar rå text utan sökväg, så det finns ingen mallkatalog att utgå från. Anropare som vill hitta filer utanför arbetsmappen skickar `asset_dir=<katalog>` till `render()`; annars gäller arbetsmappen.

> **Både `\includegraphics{logo.pdf}` och `\includegraphics{./logo.pdf}` fungerar**, liksom `\input{../delat/farger.tex}` — relativa referenser utgår från mallens katalog (eller `asset_dir`, i annat fall arbetsmappen). En skillnad finns dock: namn med `./` eller `../` faller **inte** tillbaka på arbetsmappen. TeX:s filsökning (Kpathsea) söker aldrig upp sådana namn, utan provar dem rakt av mot xelatex arbetskatalog — och den katalogen är just mallens katalog. Namn utan prefix söks däremot i hela kedjan och hittas även om filen bara ligger i arbetsmappen.

## Arkitektur

Klartex har en trelagers-arkitektur:

1. **Dokumentnivå** — `klartex-base.cls` hanterar siduppställning och grundläggande sidhuvud/sidfot. Sidmallar (`.tex.jinja`) injiceras i preambeln och styr färger, logotyp och layout.
2. **Komponentnivå** — Återanvändbara `.sty`-paket som ger strukturerade LaTeX-makron (t.ex. `klartex-signatureblock.sty`, `klartex-klausuler.sty`, `klartex-agenda.sty`)
3. **Receptnivå** — YAML-filer som deklarerar vilka komponenter och innehållsfält som ska kombineras

### Renderingsvägar

- **Recipe-mallar** (`protokoll`, `faktura`, `kvitto`) — YAML-recept som deklarerar komponenter och mappningar
- **Block engine** (`_block`) — Agenten komponerar `body[]` fritt från typade block

### Skapa en YAML-receptmall

Skapa en `recipe.yaml` i mallens katalog (t.ex. `klartex/templates/min-mall/recipe.yaml`):

```yaml
template:
  name: min-mall
  description: "Beskrivning av mallen"
  lang: sv

document:
  title: "{{ data.title }}"
  metadata:
    - label: "Datum:"
      field: date

components:
  - type: klausuler
    data_map:
      items: agenda_items
    options:
      item_title_field: title
      item_body_field: body

schema: schema.json
```

Tillgängliga recept-komponenter: `heading`, `description_list`, `agenda`, `text`, `resultatrakning`, `budgettabell`, `notapparat`, `invoice_header`, `invoice_recipient`, `invoice_table`, `payment_info`, `invoice_note`, `receipt_header`, `receipt_table`. Block-motsvarigheterna (`agenda`, `description_list`, `heading`, `resultatrakning`, `budgettabell`, `notapparat`, `text`) renderas via samma delade makron som block-engine-vägen.

Block engine-block: `heading`, `text`, `list`, `table`, `callout`, `quote`, `title_page`, `parties`, `clause`, `signatures`, `description_list`, `form`, `columns`, `agenda`, `name_roster`, `resultatrakning`, `budgettabell`, `notapparat`, `page_break`, `latex`.

## Årsmötespaket

Blockmotorn kan komponera alla dokument som behövs för ett föreningsårsmöte:

| Dokument | Blocktyper |
|----------|-----------|
| Kallelse + dagordning | heading, description_list, agenda |
| Verksamhetsberättelse | heading, name_roster, text, signatures |
| Ekonomisk årsredovisning | heading, text, resultatrakning, notapparat, signatures |
| Revisionsberättelse | heading, text, signatures |
| Budget | heading, budgettabell |
| Valberedningens förslag | heading, name_roster, signatures |
| Motion | heading, text, clause, signatures |
| Styrelsens yttrande | heading, text, signatures |

Agenten väljer och ordnar block för varje dokument — inga separata mallar behövs. Se `tests/fixtures/block_kallelse.json` m.fl. för fullständiga exempel.

## Licens

MIT
