Open Gauge
Calibration

Certificate templates

Uploading and authoring custom LaTeX templates for calibration certificates.

An admin can replace the built-in certificate design with a custom .tex template — for localization, a different layout, or matching an existing accredited-lab certificate format — without touching any code. Templates are managed from Admin Panel → Certificate Templates.

How rendering works

A template is a .tex file using Jinja placeholders with LaTeX-safe delimiters (Jinja's default {{ }}/{% %} collide with LaTeX's own {} group syntax):

Jinja constructLaTeX-safe spelling
Variable\VAR{asset_name}
Block (if/for)\BLOCK{if asset_notes} ... \BLOCK{endif}
Comment\#{ a comment }

Every free-text placeholder must go through the latex filter — \VAR{asset_name\|latex} — which escapes characters LaTeX treats specially (\ { } $ & # ^ _ % ~). Numeric/pre-formatted fields are safe either way; image paths (see below) must not be filtered, since they're plain filenames, not text to render. The same applies to any placeholder documented below as "raw LaTeX math" (coefficient_rows[].name, function_formula) — they contain literal $...$ math syntax and must be used unfiltered, or the escaping would break the math mode.

The rendered .tex is compiled to PDF by Tectonic, a small self-contained LaTeX engine (not a full TeX Live install) — see Deployment for the Docker image details. Tectonic runs on XeTeX, which is Unicode-native; the built-in template uses fontspec with DejaVu Sans (bundled with the app image) so accented names, degree signs, ±, etc. all render correctly without any special escaping beyond the latex filter.

Uploading a template

From Admin Panel → Certificate Templates, choose a scope (a specific organization, or Global for the fallback used by any organization without its own default — global templates require a superadmin, since they affect every organization), upload the .tex file, give it a name, and optionally mark it as that scope's default.

Every upload is validated with a dry-run compile (dummy data) before being saved — a template with a LaTeX syntax error or a typo'd placeholder is rejected immediately, with the compiler's error message, rather than only failing the next time a real certificate is generated.

Replacing a template's content isn't an in-place edit — upload it again as a new template and promote it to default. The old version stays around (soft-deleted, not default) so past certificates that reference it conceptually remain traceable to what generated them.

Previewing a template

Every template row (and the app's built-in default) shows a small thumbnail of its first page, rendered client-side from a freshly randomized 10-row sample calibration dataset (a synthetic linear fit with realistic noise) generated fresh on every load — a close approximation of what a real certificate looks like without needing a real calibration on hand. Click the thumbnail to open a full-size, scrollable preview of every page, with a Download button to save the rendered sample PDF.

Placeholder reference

Identity / header

PlaceholderTypeNotes
certificate_numbertexte.g. OG-CAL-OG-00001-v3
org_nametextFalls back to "Open Gauge" if unresolved
org_logo_pathimage path, optional\BLOCK{if org_logo_path}\includegraphics{\VAR{org_logo_path}}\BLOCK{endif}
performer_nametext
performer_signature_pathimage path, optionalThe performing user's signature, if set
calibration_date, due_datetextPre-formatted date strings
versionintegerAlso usable as a document "revision number"
calibration_typetext
chaptertextThe channel's physical quantity (e.g. "Pressure"), or the asset type if there's no channel — useful for document-control-style headers grouping certificates by measurement type
calibration_idtextShort identifier (first 8 chars of the calibration's UUID, uppercased) distinct from certificate_number
generated_datetextToday's date (when the certificate is being generated), distinct from calibration_date
qr_pathimage path, optional — QR code linking to the asset

Asset / channel

PlaceholderType
asset_id, asset_name, asset_manufacturer, asset_model, asset_serial, asset_part_number, asset_type_labeltext
asset_notestext, optional
channelobject, optional — channel.id, .quantity, .unit, .min, .max, .accuracy, .range (pre-formatted "min–max unit"), .signal (pre-formatted electrical output range, or "—"), .precision (sensor's stated measurement uncertainty, or "—"), .resolution (or "—")

Traceability

PlaceholderType
external_lab_name, external_lab_certificate_numbertext, optional
procedureobject, optional — .id, .name, .version, .standard_ref
reference_assetobject, optional — .id, .name, .manufacturer, .model, .serial
temperature, humidity, pressuretext, optional (°C / %RH / Pa)
calibration_locationtext, optional — name of the location the calibration was performed at, if assigned

Results

PlaceholderType
coefficient_rowslist of {coefficient, name, term, value}name is a human label with a math-mode subscript, e.g. "Offset ($a_{0}$)"; since it's raw LaTeX math, reference it as \VAR{row.name} without the latex filter (escaping would mangle the $/_/{})
function_formulatext, optional — the generic (non-numeric) calibration function in LaTeX math syntax, e.g. "f(x) = a_0 + a_1 x"; also raw LaTeX math — use \VAR{function_formula} unfiltered, typically wrapped in \[...\]
equationtext, optional — the same function with actual fitted values substituted, e.g. "y = -0.047 + 1.0004*x"
stat_rowslist of {label, value} (R², RMSE, uncertainty, etc.)
results_summarylist of {label, value} — compact 3-row Max Error / % Full-Scale Error / Expanded Uncertainty table
error_summaryobject, optional — .abs_error, .fs_error, .uncertainty, each pre-formatted and signed with ± (e.g. "±0.12 %RH") — a 3-column single-row alternative to results_summary
uncertainty_budget_rowslist of {source, distribution, standard_uncertainty, dof}
effective_dof_notetext, optional
uncertainty_statementtext, optional — full GUM §7.2.4-style sentence
conformityobject, optional — .result_label, .specification, .decision_rule_label, .expanded_uncertainty_line
conformity_derivationtext, optional — narrates max error + expanded uncertainty against the specification and states the pass/fail conclusion (e.g. "...totals 0.28 %RH, against a specification of ±0.8 (absolute) — the calibration CONFORMS..."); None unless both a conformity assessment and error/uncertainty values exist
valid_rangetext, optional
notestext, optional

Dataset

PlaceholderType
dataset_rowslist of {index, measured, reference, fit, residual_abs, residual_pct}
measured_unit, reference_unittext
chart_pathimage path, optional — scatter + fit-curve chart
lab_footertext, optional — printed in the page footer

Recipes: common patterns

Every pattern below is copy-pasteable — each is lifted directly from default.tex.jinja or example_tables.tex (see Example templates in the repo), so it's guaranteed to compile.

Plain text field, escaped

The overwhelming majority of placeholders are plain text and must go through the latex filter:

\textbf{Asset ID:} \VAR{asset_id|latex}
\textbf{Manufacturer:} \VAR{asset_manufacturer|latex}

Forgetting |latex on free text is the most common template bug — it compiles fine until a real asset name contains &, %, _, or #, at which point LaTeX either errors or silently mangles the output. Numeric/pre-formatted fields (version, calibration_id, table values already formatted by the backend) don't need it, but adding it anyway is harmless — the filter only touches the handful of characters LaTeX treats specially.

Optional field — text

Most fields can be None/empty (e.g. an asset with no notes, a calibration with no external lab). Guard them with \BLOCK{if ...} so the line disappears instead of printing "None" or a stray label with nothing after it:

\BLOCK{if asset_notes}
\small\textbf{\color{oggray}Notes:} \VAR{asset_notes|latex}\normalsize
\BLOCK{endif}
\BLOCK{if temperature or humidity or pressure}
Environmental Conditions:\ %
\BLOCK{if temperature}Temperature: \VAR{temperature|latex}~\textdegree C\BLOCK{endif}
\BLOCK{if humidity}\ \textbullet\ Humidity: \VAR{humidity|latex}~\%RH\BLOCK{endif}
\BLOCK{if pressure}\ \textbullet\ Pressure: \VAR{pressure|latex}~Pa\BLOCK{endif}
\BLOCK{endif}

Optional field — image

Image placeholders (org_logo_path, performer_signature_path, qr_path, chart_path) are None whenever that asset doesn't exist (no logo uploaded, no signature on file, chart couldn't be rendered) — always guard \includegraphics the same way, and never pass an image path through |latex (it's a plain filename, not text):

\BLOCK{if performer_signature_path}
\includegraphics[width=40mm]{\VAR{performer_signature_path}}\\[-1mm]
\rule{40mm}{0.4pt}\\
\footnotesize\VAR{performer_name|latex}
\BLOCK{endif}

Optional field — nested object

channel, procedure, reference_asset, conformity are None as a whole object when that data doesn't apply to the calibration — guard the object, then access its attributes freely inside the block without re-checking each one:

\BLOCK{if channel}
\textbf{Channel} & \textbf{Quantity} & \textbf{Unit} & \textbf{Min} & \textbf{Max} & \textbf{Accuracy}\\
\VAR{channel.id|latex} & \VAR{channel.quantity|latex} & \VAR{channel.unit|latex} & \VAR{channel.min|latex} & \VAR{channel.max|latex} & \VAR{channel.accuracy|latex}\\
\BLOCK{endif}
\BLOCK{if procedure}
\textbf{Procedure ID} & \VAR{procedure.id|latex} & \textbf{Procedure Name} & \VAR{procedure.name|latex}\\
\textbf{Procedure Version} & \VAR{procedure.version|latex} & \textbf{Standard Ref.} & \VAR{procedure.standard_ref|latex}\\
\BLOCK{endif}

Looping a list of rows

coefficient_rows, stat_rows, results_summary, uncertainty_budget_rows, and dataset_rows are all lists of small objects — loop with \BLOCK{for ... in ...}\BLOCK{endfor}, one table row per iteration:

\begin{tabular}{@{}p{24mm}@{}p{20mm}@{}p{56mm}@{}}
\toprule
\textbf{Coefficient} & \textbf{Term} & \textbf{Value}\\
\midrule
\BLOCK{for row in coefficient_rows}
\VAR{row.coefficient|latex} & \VAR{row.term|latex} & \VAR{row.value|latex}\\
\BLOCK{endfor}
\bottomrule
\end{tabular}

The same loop works for stat_rows (a flatter {label, value} shape) and dataset_rows (more columns) — only the column list inside the loop body changes:

\BLOCK{for row in stat_rows}
\VAR{row.label|latex} & \VAR{row.value|latex}\\
\BLOCK{endfor}
\BLOCK{for row in dataset_rows}
\VAR{row.index|latex} & \VAR{row.measured|latex} & \VAR{row.reference|latex} & \VAR{row.fit|latex} & \VAR{row.residual_abs|latex} & \VAR{row.residual_pct|latex}\\
\BLOCK{endfor}

Combine a list-guard with the loop when the whole table should disappear if there's no data at all (rather than rendering an empty tabular with just a header row):

\BLOCK{if uncertainty_budget_rows}
\begin{tabular}{@{}p{48mm}@{}p{28mm}@{}p{45mm}@{}p{20mm}@{}}
\toprule
\textbf{Source} & \textbf{Distribution} & \textbf{Std. Uncertainty (u)} & \textbf{dof}\\
\midrule
\BLOCK{for row in uncertainty_budget_rows}
\VAR{row.source|latex} & \VAR{row.distribution|latex} & \VAR{row.standard_uncertainty|latex} & \VAR{row.dof|latex}\\
\BLOCK{endfor}
\bottomrule
\end{tabular}
\BLOCK{endif}

Raw LaTeX math — do NOT filter

coefficient_rows[].name and function_formula are the two exceptions to "always use |latex" — they contain literal LaTeX math syntax ($a_{0}$, f(x) = a_0 + a_1 x) generated by the backend specifically to be dropped into math mode as-is. Filtering them would escape the $, _, and {} characters that make them render as math in the first place:

% Wrong — the escape filter mangles the math syntax:
\VAR{function_formula|latex}

% Right:
\[ \VAR{function_formula} \]
\BLOCK{for row in coefficient_rows}
\VAR{row.coefficient|latex} & \VAR{row.name} & \VAR{row.value|latex}\\
\BLOCK{endfor}

Note row.name (unfiltered, raw math) sitting next to row.coefficient|latex and row.value|latex (filtered, plain text) in the same row — only the field documented as "raw LaTeX math" in the placeholder reference skips the filter.

A minimal working template

Everything above assembled into the smallest template that still produces a usable certificate — strip this down further or use it as a starting skeleton:

\documentclass[11pt]{article}
\usepackage[a4paper,margin=20mm]{geometry}
\usepackage{fontspec}
\setmainfont{DejaVu Sans}
\usepackage{graphicx}
\usepackage{booktabs}

\begin{document}

{\Large\bfseries \VAR{org_name|latex}} \hfill \VAR{certificate_number|latex}

\vspace{4mm}
\textbf{Asset:} \VAR{asset_id|latex} --- \VAR{asset_name|latex}\\
\textbf{Calibrated:} \VAR{calibration_date|latex} \quad \textbf{Due:} \VAR{due_date|latex}\\
\textbf{Performed by:} \VAR{performer_name|latex}

\BLOCK{if coefficient_rows}
\vspace{4mm}
\begin{tabular}{ll}
\toprule
\textbf{Coefficient} & \textbf{Value}\\
\midrule
\BLOCK{for row in coefficient_rows}
\VAR{row.coefficient|latex} & \VAR{row.value|latex}\\
\BLOCK{endfor}
\bottomrule
\end{tabular}
\BLOCK{endif}

\BLOCK{if performer_signature_path}
\vspace{8mm}
\includegraphics[width=35mm]{\VAR{performer_signature_path}}
\BLOCK{endif}

\end{document}

Every generated certificate PDF — regardless of which template produced it, including this minimal one — is digitally signed after compilation; see Certificate digital signatures for how that works and how to verify it.

Example templates in the repo

Two complete, working templates ship in apps/api/app/templates/certificates/ as reference material — both compile as-is and can be uploaded directly to try out:

  • default.tex.jinja — the built-in fallback. The canonical, always-up-to-date example of every placeholder in use.
  • example_tables.tex — a denser, document-control-style alternative to the built-in default, covering the same content but laid out differently:
    • A document-control header (doc number, chapter, revision, sensor/calibration IDs, page) instead of a plain letterhead.
    • Asset & channel information in three columns — a QR code of the asset, then the asset fields grouped two-per-column instead of split across separate label/value columns — plus a channel specification table covering measurement range, signal output range, accuracy, precision, and resolution.
    • Fitted coefficients shown as a symbolic function (function_formula, e.g. f(x) = a_0 + a_1 x) next to a table naming each coefficient (Offset, Gain, ...) with its value, rather than a single numeric equation.
    • A three-column Error & Uncertainty summary (Abs. Error / FS Error / Uncertainty) in one row, followed by the GUM explanatory paragraph and a conformity-derivation sentence that states the pass/fail conclusion against the numbers on the page.
    • A full-width dataset table with a two-row header (column name, then unit) and no chart.
    • A three-column footer repeated on every page: the preparer's name and signature on the left, a plain ISO/IEC 17025 issuance statement in the center (no conformity badge — the pass/fail conclusion lives in the results section instead), and the asset's organization name on the right.

On this page