# Text-Only Template Format (MVP) 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 `` 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. - Exactly one of: - 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. Interpolating a column into a literal string (e.g. `"Dear {FirstName},"`) is out of scope for this template format — see the deferred story "Create a dynamic text token from a CSV column." A `column` that isn't a real CSV header fails the whole run before any page is rendered, not partway through a large batch. - `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 the element around the center of its own bounding box (not around `x`/`y`) 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. 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. 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. ## 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.