# PDFGenerator Template & Usage Guide

Design polished PowerPoint and Word reports, populate them from JSON, and preview the
result as a PDF. This handbook covers the full workflow: starting the services,
structuring data, writing tags, building native charts, replacing pictures, using the
web interface, calling the API, and diagnosing template problems.

> **Scope:** This guide documents the features implemented in this repository. The tag
> language is inspired by Carbone, but PDFGenerator is not a complete reimplementation
> of every Carbone feature. Check [Compatibility and current limits](#compatibility-and-current-limits)
> before designing an advanced template.

| Start here | Best for |
| --- | --- |
| [Five-minute first report](#five-minute-first-report) | Creating a working report quickly |
| [Tag language](#tag-language) | Looking up substitutions and formatters |
| [Native charts](#native-charts) | Populating PowerPoint or Word charts |
| [Dynamic images](#dynamic-images) | Replacing picture placeholders |
| [Report design recipes](#report-design-recipes) | Planning a professional layout |
| [Troubleshooting](#troubleshooting) | Resolving warnings or failed output |

## Getting started

### Service map

PDFGenerator uses three local services. Each has one job and one stable default port.

| Service | Port | Start command | Open in a browser |
| --- | ---: | --- | --- |
| FastAPI backend | `9010` | `python main.py` | `http://localhost:9010/docs` |
| Next.js application | `9011` | `npm run start` from `frontend` | `http://localhost:9011` |
| This guide | `9012` | `python guide.py` | `http://localhost:9012` |

The backend populates the Office template and runs the selected PDF converter. The
frontend sends the uploaded template and JSON to the backend, then displays the PDF
inline. The guide service renders this Markdown file as the page you are reading.

### Prerequisites

- Python 3.11 or later is recommended.
- Node.js and npm are required for the Next.js interface.
- On Windows x86-64, the backend can provision its managed LibreOffice runtime on the
  first start. The download is large and happens only once.
- On Linux or macOS, provide a LibreOffice executable in `config.py` or place a prepared
  runtime under `.runtime/libreoffice`.

### Install and start

From the repository root, install and start the backend:

```powershell
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
python main.py
```

Open another terminal for the application:

```powershell
cd frontend
npm install
npm run start
```

Open a third terminal for this documentation site:

```powershell
python guide.py
```

Then open `http://localhost:9011` to generate documents and
`http://localhost:9012` to read the guide.

> **First-run note:** The backend becomes reachable while the managed LibreOffice
> runtime is still preparing. Watch the renderer status in the application or request
> `GET http://localhost:9010/api/converter/health` before the first generation.

### Download the supplied example

The documentation service exposes both committed sample files:

- [Download the Tracker PowerPoint template](/downloads/TrackerFS_Carbone.pptx)
- [Download its sample JSON](/downloads/sample-data.json)

These are the best references for split text tags, native chart workbooks, empty-value
handling, and realistic one-page report design.

## Five-minute first report

This example creates a minimal customer summary. It works in either PowerPoint or Word.

### 1. Prepare the JSON

```json
{
  "ReportTitle": "Customer Summary",
  "AsAtDate": "2026-06-30",
  "Customer": {
    "Name": "Ada Lovelace",
    "AccountNumber": "INV-1042"
  },
  "PortfolioValue": 1250000.5
}
```

### 2. Design the Office template

Create the complete visual layout in PowerPoint or Word. Add these tags wherever the
values should appear:

```text
{d.ReportTitle}
Prepared for {d.Customer.Name}
Account {d.Customer.AccountNumber}
As at {d.AsAtDate:formatD('DD MMMM YYYY')}
Value R{d.PortfolioValue:formatN(2, '.', ' ')}
```

The generated text is:

```text
Customer Summary
Prepared for Ada Lovelace
Account INV-1042
As at 30 June 2026
Value R1 250 000.50
```

### 3. Generate the PDF

1. Open `http://localhost:9011`.
2. Choose the `.pptx` or `.docx` template.
3. Paste the JSON into the editor.
4. Keep **Auto preview** enabled or click **Generate PDF**.
5. Review the PDF in the right-hand preview panel.
6. Read any template warnings shown below the editor.

The web app sends the template and JSON together. The backend creates a populated copy,
converts that copy to PDF, and returns the PDF inline; it does not modify the template
on disk.

## How generation works

```text
JSON object + PPTX/DOCX template
              │
              ▼
       OOXML template engine
       ├─ replaces text tags
       ├─ expands supported loops
       ├─ updates embedded chart workbooks
       └─ replaces tagged pictures
              │
              ▼
     LibreOffice or Gotenberg
              │
              ▼
        PDF shown in browser
```

PowerPoint and Word files are ZIP-based Office Open XML packages. PDFGenerator changes
the relevant XML, embedded workbook, and media parts while retaining the template's
native masters, styles, charts, and page geometry. LibreOffice is responsible for the
final Office-to-PDF layout pass.

### Supported input and output

| Item | Current behavior |
| --- | --- |
| Template input | `.pptx` and `.docx` |
| JSON input | One JSON object at the root |
| Browser output | PDF displayed inline |
| Debug output | Populated `.pptx` or `.docx` from `/api/render-template` |
| Default converter | Managed local LibreOffice |
| Optional converter | Authenticated Gotenberg selected in `config.py` |
| Template size | Up to 50 MB by default |
| Image size | Up to 15 MB decoded/downloaded by default |

## Design a reliable JSON payload

The template should be a view of the data, not the place where business logic lives.
Prepare totals, labels, and domain-specific decisions before sending the JSON whenever
possible.

### Recommended structure

Group related values, keep collection names plural, and use ISO dates plus native JSON
numbers for calculations and charts.

```json
{
  "Report": {
    "Title": "Quarterly Portfolio Report",
    "EffectiveDate": "2026-06-30",
    "Reference": "QPR-2026-02"
  },
  "Client": {
    "Name": "Example Holdings",
    "Adviser": "Jordan Smith"
  },
  "Summary": {
    "MarketValue": 1250000.5,
    "Return": 0.0875,
    "RiskBand": "Moderate"
  },
  "Holdings": [
    { "Name": "Domestic Equity", "Weight": 0.42, "Value": 525000.21 },
    { "Name": "Global Equity", "Weight": 0.31, "Value": 387500.16 },
    { "Name": "Fixed Income", "Weight": 0.27, "Value": 337500.13 }
  ]
}
```

### Data-design rules

1. **Use an object at the root.** An array, string, or number at the root is rejected by
   the API.
2. **Match names exactly.** `EffectiveDate` and `effectiveDate` are different keys.
3. **Send chart values as numbers.** Use `0.0875`, not the string `"8.75%"`, when a
   native chart needs the value.
4. **Use ISO dates.** `2026-06-30` or a full ISO timestamp is predictable for `formatD`.
5. **Keep display-only text when it is intentional.** A prepared value such as
   `"R396 264 998"` is useful when no further calculation is needed.
6. **Prefer one array of objects for a chart or table.** Categories and series values
   should live on the same item so one structural loop controls the range.
7. **Represent absent values consistently.** Use `null` or `""`, then apply
   `ifEmpty(...)` in the template where a fallback is required.

### Numbers: raw versus preformatted

| JSON | Template | Best use |
| --- | --- | --- |
| `1250000.5` | `{d.Value:formatN(2, '.', ' ')}` | Calculations and text |
| `0.0875` | `{d.Return:mult(100):toFixed(2):append('%')}` | Percentages and charts |
| `"R1 250 000.50"` | `{d.DisplayValue}` | Fixed display text only |

Do not put currency symbols or thousands separators into values used by a native chart.
Office needs a numeric cell to plot the point correctly.

## Tag language

Tags are written between braces. The `d` prefix means “read from the submitted data
object.” Tags can be in a text box, paragraph, table cell, header, footer, notes part,
or a supported embedded chart cell.

### Basic substitutions

| Data | Tag | Result |
| --- | --- | --- |
| `{"name":"Ada"}` | `{d.name}` | `Ada` |
| `{"client":{"name":"Ada"}}` | `{d.client.name}` | `Ada` |
| `{"active":true}` | `{d.active}` | `true` |
| `{"count":12}` | `{d.count}` | `12` |

PowerPoint and Word sometimes split a visible tag into several internal text runs. The
engine joins those runs for matching and places the replacement into the run containing
the opening brace, so the replacement keeps that run's formatting.

### Fixed array indexes

Use a numeric index when you want one known item rather than a repeating structure.
Indexes are zero-based.

```json
{
  "Contacts": [
    { "Name": "Ada" },
    { "Name": "Grace" }
  ]
}
```

```text
Primary: {d.Contacts[0].Name}
Secondary: {d.Contacts[i=1].Name}
```

Both forms resolve without creating a loop. Structural `[i]` and `[i+1]` markers are
covered in [Repeating data](#repeating-data).

### Chaining formatters

Add a formatter after a colon. Each formatter receives the previous formatter's output.

```text
{d.Client.Name:lowerCase:ucWords}
{d.Return:mult(100):round(2):append('%')}
{d.EffectiveDate:formatD('DD MMMM YYYY')}
```

Arguments can be constants or another `d.path`:

```text
{d.Name:prepend('Client: ')}
{d.Amount:add(d.Adjustment):formatN(2, '.', ' ')}
```

### Empty-value formatters

| Formatter | Example | Meaning |
| --- | --- | --- |
| `ifEmpty` | `{d.MiddleName:ifEmpty('-')}` | Use `-` when missing, `null`, empty text, an empty array, or an empty object |
| `ifEM` | `{d.Value:ifEM(0)}` | Short alias for `ifEmpty` |

### Text formatters

| Formatter | Example | Example result |
| --- | --- | --- |
| `lowerCase` | `{d.Name:lowerCase}` | `ada lovelace` |
| `upperCase` | `{d.Code:upperCase}` | `BALTRF` |
| `ucFirst` | `{d.Name:lowerCase:ucFirst}` | `Ada lovelace` |
| `ucWords` | `{d.Name:lowerCase:ucWords}` | `Ada Lovelace` |
| `prepend` | `{d.Code:prepend('Fund: ')}` | `Fund: BALTRF` |
| `append` | `{d.Value:append(' units')}` | `42 units` |
| `replace` | `{d.Code:replace('-', ' ')}` | Replaces every matching substring |
| `substr` | `{d.Reference:substr(0, 8)}` | First eight characters |
| `ellipsis` | `{d.Description:ellipsis(80)}` | Shortens long text and adds `...` |
| `len` | `{d.Description:len}` | Character count, or collection length |
| `print` | `{d.Name:print('Confidential')}` | Prints the supplied constant |

### Number formatters

| Formatter | Example | Notes |
| --- | --- | --- |
| `add` | `{d.Value:add(10)}` | Addition |
| `sub` | `{d.Value:sub(10)}` | Subtraction |
| `mult` | `{d.Return:mult(100)}` | Multiplication; this implementation uses `mult` |
| `div` | `{d.Value:div(4)}` | Division; division by zero produces a warning |
| `round` | `{d.Value:round(2)}` | Returns a rounded number |
| `toFixed` | `{d.Value:toFixed(2)}` | Returns text with exactly two decimals |
| `formatN` | `{d.Value:formatN(2, '.', ' ')}` | Precision, decimal separator, thousands separator |

Examples for `1234567.8`:

```text
{d.Value:formatN(2, '.', ',')}  -> 1,234,567.80
{d.Value:formatN(2, ',', ' ')}  -> 1 234 567,80
{d.Value:round(0)}              -> 1234568
```

### Date formatter

`formatD` accepts an ISO date or timestamp. Supported tokens are `YYYY`, `YY`, `MMMM`,
`MMM`, `MM`, and `DD`. The common `LL` pattern is also available.

```text
{d.Date:formatD('DD MMMM YYYY')} -> 30 June 2026
{d.Date:formatD('YYYY-MM-DD')}   -> 2026-06-30
{d.Date:formatD('LL')}           -> June 30, 2026
```

Month names currently use the Python process locale; there is no per-request language
option in the public API.

### Array formatter

`arrayJoin(separator, start, count)` joins an array of strings or numbers.

```json
{ "Tags": ["income", "balanced", "global", "retirement"] }
```

```text
{d.Tags:arrayJoin(' · ')}       -> income · balanced · global · retirement
{d.Tags:arrayJoin(', ', 1, 2)}  -> balanced, global
```

### Missing paths and warnings

If `{d.Report.Date}` is in the template but the JSON contains `ReportDate`, the engine
does not guess. The output value becomes empty and a warning identifies the missing
path. This strict behavior prevents a spelling error from silently binding to the wrong
field.

Warnings are visible in the web interface and returned as a JSON array in the
`X-PDFGenerator-Warnings` response header.

## PowerPoint template design

PowerPoint is ideal for fixed-page reports, factsheets, certificates, and presentation-
style PDFs. Design the entire visual system in PowerPoint; PDFGenerator changes content,
not the creative direction.

### Recommended layout system

| Layer | Recommendation |
| --- | --- |
| Slide size | Choose the final PDF proportion before designing, such as A4 portrait or 16:9 |
| Master/layout | Put logos, page furniture, recurring footer text, and backgrounds on the master or layout |
| Grid | Use consistent outer margins and align text boxes, tables, pictures, and charts to common guides |
| Typography | Use Gotham or Montserrat from `assets/fonts`; keep a small hierarchy of title, section, body, and note styles |
| Color | Use one dark neutral, one accent, and a restrained chart palette with accessible contrast |
| Long text | Give descriptions enough height; PowerPoint text boxes do not automatically move content below them |
| Charts | Use native Office charts and style them in PowerPoint before adding data tags |

### Text tags in PowerPoint

- Put tags directly into text boxes, placeholders, table cells, slide masters, layouts,
  or notes text.
- Format the opening brace and its run with the desired output style.
- Keep labels and values in separate text runs or boxes when they need different styles.
- Avoid shrinking a text box to the exact size of sample text. Real data may be longer.
- Test the longest realistic title, description, and client name.

Example title block:

```text
{d.TitleName}
{d.Report.ReportName}
As at {d.FormattedEffectiveDate}
```

### PowerPoint-specific limits

- Structural loops do not duplicate entire slides or arbitrary slide shapes.
- Repeating content on a slide should normally be represented by a native chart or a
  fixed number of deliberately positioned fields.
- A long replacement can overflow a fixed text box. The engine preserves the template
  geometry instead of redesigning the slide.
- Dynamic images replace existing picture shapes; they do not create new shapes.

## Word template design

Word is ideal for flowing reports, letters, statements, and documents with tables that
may grow vertically.

### Recommended Word practices

1. Use Word styles for headings, body text, captions, and tables.
2. Put headers, footers, and page numbers in their native Word areas.
3. Use a borderless table when precise alignment is more important than free-floating
   text boxes.
4. Set paragraph spacing deliberately; avoid repeated blank paragraphs for layout.
5. Keep a table header outside the two loop-marker rows.
6. Allow table rows to break or stay together according to the expected content.
7. Test with enough rows to force a page break.

Text substitutions work in the document body, headers, footers, footnotes, endnotes,
and comments. Supported Word loops duplicate table rows vertically.

## Repeating data

`[i]` means “the current item.” `[i+1]` marks the next template row or column and tells
the engine which structure to expand. The first row or column is the visual template;
its formatting is copied for every data item.

### Vertical Word table loop

Given this data:

```json
{
  "Items": [
    { "Description": "Consulting", "Quantity": 2, "Amount": 1500 },
    { "Description": "Implementation", "Quantity": 1, "Amount": 4200 },
    { "Description": "Support", "Quantity": 3, "Amount": 900 }
  ]
}
```

Create a Word table with a header and two template rows:

| Description | Quantity | Amount |
| --- | --- | --- |
| `{d.Items[i].Description}` | `{d.Items[i].Quantity}` | `{d.Items[i].Amount:formatN(2)}` |
| `{d.Items[i+1].Description}` | `{d.Items[i+1].Quantity}` | `{d.Items[i+1].Amount:formatN(2)}` |

The two template rows become three output rows. Only vertical Word table loops are
currently supported; horizontal and nested Word loops are not.

### Loop rules

- Use the same array path on both marker rows or columns.
- Put `[i]` in the first template structure and `[i+1]` in the adjacent structure.
- Style the `[i]` row or column as the output template.
- Keep headers and totals outside the loop pair.
- An empty array removes both marker rows or columns.
- A one-item array leaves one rendered row or column.
- Use one array of objects instead of unrelated parallel arrays.

## Native charts

PDFGenerator retains native PowerPoint and Word charts. Data tags live in the chart's
embedded Excel workbook. At generation time the engine expands the workbook, changes
the connected chart formula ranges, and rebuilds the chart's cached categories and
values before LibreOffice renders the PDF.

### Create a native chart

1. In PowerPoint or Word, choose **Insert → Chart** and select the required chart type.
2. Style the chart completely: type, colors, labels, axes, legend, gap width, fonts, and
   number formats.
3. Open **Chart Design → Edit Data** to edit the embedded Excel workbook.
4. Replace sample worksheet values with tags using one of the loop patterns below.
5. Ensure the visible chart range covers both marker rows or columns.
6. Save and close the embedded workbook, then save the Office template.
7. Generate with a payload containing at least three items to prove expansion beyond the
   marker pair.

### Vertical chart data

Use adjacent rows when categories run down the worksheet. This is the pattern used by
the supplied Tracker template.

```json
{
  "Performance": [
    { "Period": "1 Month", "Fund": 0.008, "Benchmark": 0.006 },
    { "Period": "3 Months", "Fund": -0.021, "Benchmark": -0.0012 },
    { "Period": "1 Year", "Fund": 0.207, "Benchmark": 0.203 },
    { "Period": "3 Years", "Fund": 0.165, "Benchmark": 0.168 }
  ]
}
```

Embedded worksheet design:

| Cell/row | A: Period | B: Fund | C: Benchmark |
| --- | --- | --- | --- |
| Row 1 | Category | Fund | Benchmark |
| Row 2 | `{d.Performance[i].Period}` | `{d.Performance[i].Fund}` | `{d.Performance[i].Benchmark}` |
| Row 3 | `{d.Performance[i+1].Period}` | `{d.Performance[i+1].Fund}` | `{d.Performance[i+1].Benchmark}` |

Rows 2–3 expand to rows 2–5. The category and both series formulas extend to row 5.

### Horizontal chart data

Use adjacent columns when categories run across the worksheet.

Embedded worksheet design using the same JSON:

| Cell/row | A | B | C |
| --- | --- | --- | --- |
| Row 1 | Period | `{d.Performance[i].Period}` | `{d.Performance[i+1].Period}` |
| Row 2 | Fund | `{d.Performance[i].Fund}` | `{d.Performance[i+1].Fund}` |
| Row 3 | Benchmark | `{d.Performance[i].Benchmark}` | `{d.Performance[i+1].Benchmark}` |

Columns B–C expand to B–E. The engine copies the template column's width, styles, and
number formats. Category and value formulas extend through column E, and chart caches
are rebuilt from the rendered cells.

### Chart design recommendations

- Keep chart cells numeric. Format percentages and currencies using the embedded
  workbook's number format and the chart's axis/data-label settings.
- Use `ifEmpty(0)` when a missing numeric value should plot as zero:
  `{d.Allocation[i].Weight:ifEmpty(0)}`.
- Use short category labels or rotate labels in the chart design.
- Set axis minimum/maximum rules deliberately; automatic scaling can make comparable
  reports look inconsistent.
- Use the same array path for categories and every series in the loop.
- Do not replace native charts with SVG. This project intentionally keeps chart control
  inside PowerPoint or Word.
- If a chart is blank, inspect the populated Office file from `/api/render-template`
  before diagnosing the PDF converter.

### Chart checklist

- [ ] `[i]` and `[i+1]` are in adjacent rows or adjacent columns.
- [ ] Both markers resolve from the same JSON array.
- [ ] Numeric series cells receive JSON numbers.
- [ ] The original chart formulas include both template markers.
- [ ] Series titles are outside the expanding range.
- [ ] Cell and chart number formats are correct.
- [ ] The test payload has zero, one, and several items.

## Dynamic images

A dynamic image starts as a normal placeholder picture in PowerPoint or Word. Put the
data tag into the picture's alt text/description; the engine replaces only that picture's
media while preserving the shape's placement.

### Add a picture placeholder

1. Insert a temporary raster picture into the Office template.
2. Crop and size its shape to the desired output box.
3. Open the picture's **Alt Text** pane.
4. Put a tag such as `{d.ProfilePhoto}` in the description or title.
5. Supply an HTTP(S) URL or Base64 data URI at that JSON path.

Schematic JSON value:

```json
{
  "ProfilePhoto": "data:image/png;base64,<BASE64_DATA>"
}
```

Or use an accessible remote image:

```json
{
  "ProfilePhoto": "https://images.example.com/reports/client-1042.png"
}
```

### Image fit modes

| Alt-text tag | Geometry behavior | Best for |
| --- | --- | --- |
| `{d.Photo}` | Exact `fill`; keeps the shape box and stretches raster to it | Logos, backgrounds, controlled-aspect images |
| `{d.Photo:imageFit(fill)}` | Explicit exact fill | Same as the default |
| `{d.Photo:imageFit(contain)}` | Preserves aspect ratio inside the same box; may letterbox | Portraits, product photos, charts as raster images |
| `{d.Photo:imageFit(fillWidth)}` | Preserves aspect ratio and width; changes shape height | Flowing Word layouts where height may change |

PDFGenerator deliberately defaults a plain image tag to `fill`, so the replacement uses
the exact placeholder box. Carbone defaults a plain image to `fillWidth`; use an explicit
formatter when moving templates between the two systems.

Existing crop rectangles are cleared after replacement so a stale template crop does not
hide the new image. EXIF orientation is normalized while decoding; `:autoOrient` is also
accepted for compatibility.

### Image design recommendations

- Match the placeholder aspect ratio to the expected image source.
- Use PNG for transparency and JPEG for photographic content without transparency.
- Avoid very small source images that will become blurry in PDF output.
- Do not place a tagged and untagged picture on the assumption that they share one media
  object. PDFGenerator gives each replacement its own generated media part.
- Remote URLs require backend network access and must respond within the configured
  timeout.
- Raster images are supported. SVG is intentionally not part of this workflow.
- Image tags inside a repeating Word row do not currently receive the row loop index;
  use fixed image locations or prebuild a fixed number of picture placeholders.

## Report design recipes

These recipes show how to pair a clean Office layout with a maintainable JSON shape.

### Investment factsheet — PowerPoint

Use one A4 portrait slide for a concise, high-density report.

```text
┌────────────────────────────────────────────────────┐
│ LOGO    FUND NAME                         DATE     │
├────────────────────────────────────────────────────┤
│ AUM       TARGET       RISK       INCEPTION        │
├───────────────────────┬────────────────────────────┤
│ Investment objective  │ Asset allocation chart     │
│ and description       │ (native donut/bar)         │
├───────────────────────┴────────────────────────────┤
│ Historical return chart (native clustered bars)   │
├────────────────────────────────────────────────────┤
│ TER table                      Disclaimer / footer │
└────────────────────────────────────────────────────┘
```

Recommended data groups:

```json
{
  "Fund": {
    "Name": "Balanced Tracker Fund",
    "Code": "BALTRF",
    "EffectiveDate": "2026-06-30",
    "AUM": 396264998,
    "Target": "4–6% p.a. above CPI over rolling seven-year periods"
  },
  "Allocation": [
    { "AssetClass": "SA Equity", "Actual": 0.43, "Strategic": 0.4 },
    { "AssetClass": "Global Equity", "Actual": 0.29, "Strategic": 0.3 },
    { "AssetClass": "Fixed Income", "Actual": 0.2, "Strategic": 0.2 },
    { "AssetClass": "Cash", "Actual": 0.08, "Strategic": 0.1 }
  ],
  "Fees": {
    "Date": "March 2026",
    "TER": 0.0028,
    "TIC": 0.0028
  }
}
```

Useful tags:

```text
{d.Fund.Name}
{d.Fund.EffectiveDate:formatD('DD MMMM YYYY')}
R{d.Fund.AUM:formatN(0, '.', ' ')}
{d.Fees.TER:mult(100):toFixed(2):append('%')}
```

Design notes:

- Keep no more than two dominant charts on one page.
- Use the same series colors across reporting periods.
- Put legal text in a dedicated footer zone with a tested minimum size.
- Use one long-description text box with a deliberate maximum character budget.

### Client statement — Word

Use a flowing DOCX when transaction or holding counts vary.

```json
{
  "Statement": {
    "Number": "ST-2026-0061",
    "Date": "2026-06-30"
  },
  "Client": {
    "Name": "Example Holdings",
    "AddressLines": ["18 Market Street", "Cape Town", "8001"]
  },
  "Transactions": [
    { "Date": "2026-06-03", "Description": "Contribution", "Amount": 25000 },
    { "Date": "2026-06-14", "Description": "Management fee", "Amount": -350.5 },
    { "Date": "2026-06-28", "Description": "Distribution", "Amount": 1320.75 }
  ],
  "ClosingBalance": 485970.25
}
```

Suggested document structure:

1. Branded header with statement number and date.
2. Client and account block in a two-column borderless table.
3. Transaction table with a vertical `[i]` / `[i+1]` row pair.
4. Closing balance in a separate totals table outside the loop.
5. Notes and contact information in the footer.

Template row tags:

```text
{d.Transactions[i].Date:formatD('DD MMM YYYY')}
{d.Transactions[i].Description}
{d.Transactions[i].Amount:formatN(2, '.', ' ')}
```

### Profile card — PowerPoint or Word

Use a fixed image placeholder plus a compact metadata block.

```json
{
  "Person": {
    "Name": "Ada Lovelace",
    "Role": "Quantitative Analyst",
    "Biography": "Ada leads portfolio analytics and reporting automation.",
    "Photo": "data:image/jpeg;base64,<BASE64_DATA>"
  }
}
```

```text
{d.Person.Name}
{d.Person.Role:upperCase}
{d.Person.Biography:ellipsis(180)}
```

Put `{d.Person.Photo:imageFit(contain)}` in the temporary portrait's alt-text
description. Use a neutral placeholder with the exact output aspect ratio.

## Use the web application

### Standard upload

**Browse upload** and drag-and-drop create a browser snapshot of the selected file. If
you edit the template afterward, select it again to upload the new version.

### Live template mode

On Chrome or Edge over HTTPS or `localhost`, **Open live template** can retain a secure
read handle to the selected local file. The app polls the file, waits for a save to become
stable, and regenerates the preview after a change.

Browser security intentionally limits this feature:

- The permission applies only to the current browser session.
- Firefox, Safari, insecure remote HTTP origins, and normal upload controls remain
  snapshot-based.
- If file permission is lost, choose the live file again.
- Saving through an editor that replaces the file may require permission again.

### Auto preview

Auto preview is enabled by default. After valid JSON changes, generation is debounced by
about 900 ms. An older in-flight request is aborted when a newer edit arrives, preventing
stale PDFs from replacing the latest result.

Turn auto preview off when:

- working with a very large template;
- editing JSON in several incomplete steps;
- using a slow remote converter; or
- intentionally batching template and data changes.

### Read the preview status

| Status | Meaning |
| --- | --- |
| Waiting for a report | No successful generation yet |
| Update queued | Valid change is waiting for the debounce timer |
| Waiting for valid JSON | JSON syntax must be corrected |
| Rendering update | Backend population/conversion is running |
| Up to date | Current template snapshot and JSON produced the visible PDF |
| Update failed | Inspect the error message and backend log |

## Use the HTTP API

The interactive API schema is available at `http://localhost:9010/docs`.

### Generate a PDF

`POST /api/generate` accepts multipart form data:

| Field | Type | Description |
| --- | --- | --- |
| `template` | File | `.pptx` or `.docx` template |
| `data` | Text | JSON object serialized as text |

PowerShell using `curl.exe`:

```powershell
curl.exe -X POST "http://localhost:9010/api/generate" `
  -F "template=@C:\Reports\fund-template.pptx" `
  -F "data=<C:\Reports\payload.json" `
  --output "C:\Reports\fund-report.pdf"
```

Python using `httpx`:

```python
import json
from pathlib import Path

import httpx

template_path = Path("fund-template.pptx")
payload = {
    "Fund": {"Name": "Balanced Tracker Fund"},
    "EffectiveDate": "2026-06-30",
}

with template_path.open("rb") as template:
    response = httpx.post(
        "http://localhost:9010/api/generate",
        files={
            "template": (
                template_path.name,
                template,
                "application/vnd.openxmlformats-officedocument.presentationml.presentation",
            )
        },
        data={"data": json.dumps(payload)},
        timeout=240,
    )

response.raise_for_status()
Path("fund-report.pdf").write_bytes(response.content)
warnings = json.loads(response.headers.get("X-PDFGenerator-Warnings", "[]"))
print(warnings)
```

Browser JavaScript:

```javascript
const form = new FormData();
form.append("template", templateFile);
form.append("data", JSON.stringify(payload));

const response = await fetch("http://localhost:9010/api/generate", {
  method: "POST",
  body: form,
});

if (!response.ok) {
  const error = await response.json();
  throw new Error(error.detail ?? `Generation failed: ${response.status}`);
}

const pdfUrl = URL.createObjectURL(await response.blob());
pdfFrame.src = pdfUrl;
```

The PDF response has `Content-Disposition: inline`. The converter name is returned in
`X-PDFGenerator-Renderer`, and template warnings are returned in
`X-PDFGenerator-Warnings`.

### Inspect the populated Office file

Use `POST /api/render-template` with the same multipart fields. The response is the
populated `.pptx` or `.docx` before PDF conversion. This endpoint is invaluable when
checking:

- whether a tag resolved;
- whether chart workbook cells expanded;
- whether a chart formula range changed;
- whether a picture was embedded; or
- whether the problem appears only during LibreOffice conversion.

### Health and configuration endpoints

| Endpoint | Purpose |
| --- | --- |
| `GET /health` | Backend process health and version |
| `GET /api/config` | Public ports, template types, size limit, and selected converter |
| `GET /api/converter/health` | Active LibreOffice or Gotenberg readiness |
| `GET /api/gotenberg/health` | Explicit remote Gotenberg diagnostic |
| `GET http://localhost:9012/health` | Guide service health |

## Fonts and output fidelity

Gotham and Montserrat are committed under `assets/fonts` and activated for the managed
LibreOffice process. Use those exact family names in PowerPoint and Word for the most
predictable PDF output.

### Font checklist

- Install or otherwise make the fonts available on the machine used to author the
  template, so PowerPoint or Word displays the intended layout.
- Prefer static Montserrat faces over variable-font faces.
- Use actual bold/italic faces rather than synthetic styling when possible.
- Check the final PDF's embedded fonts before production rollout.
- Confirm that your organization has the right to redistribute every font. Montserrat
  includes the SIL Open Font License; the supplied Gotham archive did not include a
  redistribution licence.

Layout can still differ slightly between Microsoft Office and LibreOffice. Test the
actual converter used in production, especially for line wrapping, chart labels, page
breaks, and uncommon font weights.

## Troubleshooting

### The template does not stay selected

- Confirm the file ends in `.pptx` or `.docx`.
- A standard upload is a snapshot. Use **Open live template** for saved-file watching.
- Live mode requires Chrome or Edge plus HTTPS or `localhost`.
- If a browser loses permission, choose the file again.

### JSON is rejected

- The root must be an object: `{ ... }`, not `[ ... ]`.
- Remove trailing commas and comments; JSON does not allow them.
- Put property names and string values in double quotes.
- Use the **Format JSON** button to validate and re-indent the payload.

### A tag becomes blank

- Match capitalization and spelling exactly.
- Confirm every parent object exists.
- Use a fixed array index or a recognized structural loop context.
- Read `X-PDFGenerator-Warnings` or the warning panel.
- Check for a template/JSON mismatch such as `FormattedEffectiveDate` versus
  `FormatedEffectiveDate`.

### A loop does not expand

- Put `[i+1]` immediately below `[i]` for vertical expansion.
- Put `[i+1]` immediately to the right of `[i]` for horizontal embedded-chart expansion.
- Use the same array path for both markers.
- Word supports table-row expansion only.
- PowerPoint supports chart-workbook loops, not slide or arbitrary-shape duplication.

### A chart is blank or stale

1. Call `/api/render-template` and open the populated Office file.
2. Open **Edit Data** and verify the embedded worksheet values.
3. Confirm the series/category formula includes both marker rows or columns.
4. Ensure plotted cells are numbers rather than formatted strings.
5. Apply `ifEmpty(0)` where blank input should become zero.
6. Check that the JSON array contains data.

### An image is not replaced

- Put the tag in the picture's alt-text description or title, not in a nearby text box.
- Confirm the JSON value is a valid Base64 image data URI or HTTP(S) URL.
- Do not omit the `data:image/...;base64,` prefix.
- Confirm decoded/downloaded content is below the configured 15 MB limit.
- Ensure the backend can reach a remote URL.
- Use a raster source rather than SVG.

### The PDF layout differs from PowerPoint or Word

- Confirm the intended fonts are embedded in the PDF.
- Test with the managed LibreOffice converter, not only Microsoft Office export.
- Avoid text boxes sized to exactly fit short sample text.
- Check chart labels and axis scaling with the longest categories and extreme values.
- Use native page/slide geometry and avoid unsupported floating-layout tricks.

### Unknown internet requests appear in backend logs

Public IP addresses are routinely scanned for `.env`, `.git/config`, WordPress,
GraphQL, and framework debug endpoints. A `404 Not Found` response means those paths do
not exist; it does not mean the application initiated the request.

For any internet-facing deployment:

- place the services behind an authenticated reverse proxy;
- restrict inbound ports with a firewall;
- terminate HTTPS at the proxy;
- rate-limit upload endpoints;
- do not expose the guide's template downloads if they contain confidential designs;
- move secrets out of source control and rotate previously committed credentials; and
- monitor generation size, duration, and failure rates.

## Compatibility and current limits

| Capability | PPTX | DOCX | Notes |
| --- | :---: | :---: | --- |
| Basic and nested text tags | Yes | Yes | Tags split across Office runs are joined |
| Static array indexes | Yes | Yes | `[0]` and `[i=0]` |
| Vertical table-row loops | No | Yes | Two adjacent template rows |
| Vertical native-chart loops | Yes | Yes | Two adjacent embedded-workbook rows |
| Horizontal native-chart loops | Yes | Yes | Two adjacent embedded-workbook columns |
| Native Office charts | Yes | Yes | Workbook formulas and caches are refreshed |
| Base64/HTTP raster pictures | Yes | Yes | Alt-text title/description tags |
| Exact-box image fill | Yes | Yes | Default for an unformatted image tag |
| Headers, footers, notes text | Yes | Yes | According to the native Office part |
| Entire slide duplication loops | No | N/A | Planned compatibility work |
| Horizontal or nested Word loops | N/A | No | Planned compatibility work |
| Images inside structural loops | No | No | Use fixed image placeholders |
| Smart conditional blocks | No | No | Only the documented formatter subset is supported |
| Aliases and translations | No | No | Not implemented |
| ECharts or SVG generation | No | No | Use native Office charts and raster pictures |
| Barcodes and file append | No | No | Not implemented |
| Uploaded XLSX templates | No | No | XLSX is supported only as embedded chart data |

When migrating an existing Carbone template, begin with substitutions, Word table loops,
native chart loops, and picture alt-text tags. Remove or redesign any unsupported smart
conditions, nested structures, transformations, barcodes, and file operations.

## Deployment and operations

### Select the PDF converter

In `config.py`:

```python
PDF_CONVERTER = "libreoffice"
```

uses the application-managed local renderer. To use the configured remote service:

```python
PDF_CONVERTER = "gotenberg"
```

The selection is explicit. A failed local conversion is not silently sent to Gotenberg.

### Production recommendations

- Run backend, frontend, and guide services under a process supervisor.
- Put all three behind one HTTPS reverse proxy and use path-based routing or distinct
  internal hostnames.
- Keep ports `9010`, `9011`, and `9012` private whenever possible.
- Set `BACKEND_INTERNAL_URL` for the frontend if the backend is not reachable at
  `http://127.0.0.1:9010` from the frontend process.
- Cache the managed LibreOffice runtime on the deployment host rather than downloading
  it on every ephemeral start.
- Limit concurrent conversions according to memory and CPU capacity.
- Treat uploaded templates and JSON as confidential transient data.
- Add authentication before exposing generation to untrusted users.

### Guide maintenance

`guide/USER_GUIDE.md` is the canonical source. The guide server converts it on every
request, so a saved Markdown change is visible after refreshing the browser; no static
site build is required. Keep examples synchronized with automated tests and
`information files/aicontext.md` whenever the template language changes.

## Template review checklist

### Data

- [ ] Root JSON value is an object.
- [ ] Key spelling and capitalization match the template.
- [ ] Dates are ISO strings.
- [ ] Chart values are numeric.
- [ ] Empty and missing values have an intentional fallback.
- [ ] Arrays include zero-, one-, and many-item test cases.

### PowerPoint or Word design

- [ ] Final page or slide size is correct.
- [ ] Gotham/Montserrat faces are available and tested.
- [ ] Long titles and descriptions fit.
- [ ] Repeated Word rows have a header outside the marker pair.
- [ ] Images have the correct placeholder ratio and alt-text tag.
- [ ] Charts use native Office objects and intentional number formats.
- [ ] Legal text, page numbers, and source notes are present.

### Generation QA

- [ ] No unresolved `{d.` text is visible.
- [ ] Warning list is empty or every warning is understood.
- [ ] Chart categories, series, legend, and axis scale are correct.
- [ ] Picture crops and aspect ratios are correct.
- [ ] PDF fonts are embedded rather than substituted.
- [ ] Word page breaks and table headers behave correctly.
- [ ] The final PDF is reviewed at 100% zoom and in print preview.

## Reference examples and documentation basis

This guide uses the same approachable data/template/result teaching pattern found in
Carbone's documentation while describing PDFGenerator's smaller, tested compatibility
surface. For broader template-design ideas, consult these official Carbone resources and
then verify every feature against this guide's compatibility table:

- [Carbone: design your first template](https://carbone.io/documentation/design/overview/getting-started.html)
- [Carbone: substitution basics](https://carbone.io/documentation/design/substitutions/the-basics.html)
- [Carbone: formatter overview](https://carbone.io/documentation/design/formatters/overview.html)
- [Carbone: dynamic pictures](https://carbone.io/documentation/design/advanced-features/pictures.html)
- [Carbone: native charts](https://carbone.io/documentation/design/advanced-features/charts.html)

The supplied Tracker template and JSON remain the authoritative end-to-end example for
this repository:

- [Download the sample PPTX](/downloads/TrackerFS_Carbone.pptx)
- [Download the sample JSON](/downloads/sample-data.json)
- [View this guide as raw Markdown](/guide.md)
