# Envelope Template Format
Implementation: [`src/EnvelopeRenderer.Cli/Render`](src/EnvelopeRenderer.Cli/Render). Sample:
[`sample-data/sample-envelope-template.xml`](sample-data/sample-envelope-template.xml).
```xml
Static label:
Rotated label
```
## ``
- `pageWidth`, `pageHeight` — required, PDF points. **Not** a named paper size: Debenu Quick PDF
10.13's only named-size function (`SetPageSize`) covers ISO/ANSI paper sizes and DL, but no US
envelope sizes, so this template format goes straight to `SetPageDimensions(width, height)`
instead and lets the operator supply exact points for any envelope. A #10 envelope
(4.125" x 9.5") is `pageWidth="297" pageHeight="684"` (1" = 72pt).
## ``
One page is rendered per CSV record, in the order the CSV rows appear, each carrying every
`` and `` element in the template.
- `x`, `y` — required, PDF points, measured from the page's **bottom-left** corner (Debenu's
default coordinate origin — this template format doesn't call `SetOrigin`, so that default
holds).
- `font` — required. A TrueType font name as it appears in the Windows Fonts folder (e.g.
`"Arial"`), optionally with a style suffix: `"Arial [Bold]"`, `"Arial [Italic]"`,
`"Arial [BoldItalic]"`. The font must be installed on the machine running the CLI; it is
always embedded in the output PDF so the reader doesn't need it installed too.
- `size` — required, positive, points.
- Content is either the legacy single-run shape or the Sprint 5 multi-run shape — never both:
- inline text content — static, printed as-is on every page, e.g. `Static label:`
- `column=""` — dynamic, pulled from that column per record. Column matching is
case-insensitive.
- **Sprint 5, "Mix static text and CSV fields within a single text element":** one or more
`` child elements, each either `text=""` (printed as-is) or
`column=""` (resolved per record, same case-insensitive matching as the
single-`column` form above), concatenated in document order at render time into the one
string the page draws — e.g.:
```xml
```
A `` element with any `` children must have no `column` attribute and no inline
text of its own (mixing the two forms is a template error, same style as the legacy
both-column-and-inline-text error). This is a genuinely new XML shape — it never appears in
any template saved before Sprint 5 — so it cannot be confused with, and never changes the
parsing of, the legacy single-run forms above; every pre-Sprint-5 template continues to parse
and render byte-for-byte identically. Formatting/transforming a run's resolved value (date
formatting, casing, truncation) remains out of scope — verbatim substitution only, matching
the single-`column` form's existing behavior exactly.
A `column` (on either a `` element or a `` child) that isn't a real CSV header fails
the whole run before any page is rendered, not partway through a large batch — evaluated per
token when a `` element has multiple runs, so one bad field reference fails the run even
when it's mixed alongside other, valid runs on the same line.
**Desktop designer editing convention (not part of the persisted format above):** the Template
Designer's properties panel lets an operator compose mixed content by typing plain text with
`{Column Name}` bracket tokens (e.g. `Attn: {Full Name}`), parsed into the run sequence above when
the field loses focus. This is a *editing-time* convention only — the persisted XML always uses
the explicit `` shape above, never inline `{...}` text. Known limitation: literal text that
itself contains a matched `{...}` pair not intended as a field token (e.g. typing `note: {see
below}` as literal text) is indistinguishable from a real field token by this convention and will
be parsed as a field run bound to a column literally named "see below" — which then fails the
per-token pre-flight check above if no such column exists. This is an accepted trade-off of the
bracket convention (chosen as the faster-to-deliver option over a full protected-token-chip rich
editor, per this story's sizing note) rather than a silently-swallowed edge case.
- `collapsible` — optional, `true` or `false` (default `false`). Sprint 4, "Collapse blank
optional address lines consistently": when `true` and this element's resolved text is blank
for a given CSV record, the element is skipped entirely for that record's page, and every
*other* `` element that shares its `x` position (rounded to 2 decimal places) — a
"vertical stack," the closest thing this format has to an address block — shifts upward to
close the gap that line would have left. A non-collapsible blank field still renders nothing
visible at its fixed position, exactly as before this story (no change for templates that don't
opt in). See [`src/EnvelopeRenderer.Cli/Render/AddressLineCollapser.cs`](src/EnvelopeRenderer.Cli/Render/AddressLineCollapser.cs)
for the exact shift math (row height = the gap between a line and the one below it, using
original, not already-shifted, positions) and its design-decision rationale (X-position grouping
in lieu of an explicit block concept). The desktop designer's canvas previews the identical
math (`EnvelopeRenderer.Desktop.Core.Design.AddressLineCollapser`) against a loaded CSV's first
sample row, so what an operator sees while designing matches what the CLI renders.
- **Distinguishing "intentionally blank" from "mapping error":** a `column` that doesn't exist
in the CSV's header row is *always* a hard failure for the whole run (see above) — it never
silently collapses, regardless of `collapsible`. Only a column that *does* exist, but whose
value is blank for a specific record, is eligible to collapse. The desktop designer surfaces
this distinction visually: a dynamic field bound to a column that isn't among the currently
loaded CSV's headers gets a dashed orange/red highlight (a real mapping problem), while a
collapsible field bound to a real column that's simply blank in the loaded sample data gets
no warning at all — it just doesn't render, exactly as intended.
- `angle` — optional, degrees, positive or negative, default `0`. Sprint 4, "Set a rotation angle
for text and dynamic field elements": rotates text by this many degrees. **Positive is
counterclockwise**, confirmed empirically against the real Debenu Quick PDF Library 10.13 DLL
(not assumed) — see [`src/EnvelopeRenderer.Cli/Render/RotatedTextAnchorCalculator.cs`](src/EnvelopeRenderer.Cli/Render/RotatedTextAnchorCalculator.cs)'s
class remarks for the probe methodology and result.
Static text rotates around the center of its own bounding box, preserving the original Sprint 4
behavior. Internally this calls Debenu's `DrawRotatedText(x, y, angle, text)`, which rotates
around the given `(x, y)` anchor, not a center — `RotatedTextAnchorCalculator` solves for the
*different* anchor point that keeps the unrotated bounding-box center fixed, using
`GetTextWidth`/`GetTextAscent`/`GetTextDescent` to measure that box.
Dynamic and mixed-content text (any `` with a `column` attribute or at least one
field-run child) rotates around the fixed authored `(x, y)` anchor instead of a measured
bounding-box center. This Sprint 6 rule prevents record-to-record drift: resolved text width
can vary per CSV row, but the pivot cannot. The desktop canvas uses the same rule so the preview
and the rendered PDF agree on placement for variable-width rotated content. One more
empirically-confirmed vendor quirk: `DrawRotatedText` rejects a negative `Angle` outright
(despite rotation being mathematically periodic) — the renderer normalizes to Debenu's expected
`[0, 360)` range before that specific call; a negative `angle` in a template is fully supported
and unaffected by this internal normalization. An `angle` of `0` (including when the attribute
is absent) takes the exact same `DrawText` code path this renderer used before this story —
byte-for-byte unchanged output for every pre-existing template.
## ``
Sprint 6 adds a grouped Address Control: a movable address block with one anchor point and an
ordered list of child lines. The desktop designer saves it as an `` container,
and the CLI renders it directly; the desktop app is not required at render time.
- `x`, `y` — required, PDF points. This is the control's single anchor: the baseline position of
the first line.
- `width` — required, positive, PDF points. The designer uses this as the group's resize box.
Current render output does not wrap or clip text to this width; each line still draws as one
resolved text string.
- `lineSpacing` — optional, positive multiplier, default `1.25`. Line N+1's baseline is computed
from the previous line's font size: `previousY - previousSize * lineSpacing`.
- Child `` elements are required; an address control must contain at least one line.
- Each `` has required `font` and positive `size` attributes, and supports the same content
forms as ``: inline literal text, `column="CSV header"`, or explicit `` children for
mixed static/field content.
- `collapsible` on a `` is optional and defaults to `true`, unlike standalone ``,
whose default remains `false`. Set `collapsible="false"` on a line that must keep its row even
when it resolves blank.
- `angle` — optional, degrees, positive or negative, default `0`. Sprint 8, "Rotate the whole
Address Control as a single unit": rotates the **entire control** — its box, every line's text,
and any per-line highlight/selection overlay in the desktop designer — together as one rigid
unit. Same **positive is counterclockwise** convention as standalone ``'s own `angle`.
Unlike a standalone element, an Address Control's box geometry (`width`, and `height` derived
purely from each line's authored `size`/the control's `lineSpacing`) never depends on any
record's resolved text, so the whole control always rotates around its own fixed, authored box
center — no per-record pivot drift is possible here, and no fixed-anchor/bounding-box-center
branch choice like standalone text's is needed. Internally, each visible line's own
(collapse-adjusted) anchor is rotated as a rigid group around that fixed box center before being
handed to the same `DrawRotatedText` path standalone rotated text already uses — `angle="0"`
(including when the attribute is absent) takes the exact same unrotated code path as before this
story, so every template written before Sprint 8 renders byte-for-byte unchanged.
At render time, the control expands into one draw per visible line, in child-line order. Blank
collapsible lines are omitted and following lines in the same control shift upward using the same
`AddressLineCollapser` rule described for standalone `` elements; because every child line
shares the control's single `x`, the existing stack rule and the control boundary agree. Static,
field, and mixed-content lines all resolve per CSV record exactly like standalone text elements.
The rotation pivot is always computed from the control's authored, unrotated `x`/`y`/`width`/
`height`, never from any record's collapse-shifted line positions, so two records differing only
in whether a blank optional line collapses still rotate around the identical pivot.
## Known gaps
- **Font resolution failure is blocking**, not a warning — matches the Definition of Done's
"missing-font behavior remains blocking" rule. A page is never silently rendered with a
substitute font.
- Rendering requires a Debenu Quick PDF Library license key at runtime — see the "Debenu license
key" note in [`CLI_CONTRACT.md`](CLI_CONTRACT.md). Without one, every render fails at the save
step (`SaveToFile`/`SaveToString` both return error code 999) even though every earlier step
(page creation, font embedding, text drawing) succeeds — verified directly against the vendor
DLL, not assumed.