# PDF Export
TunnelCAD exports cross-sections and reports to PDF through a layout-driven engine. A layout is an ICAD Paper Space drawing — authored in ICAD itself — that carries tagged viewports, text, and rectangles. At plot time, TunnelCAD walks each tagged entity, invokes a matching data provider, and produces a PDF page through ICAD's plot pipeline.
:::{note}
Single-section + batch (multi-page) PDF export are both shipping. Multi-section sheet booklets aren't part of the PDF engine — when you need a multi-section sheet layout, use the Section DXF Atlas export (right-click a tunnel → *Export Sections to DXF…* → atlas mode) instead. The analytics, template, and volume PDF exports are planned to migrate to the layout engine in a future release.
:::
## Quick start
1. Open a project and select a section.
2. Choose **File → Export Section to PDF…** or click the **Export PDF** button on the Sections toolbar.
3. Pick a layout (every new project starts with a seeded `section-A4` default).
4. Pick an output filename.
5. Click OK.
## Architecture
Four components separate cleanly:
| Component | Unit | Responsibility |
|---|---|---|
| Storage | `Project/UTunnelProjectIO.pas`, `Print/UProjectLayouts.pas` | Layouts + assets live as XML subtrees inside the `.tun` project file |
| Layout executor | `Print/ULayoutExecutor.pas` | Loads a layout, walks tagged entities, calls providers, plots via ICAD |
| Provider registry | `Print/UProviderRegistry.pas` | `IProvider` interface + global `RegisterProvider` / `LookupProvider` |
| ICAD plot bridge | `cad/PrintEngine/UCADPlotAPI.pas` | Thin `RenderCanvasToPDF` API that drives ICAD's existing plotter |
## Layouts
A layout is a regular ICAD drawing authored in Paper Space. Entities can be tagged with two reserved attributes on any `TDrawObject`:
- `provider` — the provider kind (e.g. `SECTION_VIEW`, `TEXT`, `DR_TABLE`)
- `providerArgs` — `key=val;key=val` argument string parsed by `TArgMap`
Untagged entities pass through as static layout content (title block lines, borders, company text).
Layouts live inside the `.tun` project as `…` nodes under a `` subtree. Binary assets (logos, photos) live as base64-encoded `` nodes under ``. New projects are seeded with a default `section-A4` layout on creation.
## Built-in providers
| Provider | Role | Target | Purpose |
|---|---|---|---|
| `TEXT` | paper | text entity | Substitute `%FIELD%` tokens (or replace entire text via `field=X`) from the plot context |
| `SECTION_VIEW` | model | viewport | Draw all section line data into model space on layers named `LINE_` and frame the viewport |
| `DR_TABLE` | paper | rectangle | Draw DR min/max/avg/std table (placeholder until analytics integration in Plan 3) |
| `POLAR_CHART` | paper | rectangle | Concentric deviation polar grid + data polyline (placeholder as above) |
| `IMAGE` | paper | rectangle | Insert a project asset (PNG / JPEG / BMP) at the slot rectangle as an embedded `TCADImage` |
| `STATUS_STRIP` | paper | rectangle | Chainage bar with one grey tick per section in the alignment plus a red tick at the current section |
### Layout TEXT placeholder fields
Layout text entities tagged with the `TEXT` provider can substitute project values either inline as `%FIELD%` tokens or whole-text via the `field=` argument. All numeric fields default to the project's unit settings (Tunnel Settings → Units); override decimals or format with `fmt=` on the slot's args.
#### No-argument fields
| Field | Source | Default format |
|---|---|---|
| `PROJECT_NAME` | Project name | string |
| `TUNNEL` | Active alignment name | string |
| `STATION` / `CHAINAGE` | Section station | `0.000` |
| `SECTION_NAME` | Section name | string |
| `TEMPLATE` | Section template name | string |
| `PAGE_INDEX` / `PAGE_COUNT` | Layout pagination | integer |
| `EASTING` / `NORTHING` | Alignment world coordinates at the station | project `CoordinateDecimals` |
| `ELEVATION` | Alignment elevation | project `ElevationDecimals` |
| `BEARING` | Alignment bearing at the station | project `AngularUnit` + `AngleDecimals` (e.g. `100.0000g`) |
| `GRADIENT` | Vertical gradient at the station, percent | project `OffsetDecimals`, `%` suffix |
| `RADIUS` | Horizontal radius at the station | project `CoordinateDecimals`; `inf` on straight runs |
| `START_CHAINAGE` / `END_CHAINAGE` | Alignment chainage range | project `StationDecimals` |
| `ALIGNMENT_LENGTH` | End − start chainage | project `CoordinateDecimals` |
| `SECTION_INDEX` / `SECTION_COUNT` | 1-based index in the alignment / total sections | integer |
| `POINT_COUNT` | Total measured points on the section (sum across all line definitions) | integer |
| `DATE_NOW` / `TIME_NOW` | `Now`, override format via `fmt=` | `yyyy-mm-dd` / `hh:nn` |
| `PROJECT_FILE` | Project file path; literal `(unsaved)` when the project has not been saved | string |
#### Parameterised fields (`field=` mode only)
These cannot be used inline as `%FIELD%`; set them via `field=` with the companion arg.
| Field | Required arg | Description |
|---|---|---|
| `AREA` | `vol=` | Cross-section area (m²) for the named volume definition at the current section. |
| `POINT_COUNT` | `line=` (optional) | Without `line=`, sums all lines (same as inline `%POINT_COUNT%`). With `line=`, returns only that one line's point count. |
For cumulative or total volume across the whole alignment, use the **Mass Table** dialog (right-click a volume definition → *Mass Table…*). The dialog computes both values and exports them to CSV / XLSX / PDF.
#### Tagging a text entity — step by step
Two modes, picked per text entity:
| Mode | When to use |
|---|---|
| **Inline `%FIELD%` substitution** (default) | The entity's existing text body holds a sentence with one or more `%TOKEN%` placeholders. The provider replaces only the tokens; everything else stays. |
| **Whole-text `field=` mode** | The entity should be replaced *entirely* by one resolved value (with optional `prefix=` / `suffix=`). Required for parameterised tokens like `AREA` and `POINT_COUNT line=`. |
##### 1. Drop the text in the layout
1. Open the layout in the layout editor (paper-space tab on the **Sections** workspace).
2. Use the standard CAD **Text** tool to place a text entity where you want the label. Type the **default** body — this is what shows in the layout when no project is loaded, and (in inline mode) it's also the template the provider rewrites.
##### 2. Tag it as `TEXT`
1. Select the text entity on the paper canvas.
2. In the right-side **Provider Palette** panel, click `TEXT`. The palette's **args form** below the provider list switches to the TEXT schema (six widgets: `field`, `fmt`, `prefix`, `suffix`, `vol`, `line`).
3. Configure the args (next section).
4. Click **Tag Selected**. The entity now carries `Provider=TEXT` and the serialized args string. The **Outline** panel shows it; the **Property Inspector** has both rows.
To re-edit a tag later, select the entity again — the palette auto-loads its current `Provider` + args. Change values, click **Tag Selected** to save.
##### 3. Configure the args form
The args form widgets:
| Widget | Type | Effect |
|---|---|---|
| `field` | dropdown of every supported field | **Empty** → inline `%FIELD%` substitution mode. **Set to a name** → whole-text replacement mode. |
| `fmt` | text box | Optional override for numeric formatting. Examples: `0.000`, `0`, `0.00 m`. For `DATE_NOW` / `TIME_NOW` it's any FPC `FormatDateTime` pattern (`yyyy-mm-dd hh:nn`). Leave empty to use the project's unit defaults. |
| `prefix` | text box | Prepended to the resolved value. **Only honoured in `field=` mode** — ignored when `field` is empty. |
| `suffix` | text box | Appended to the resolved value. **Only honoured in `field=` mode**. |
| `vol` | text box | The volume-definition name. Required when `field=AREA`; ignored otherwise. Case-insensitive. |
| `line` | text box | The line-definition name. Optional when `field=POINT_COUNT` (without it, all lines are summed). Ignored when `field` is anything else. |
##### 4. Worked examples
These are the most common patterns. The "ProviderArgs" column shows the serialized form the editor stores under the hood (`key=val;key=val`); in normal use you fill in widgets, you don't type the string.
###### Inline mode — leave `field` empty
Type the tokens directly into the entity's text body. Tokens are uppercase and surrounded by `%`.
| Entity text body | ProviderArgs | Resolved output (sample) |
|---|---|---|
| `Sta %STATION%` | *(empty)* | `Sta 125.500` |
| `Sta %STATION%, %SECTION_NAME%` | *(empty)* | `Sta 125.500, S-15` |
| `Tunnel: %TUNNEL%, axis %EASTING%E %NORTHING%N` | *(empty)* | `Tunnel: Bypass, axis 472301.250E 4218905.732N` |
| `Bearing %BEARING%, gradient %GRADIENT%` | *(empty)* | `Bearing 195.4220g, gradient 1.500%` |
| `Section %SECTION_INDEX% of %SECTION_COUNT%` | *(empty)* | `Section 12 of 47` |
| `Plotted %DATE_NOW% by %PROJECT_NAME%` | *(empty)* | `Plotted 2027-05-07 by Bypass-East-Phase2` |
The entity's body is the template. Any text outside `%…%` is preserved literally.
###### Whole-text mode — set `field` to a token name
The entity's existing text body is **discarded** and replaced by `prefix + resolved + suffix`.
| `field` | Other args | Resolved output | Use for |
|---|---|---|---|
| `STATION` | *(none)* | `125.500` | A station label that is *just* the station, no surrounding sentence. |
| `STATION` | `prefix=Sta `, `suffix= m` | `Sta 125.500 m` | Same, with caption + unit. |
| `BEARING` | *(none)* | `100.0000g` | Default project units (gradians here). |
| `BEARING` | `fmt=0.00` | `100.00g` | Override decimals; the angular unit and `g` suffix come from the project. |
| `RADIUS` | `prefix=R = `, `suffix= m` | `R = 250.000 m` *or* `R = inf m` on a straight run | Horizontal radius label. |
| `DATE_NOW` | `fmt=dd/mm/yyyy` | `07/05/2027` | Custom date format. |
| `AREA` | `vol=Excavation` | `87.530` | Per-section excavation area. |
| `AREA` | `vol=Excavation`, `prefix=Area = `, `suffix= m²` | `Area = 87.530 m²` | Same with full caption. |
| `POINT_COUNT` | *(none)* | `42` | Total points on the section. |
| `POINT_COUNT` | `line=Crown` | `12` | Just the Crown line's points. |
| `POINT_COUNT` | `line=Crown`, `prefix=Crown: `, `suffix= pts` | `Crown: 12 pts` | Caption + unit. |
###### When you need multiple parameterised values on one label
You can't write `%AREA[Excavation]%` inline (parameterised tokens are field-mode only). To show, e.g., excavation **and** lining areas on the same drawing, drop **two** text entities side by side:
- Entity A: `field=AREA`, `vol=Excavation`, `prefix=Excavation: `, `suffix= m²`
- Entity B: `field=AREA`, `vol=Lining`, `prefix=Lining: `, `suffix= m²`
##### 5. Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| The token shows up literally as `%STATION%` in the PDF | Inline mode but the token is misspelled, lower-case, or uses an unsupported name | Tokens are **uppercase** and must match the supported list. The PDF export emits a warning diagnostic listing the offending field. |
| Field-mode entity shows the original text instead of the resolved value | `field=` is empty (still in inline mode) | Set the `field` dropdown to the desired token. |
| `%AREA%` shows up literally | `AREA` is a parameterised token; inline `%AREA%` is intentionally not supported | Switch to `field=AREA` mode and fill `vol=` with the volume-definition name. |
| Empty text where you expected an area / radius / bearing | The alignment has no engine geometry yet, or `vol=` references a name that doesn't exist | Import the alignment geometry; verify the volume-definition name in *Tunnel Settings → Volumes*. |
| `fmt=0.000` ignored on `BEARING` | `fmt=` only overrides decimals; the angular unit stays from project units | Change *Tunnel Settings → Units → Angular Unit* if you want degrees / DMS / radians instead of gradians. |
### Tagging an IMAGE entity — step by step
The `IMAGE` provider drops a project-embedded raster (logo, photo, scan) into the slot rectangle.
##### 1. Register the image as a project asset
Images live in the project's **Assets** collection — named binary blobs serialized inside the `.tun` file, so the layout travels with the project.
1. Open the asset manager from one of:
- Ribbon **File → Manage Project Assets…**
- Right-click the project root in the **Project Tree → *Manage Project Assets…***
2. Click **Add…** → pick a PNG / JPEG / BMP file.
- The MIME type is auto-detected from the file extension.
- The asset name defaults to the **filename stem** (e.g. `company_logo.png` → asset name `company_logo`).
- On a name collision the dialog prompts **Replace / Rename / Cancel**.
3. The list view shows every registered asset with name, MIME type, and size in KB. **Rename** and **Remove** operate on the selected row.
4. Close the dialog. Save the project (the assets are embedded as base64 in the `.tun`).
##### 2. Drop a placeholder rectangle in the layout
In the layout editor, draw a regular CAD rectangle where the image should appear. The `IMAGE` provider sizes the bitmap to the rectangle's bounds (no scaling preserves aspect — make the rectangle the intended display size).
##### 3. Tag the rectangle as `IMAGE`
1. Select the rectangle.
2. In the Provider Palette, click `IMAGE`. The args form has one widget: **`asset`**.
3. Type the asset name you registered in step 1 (e.g. `company_logo`) — case-insensitive.
4. Click **Tag Selected**.
The rectangle now resolves to your bitmap on the next plot. The bottom-left of the rectangle becomes the bottom-left of the image; the rectangle's width and height become the image's world width and height.
##### 4. Worked examples
| ProviderArgs | Effect |
|---|---|
| `asset=company_logo` | Inserts the asset named `company_logo` into the slot rectangle. |
| `asset=` (empty) | Warning — `IMAGE` requires `asset=`; nothing is drawn. |
| `asset=missing` (name not in the asset list) | Warning — `missing asset: missing`; the placeholder X-rectangle is drawn so you can still see the slot. |
If the asset's MIME isn't one of `image/png` / `image/jpeg` / `image/bmp`, the provider draws the placeholder rectangle and emits a `unsupported MIME type` diagnostic.
## Editing layouts on the Sections tab
Layouts are paper-space tabs on the existing **Sections** tab workspace. Model space holds the current section's geometry (auto-updated as you navigate); paper tabs compose the PDF output.
1. Open a project. The **Sections** tab shows the current section in model space. A **Layouts** side panel docks on the right.
2. **Layouts → New Layout…** creates an empty paper tab; **Layouts → Load Layout…** loads a dormant layout already in the project.
3. Flip to the paper tab on the workspace tab strip. The side panel's palette + args form become active.
4. Draw entities on the paper tab with standard CAD tools, or click **Insert** on the palette to drop a default-sized placeholder. Tag existing entities via **Tag Selected**.
5. Fill in provider arguments in the schema-driven form. **Insert** and **Tag Selected** both read the current args.
6. The side panel's **Outline** lists every tagged entity. The loaded-layouts list lets you unload without going through the menu.
7. Navigate to another section — tagged entities on every loaded paper tab re-resolve automatically (live WYSIWYG). The viewport auto-frames the new model geometry.
8. **Layouts → Export Section to PDF…** (or `TUNPDF`) plots the active paper tab. **Layouts → Batch Export PDF…** (or `TUNPDFBATCH`) plots a station range as a single multi-page PDF.
9. The **Property Inspector** shows `Provider` and `ProviderArgs` rows for any selected entity — edit tags there too.
10. Save the project — loaded layouts capture their live state into their `IQMLBlob`s; dormant layouts pass through unchanged.
## Known limitations
- **DR data (for `DR_TABLE` and `POLAR_CHART`) is not wired.** Radial deviations come from the analytics engine; Plan 3 will integrate them. For now both providers emit a warning and draw a placeholder frame.
- **No ICAD command-registry integration yet.** `TUNPDF` / `TUNPDFBATCH` / `TUNLAYOUT*` are plain procedures invoked from menu items; typing them at the command line isn't wired until TunnelCAD adopts ICAD's command registry.
- **Sheet sets / per-section booklet output are out of scope.** Use the Section DXF Atlas export (right-click a tunnel → *Export Sections to DXF…* → atlas mode) when you need a multi-section sheet layout.
## Verifying the editor (manual smoke test)
After building TunnelCAD locally:
1. Open TunnelCAD, create a new project → Sections tab active, Layouts side panel visible, no layouts loaded.
2. **Layouts → New Layout…** → type a name → empty paper tab appears on the workspace tab strip.
3. Flip to the paper tab. Click `DR_TABLE` in the palette → click **Insert** → a rectangle appears tagged with `DR_TABLE`. The outline lists it. The inspector shows `Provider=DR_TABLE`.
4. Navigate to a section that has data → the DR_TABLE rectangle refreshes (placeholder content until Plan 3 wires analytics).
5. **Layouts → Export Section to PDF…** → pick an output path → a valid single-page PDF is produced.
6. **Layouts → Batch Export PDF…** → pick the loaded layout + a station range → a multi-page PDF is produced.
7. Save project, close, reopen → the layout is present in the loader picker (dormant). **Layouts → Load Layout…** restores it with the same content.
## Commands
- `TUNPDF` — plot the active paper tab on the Sections workspace to PDF. Wired as `ExecuteTunPDFCommand(Owner, Project, Section, Session)` in `Print/UCmdTunPDF.pas`.
- `TUNPDFBATCH` — plot a station range as a single multi-page PDF. Wired as `ExecuteTunPDFBatch(Owner, Project, Session, Navigate)` in `Print/UCmdLayouts.pas`.
- `TUNLAYOUTLOAD` / `TUNLAYOUTUNLOAD` / `TUNLAYOUTNEW` / `TUNLAYOUTDELETE` / `TUNLAYOUTIMPORT` / `TUNLAYOUTEXPORT` — the load/unload lifecycle.
## Extending the engine
To add a new provider — for example, a custom chart type:
1. Create a new `Print/UProviderX.pas` unit implementing `IProvider` (name, role, `Resolve(Ctx, Slot, Canvas, Args, Result, LayoutName)`).
2. Call `RegisterProvider(TProviderX.Create)` — usually from a `RegisterProviderX` procedure invoked at app startup.
3. Author a layout in ICAD with a tagged entity using your new provider name and args.
4. That's it — the engine picks it up automatically.
See `Print/UProviderText.pas` and `Print/UProviderSectionView.pas` for worked examples of paper-role and model-role providers respectively.
## Design documents
- Spec: `docs/superpowers/specs/2026-04-18-tunnel-pdf-export-design.md`
- Plan 1: `docs/superpowers/plans/2026-04-18-tunnel-pdf-export-plan-1-engine-skeleton.md`
- Layout Editor: `docs/superpowers/plans/2026-04-18-tunnel-layout-editor-plan.md`