Chapter 7. Template Format Reference
Chapter 7. Template Format Reference
7.1 Overview and storage
One template = one JSON document + a resource directory (UTF-8, no binary inlining — images and other assets sit alongside and are referenced by path):
~/.wxprint/templates/<id>/
├── template.json # main template document (this specification)
├── waybill-scan.png # scanned background (design-time aid)
└── stamp.png # template asset image
| Dimension | Overlay template | HTML template |
|---|---|---|
| Positioning | Absolutely positioned elements, mm coordinates | Document flow (HTML+CSS) |
| Best for | Pre-printed forms, waybills, labels, tickets | Reports, statements, manuals |
| Data injection | {{variables}} inside elements |
Twig template ({{ }} / {% for %}) |
| Pagination | One page per record (per the template page) | Automatic slicing by page height + break classes |
Templates come from three sources: the local directory (scanned and
registered at server start), POST /api/templates uploads, or a URL
fetched at print time (Section 4.5).
7.2 Top-level fields
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | ✓ | Unique identifier (directory name), ^[a-z0-9][a-z0-9._-]{0,63}$ |
name |
string | ✓ | Display name |
kind |
string | ✓ | "overlay" / "html" |
version |
int | ✓ | Format version, currently 1 |
page |
object | ✓ | Page definition (Section 7.3) |
background |
object | Background and calibration (Section 7.9, overlay only) | |
elements |
array | ✓ | Element list (Section 7.4 onward, overlay only) |
entry / assets |
string / array | HTML template entry and assets (Section 7.8) | |
calibration_offset |
object | Template-level overlay tweak {x, y} mm (stacked on printer calibration) |
|
meta |
object | Free-form extension (author/notes/business tags) |
7.3 Page definition page
| Field | Type | Default | Description |
|---|---|---|---|
width_mm / height_mm |
number | — | Physical paper width/height, any value (fixed-length continuous forms such as 100×93.5; height_mm: 0 = fit content height, overlay only) |
orientation |
string | portrait |
portrait / landscape (a convenience that swaps width/height) |
continuous |
bool | false | Continuous feeding: gap-free pages, output uses the custom paper form |
margin_mm |
object | 0 | {top, bottom, left, right}: overlay = designer safe-area hint only; html = content-box margins (the page keeps its full size) |
size |
string | — | HTML template paper preset ("A4", …; alternative to width/height) |
Coordinate system: origin = the physical top-left corner of the paper; x rightward, y downward; millimeters with two decimals (0.01mm precision). Precision chain: template coordinates → ×72/25.4 → PDF points (exact rational conversion) → 1:1 unscaled output → mechanical error absorbed by printer calibration (Section 7.10).
7.4 Common element fields
Fields shared by all elements:
| Field | Type | Default | Description |
|---|---|---|---|
type |
string | ✓ | text / line / rect / image / barcode / qrcode / table / chart |
x, y, w, h |
number | ✓ | Element box (mm) |
rotate |
number | 0 | Rotation (degrees, clockwise around the box center — slanted text such as bank slips) |
id |
string | — | Element identifier (anchor/reference); elements without an id are referenced by index |
scope |
string | body |
page-header / page-footer (drawn on every page), first-page / last-page (first/last page only) |
7.5 text
{ "type": "text", "x": 42.5, "y": 60.0, "w": 60, "h": 6,
"text": "{{sender.name}}, {{sender.phone}}",
"font": { "family": "", "size_pt": 10, "weight": 400, "italic": false,
"color": "#000000", "letter_spacing_em": 0, "line_height": 1.2 },
"align": "left", "valign": "top", "origin": "top-left", "overflow": "shrink" }
| Field | Type | Default | Description |
|---|---|---|---|
text |
string | ✓ | Content: variables + filters + literals; \n line breaks |
font.family |
string | "" |
Empty = system default stack (same stack as the HTML pipeline); alternates list allowed |
font.size_pt |
number | 10 | Font size in points (the printing convention — mm coordinates coexist with pt sizes in overlay work) |
font.weight |
int | 400 | Weight; 700 = bold |
font.italic / font.underline |
bool | false | Italic / underline |
font.color |
string | "#000000" |
Foreground #rrggbb |
font.background |
string | null | Background #rrggbb; null = transparent (not drawn) |
font.letter_spacing_em |
number | 0 | Letter spacing (em multiples) |
font.line_height |
number | 1.2 | Line-height multiple (effective with overflow: wrap) |
align |
string | left |
left / center / right (in-box horizontal) |
valign |
string | top |
top / middle / bottom (in-box vertical) |
origin |
string | top-left |
top-left = y is the box top; baseline = y is the first line’s baseline — text lands exactly inside grid lines regardless of font-size/line-height tweaks (invoice amount cells, voucher grids) |
overflow |
string | shrink |
shrink (recommended; binary-search font size until it fits, minimum configurable) / clip / wrap |
7.6 Variables and filters
- Syntax
{{path}}; dot paths{{a.b.c}}, array indices{{items.0.qty}}. - Filter chains
{{v|f1|f2:arg}}:
| Filter | Example | Description |
|---|---|---|
date |
{{created_at\|date:"Y-m-d H:i"}} |
PHP/Twig format characters (Y m d H i s); timestamps or ISO strings in |
number |
{{amount\|number:2}} |
Decimals + thousands separators; a negative argument = decimals only |
upper / lower |
{{name\|upper}} |
Case conversion |
default |
{{remark\|default:"--"}} |
Fallback for empty values |
truncate |
{{addr\|truncate:20}} |
Truncate with an ellipsis |
pad |
{{no\|pad:8}} |
Left-pad with zeros (order numbers) |
- Built-in page variables
{{page}}/{{pages}}(current page / total pages): valid only insidescope: page-header/ page-footerelements. - Missing variables: rendered as empty strings and listed in the
job’s
warnings(Section 4.7). - Overlay text is not HTML-escaped (not an HTML context); the
HTML pipeline escapes by default with
|rawas the opt-out.
7.7 line / rect / image / table / chart
{ "type": "line", "x": 10, "y": 55, "w": 190, "h": 0,
"stroke_mm": 0.3, "color": "#000000", "dash": [2, 1] }
{ "type": "rect", "x": 10, "y": 15, "w": 190, "h": 35,
"stroke_mm": 0.3, "fill": null, "corner_mm": 1 }
{ "type": "image", "x": 5, "y": 5, "w": 20, "h": 20,
"src": "stamp.png", "fit": "contain" }
{ "type": "table", "x": 10, "y": 100, "w": 190,
"columns": [
{ "header": "Item", "key": "name", "width": 0.5, "align": "left" },
{ "header": "Qty", "key": "qty", "width": 0.2, "align": "right" } ],
"rows": "{{items}}", "row_height_mm": 6, "header_rows": 1,
"grid": { "stroke_mm": 0.2, "color": "#000000" },
"font": { "size_pt": 9 } }
| Element | Field essentials |
|---|---|
line |
stroke_mm width, color, dash pattern (e.g. [2,1]) |
rect |
stroke_mm border, fill color (null = transparent), corner_mm rounding, border color |
image |
src: template-relative asset path / http(s) / data: URL (fetched at print time under size/timeout limits); fit: contain / stretch |
table |
columns[].width is a width fraction (sum = 1±0.001); rows binds an array; cells support variables and filters; zebra via odd_color/even_color; v1 uses fixed row heights without page spanning (overlong rows truncate with a warning) — use the HTML pipeline for long cross-page tables |
chart |
kind: bar/hbar/line/pie/funnel/gauge/radar/scatter/histogram/candlestick/parallel/polar_bar/theme_river (13 types); categories/series bindings; show_legend/title/theme |
7.8 barcode / qrcode, and HTML templates
Barcode and QR code:
{ "type": "barcode", "symbology": "code128", "x": 10, "y": 20, "w": 80, "h": 12,
"content": "{{order.no}}", "show_text": true, "text_size_pt": 7 }
{ "type": "qrcode", "x": 170, "y": 20, "size": 22, "content": "{{order.url}}",
"ec_level": "M", "quiet_zone_mm": 2 }
| Field | Description |
|---|---|
symbology |
code128 / ean13 / ean8 / code39 / itf14 / upca; drawn as vector paths, no bitmap jaggies |
w / h (barcode), size (QR) |
Target physical size (mm); module widths snap to device pixels to avoid smudged codes |
ec_level |
QR error correction: L(7%) / M(15%) / Q(25%) / H(30%) |
quiet_zone_mm |
QR quiet-zone margin |
HTML templates (kind: "html"):
{ "id": "monthly-report", "kind": "html", "entry": "report.html.twig",
"engine": "twig",
"assets": ["styles.css", "logo.png"],
"page": { "size": "A4", "margin_mm": { "top": 15, "bottom": 15,
"left": 12, "right": 12 } },
"sample_data": "sample.json" }
entryis a Twig template:{{ }}interpolation,{% for %}/{% if %}control, common filters (date,number_format,nl2br,raw,escape) and theincludefunction. All data loading happens server-side; templates contain no JavaScript (<script>is ignored outright).- Pagination control: the CSS classes
.wxprint-page-break(force a break before an element) and.wxprint-avoid-break(row-level orphan avoidance). - The CSS support is a modern subset: box model, tables, flex, float,
fonts/colors/backgrounds, zebra stripes;
@pagepaged media is not supported (use the break classes).
7.9 Background calibration (design time)
The background only serves as a tracing aid inside the designer; calibration establishes the pixel→millimeter mapping so that drag-placed coordinates are physical millimeters right away. Three modes by scan quality:
| Mode | Input | Conversion | Best for |
|---|---|---|---|
physical-size |
The scan’s physical width/height in mm | Uniform px_per_mm = img_px_w / width_mm |
Clean, undistorted scans |
reference |
A line of known physical length in the image (two points) | px_per_mm = pixel distance / known mm |
A known reference object |
affine-3pt |
≥3 points with their physical mm coordinates | Solves 2D affine (6 parameters; least squares for >3) | Skewed/perspective scans |
"background": {
"image": "waybill-scan.png",
"calibration": { "type": "affine-3pt", "points": [
{ "px": [18, 22], "mm": [0, 0] },
{ "px": [2010, 25], "mm": [209.8, 0.3] },
{ "px": [21, 2831], "mm": [0.2, 296.5] } ] },
"opacity": 0.6, "print": false }
print: false (default) = overlay semantics, the background is not
printed; true is only for proofing. The calibration result is
stored in the template together with the raw inputs; the background
takes no part at run time.
7.10 Printer calibration (run time)
Per-printer calibration (absorbs the systematic part of the ±0.1~0.3mm mechanical repeatability):
~/.wxprint/calibration/<printer>.json
{ "offset_mm": { "x": 0.35, "y": -0.20 }, "scale": { "x": 1.0002, "y": 1.0002 } }
Source: POST /api/printers/{name}/calibration-page prints the
calibration page (crosshair rules + an mm grid) → measure the
deviation → PUT /api/printers/{name}/calibration writes it back
(endpoints in Section 4.10). Applied to all element coordinates as
p' = offset + scale·p before rendering.
Render stacking order: template coordinates → printer calibration
(global) → template-level calibration_offset ({x, y} mm, a
per-template tweak for a specific machine).
7.11 Validation rules (on upload/registration)
idnaming rules (^[a-z0-9][a-z0-9._-]{0,63}$);- semantic checks (
Template::warnings): coordinate/size legality, column-width sums, unresolved variables, … — returned as a warning list with the registration response and in jobs (non-blocking); - an invalid
idor a directory-creation failure returnsE_TEMPLATE_INVALID.
7.12 Complete example (express waybill, continuous paper)
{
"id": "sf-waybill-100x93",
"name": "SF waybill 100×93.5 continuous",
"kind": "overlay",
"version": 1,
"page": { "width_mm": 100, "height_mm": 93.5, "continuous": true },
"background": {
"image": "scan.png",
"calibration": { "type": "physical-size", "width_mm": 100, "height_mm": 93.5 },
"print": false },
"elements": [
{ "type": "barcode", "symbology": "code128", "x": 8, "y": 6, "w": 60, "h": 10,
"content": "{{waybill_no}}", "show_text": false },
{ "type": "qrcode", "x": 74, "y": 4, "size": 18, "content": "{{pickup_url}}",
"ec_level": "M" },
{ "type": "text", "x": 8, "y": 20, "w": 40, "h": 5, "origin": "baseline",
"text": "{{receiver.name}} {{receiver.phone}}",
"font": { "size_pt": 9, "weight": 700 } },
{ "type": "text", "x": 8, "y": 26, "w": 84, "h": 10, "overflow": "shrink",
"text": "{{receiver.province}}{{receiver.city}}{{receiver.district}}{{receiver.addr}}",
"font": { "size_pt": 9 } },
{ "type": "table", "x": 8, "y": 40, "w": 84, "row_height_mm": 5.5,
"header_rows": 1,
"columns": [
{ "header": "Item", "key": "name", "width": 0.6 },
{ "header": "Qty", "key": "qty", "width": 0.2, "align": "right" },
{ "header": "Insured", "key": "insured", "width": 0.2, "align": "right" } ],
"rows": "{{items}}" },
{ "type": "text", "x": 8, "y": 88, "w": 50, "h": 4, "origin": "baseline",
"text": "{{printed_at|date:\"Y-m-d H:i\"}} from {{sender.name}}",
"font": { "size_pt": 7 } }
]
}