Calco

Rendering documents

Rendering is the core job of this service: you give it a template (an .xlsx or .docx file) plus a JSON blob of data, and it gives you back a finished document in the format you ask for — a PDF, a self‑contained HTML page, image pages, or the filled‑in source file itself.

This page explains how the render pipeline behaves today, in the web UI, the CLI, and the HTTP API. For the machine‑facing request/response reference, see the API reference.

The mental model

Every render is the same three‑part equation:

template (.xlsx / .docx)  +  data (JSON)  →  output (pdf / html / jpg / webp / original)
  1. Template — an Office document with {d.*} placeholders where dynamic values should go. The service auto‑detects whether it is a spreadsheet (XLSX) or a Word document (DOCX) by looking inside the file; you never have to tell it.
  2. Data — a JSON object. Each {d.field} placeholder is replaced by the matching value. Data is optional: render a template with no data and you get the template's literal text.
  3. Output format — what you want back. One of original, pdf, html, jpg, or webp (see Output formats).

The same template can be rendered to any output format with the same data — the format is just a parameter on the request.

Output formats

The format parameter selects what comes back. It accepts five values:

formatWhat you getMIME type
originalThe source file with data substituted — a filled‑in .xlsx or .docx. The shape of the document is untouched; only {d.*} placeholders change.…spreadsheetml.sheet / …wordprocessingml.document
pdf (default)A print‑ready PDF. Can be digitally signed in the same request (see Signing).application/pdf
htmlA self‑contained HTML page — one inline SVG per page, no external assets, opens straight in a browser.text/html; charset=utf-8
jpgRasterised JPEG image pages.image/jpeg (single page) / application/zip (multi‑page)
webpRasterised WebP image pages (lossless VP8L).image/webp (single page) / application/zip (multi‑page)

format=csv is rejected with HTTP 400. It was removed deliberately — CSV cannot represent the styled, paginated layout the renderer produces. Use original if you need the raw spreadsheet data.

Single page vs. multi‑page images

For jpg and webp, the response shape depends on the page count:

  • One page → the bare image is returned directly (image/jpeg / image/webp).
  • More than one page → the pages are packed into a ZIP archive (application/zip) with entries named pages/page-001.jpg, pages/page-002.jpg, … (zero‑padded to the total page count).

This keeps simple one‑page callers trivial while still handling long documents.

Template data substitution

Placeholders use Carbone‑style {d.*} syntax. Pass your data as a JSON object (the service automatically wraps a plain object so {d.company} resolves against the top level).

json
{ "company": "Acme Corp", "invoice_no": "INV-001", "paid": true }
PlaceholderResolves to
{d.company}Acme Corp
{d.invoice_no}INV-001
{d.customer.name}nested field lookup with dotted paths

Conditionals

Placeholders can carry inline conditional operators:

SyntaxBehaviour
{d.field:ifEQ('x'):show('yes'):elseShow('no')}Show yes when field == x, otherwise no
{d.field:ifNE('x'):show('yes')}Show yes when field != x

Loops and images (DOCX)

DOCX templates additionally support repetition blocks — {#items}…{/items} expands once per element of an array — and image injection, where a {d.*} value that points at an image URL/path is embedded into the document's media folder. Word frequently splits a single placeholder across multiple internal runs as you edit it; the converter re‑joins those runs before substituting, so placeholders survive ordinary editing.

What happens under the hood

You don't need to know this to use the service, but it explains the formats' characteristics.

XLSX pipeline — the spreadsheet is parsed, {d.*} placeholders are replaced in cells and drawings, each sheet is laid out as an intermediate styled HTML table (colours, borders, merged cells, absolutely‑positioned images and shapes), paginated to the chosen page size, and handed to LibreOffice to produce the PDF.

DOCX pipeline — the OOXML is unzipped, runs are merged, loops and conditionals are expanded, placeholders and images are substituted, the package is rezipped, and LibreOffice produces the PDF (or serves the filled DOCX directly for format=original).

From PDF to the other formats — html, jpg, and webp are all produced from the PDF:

  • html → the PDF is converted to one SVG per page (via pdftocairo, rendered in parallel) and assembled into a single self‑contained HTML wrapper.
  • jpg / webp → the PDF is rasterised page‑by‑page (via pdftoppm); WebP pages are re‑encoded in‑process by a pure‑Go encoder.

Image‑heavy documents can produce large html files (tens of MB) because every page is an inline SVG. That is fine for a one‑shot export, but not intended for interactive preview.

Image quality defaults

Rasterised output (jpg / webp) uses these built‑in defaults:

SettingDefaultRange
DPI150clamped to 72–300
JPEG quality851–100
WebP quality801–100 (controls compression effort; output is lossless)
Max pages200hard cap on pages rasterised

Signing PDFs

When format=pdf, the render can produce a digitally signed (PAdES) PDF in the same call — signing is the final step of the pipeline. You reference a signing certificate that was previously uploaded to your organization (see the Signing Certs API), and may supply metadata such as reason, location, contact, a timestamp‑authority (TSA) URL, and an optional visible signature rectangle. Signing applies only to pdf; it is ignored for every other format.

The CLI can also sign a PDF you already have, independently of rendering — see calco sign below.

Ways to render

Web UI

Open a template under Plantillas, choose a version, supply data, and pick the output format. The dashboard renders on demand and previews the result.

CLI

The single calco binary renders from the command line:

bash
# Convert a template to PDF with data
go run ./cmd/calco convert \
  -in examples/proalt_template.xlsx \
  -data examples/payload.json \
  -format pdf \
  -out out.pdf

# Render and sign in one step
go run ./cmd/calco convert -in template.xlsx -data payload.json \
  -format pdf -sign -cert mycert.p12 -password '…' -out signed.pdf

# Sign an existing PDF
go run ./cmd/calco sign -in in.pdf -out signed.pdf -cert mycert.p12 -password '…'

# Verify signatures in a PDF (JSON report to stdout)
go run ./cmd/calco verify -in signed.pdf

-format accepts the same values as the API (original, pdf, html, jpg, webp).

HTTP API

The relevant render endpoints (full schemas in the API reference):

MethodPathAuthPurpose
POST/convertsession / api‑keyOne‑shot stateless convert — upload a file, get a rendered document, nothing stored.
POST/api/templates/{id}/versions/{version}/rendersessionRender a stored template version.
GET/api/templates/{id}/versions/{version}/downloadsessionSame render, parameters as query string (easy to link).
POST / GET/api/render/{hash}api‑keyHash‑first machine endpoint — identify the template version by its file SHA‑256 and render it.
GET/api/render/{hash}/downloadapi‑keyHash‑first render via query string.
POST/api/batch-rendersessionMail‑merge — render one template against many records, get a ZIP back.

Common request fields (multipart form or query): format, data_json, sheet (XLSX), title, plus the sign_* fields when signing. The response carries a Content-Disposition filename derived from the title or upload name.

Hash‑first workflow

For machine‑to‑machine integration, address templates by the SHA‑256 hash of the file rather than by an ID you have to store:

  1. Upload a template once → receive its hash.
  2. Reference it forever by that hash.
  3. Use HEAD /api/render/{hash} to cheaply check whether a version is already registered before re‑uploading.

The hash is looked up within the API key's organization, so the same hash can map to different templates in different orgs.

Batch render

POST /api/batch-render renders the same template once per record and returns a ZIP. Each item in records_json is shaped {hash, title?, data}. Records that fail are collected into an errors.csv (columns index,hash,error) at the archive root instead of aborting the whole job. Limits are configurable via BATCH_* environment variables (max items, max distinct templates, max archive size, per‑request timeout); query the current limits with GET /api/batch-render/formats.

Render caching

Stored‑template renders are cached: an identical render (same template, version, data, sheet, title, and format) is served from cache and the response carries X-Render-Cache: hit. The cache driver is pluggable (none, memory, local, s3).

Full API & OpenAPI spec

The complete machine‑facing API — every endpoint, parameter, and response schema, plus how to download the OpenAPI document and import it into Postman — lives on the dedicated API reference page.