Escape
HomeDocsImages and splits

Images and splits

Intent

Band items stack, one line under the next. Two item types break out of that. A split places cells across the width, which is how an invoice gets a seller on the left and a customer on the right. An image puts host-supplied raster bytes in the flow, which is how a logo reaches the page.

Design decisions

An image source is an expression, never a literal. The bytes come from render data or from a registered function, and there is no path for the engine to fetch. A definition carries no I/O.

The bytes carry the format and the size. The engine recognises PNG and JPEG by their magic numbers, and reads the picture’s pixel dimensions from the same header. No format or size key to keep in sync with the file, and no target parses the header again for itself.

Nullish skips the item silently. A source resolving to null produces no output and no event, which is the missing-data twin of visible: false. A report whose input sometimes lacks a logo needs no guard.

Anything else invalid throws. A value that’s not a Uint8Array, or bytes that are neither PNG nor JPEG, is a located render error naming the item.

Past the size, the engine has vouched for nothing. A target has to know how big a picture is before it can place one. So a file whose header truncates before its dimensions fails on every target alike: header[0].source [=$.input.logo]: could not read the image's size from its bytes. A file whose size reads but whose pixel data is corrupt has no single answer. Each target hands those bytes to its own machinery.

There is no vector form. SVG is markup, and letting author- or data-supplied markup reach an HTML fragment would breach the escaping rule that protects every interpolated value. First-party chart packages compiling to an image item are the recorded route to vector graphics.

An image’s style vocabulary covers only what an image has. The rest of the vocabulary describes text, so any other name on an image is a definition error rather than a silent no-op.

width is a slot property, not an item property. It appears on an item only inside a split, because only there is there a width to take a share of.

Width-less slots divide what’s left, evenly. A table measures its width-less columns from their content, and a split doesn’t. A split is a line of furniture, so its geometry reads off the document rather than off the data.

A split names its two slot types rather than excluding splits. The schema names text and image, so a future item type states its own case instead of inheriting a promise made before it existed.

API walkthrough

Placing an image

a logo in the report header
{
  "type": "image",
  "source": "=$.input.logo",
  "fit": "width",
  "alt": "{{ $.params.company }} logo",
  "style": { "align": "center", "spaceAfter": 12 }
}

source yields the complete file as a Uint8Array, from render data (=$.input.logo) or a registered function (=stamp(@.status)).

fit is "natural" by default, rendering at intrinsic size with pixels converted to points at 96 dpi and capped at the content width. "width" scales to the content width. Both keep the aspect ratio, and there is no crop, stretch, or explicit height.

alt is one template string, and the one cell value the run form doesn’t reach. A styled run means nothing on a stand-in for a picture. Today the HTML target renders it and the others don’t read it.

style covers align, background, and the box and flow spacing, and nothing else. An image’s box is as wide as the container it sits in: the content width, or the slot’s share inside a split. A text item’s box has always taken that width too. fit sizes the picture and align places it inside. So a background or border* spans the width rather than hugging the picture, and no declaration asks for the old shape.

valign is the reverse case. It’s legal on an image inside a split slot, whose box takes the split’s height. It’s a definition error on a band image, whose height is its own picture’s.

An image may appear in any item array. That means the report header and footer, a group header and footer, the detail band’s array form, empty, and page bands. It may not appear in a table cell or a total cell.

Laying cells across the width

an invoice header
{
  "type": "split",
  "style": { "spaceAfter": 24 },
  "slots": [
    { "type": "text", "value": "{{ $.input.seller.name }}", "width": 60,
      "style": { "size": 10, "bold": true } },
    { "type": "text", "value": "{{ $.input.customer.name }}",
      "style": { "size": 10, "align": "right" } }
  ]
}

slots takes two or more, each a text or image item plus an optional width. Splits don’t nest, and neither appears in a table or total cell.

width is a percentage share of the content width, and it must be greater than 0 and at most 100. Shares total under 100 when any slot has none, and at most 100 when every slot has one. Over-commitment is a definition error.

The split’s own style is the layer under its slots, so a bold split renders bold slots unless a slot says otherwise. The box stays on the split and does not layer into them, and flow spacing stays on the split too.

Every slot’s box is the split’s height rather than its own content’s. A bordered split therefore draws one straight edge across whether or not a slot wraps. A slot rendering nothing still holds that full-height box.

Reaching across table columns

A split is for a band. Inside a table, the equivalent is span.

a total row that spans
{
  "cells": [
    { "value": "Total due", "span": 4 },
    { "value": "{{ round($.subtotal + $.vat, 2) }}", "style": { "format": "currency" } }
  ]
}

A spanning cell draws as one box across the columns it covers, starting where the first starts. It takes no part in measuring them. A header cell may span too, and a spanning header takes the first column it covers.

Each total row must cover the columns exactly once, counting span.

What a target does with either

A split reaching a grid is one row, its slots the cells across it. The width shares go unread, because a worksheet’s columns are global to the sheet. In CSV it’s one record and its slots are the fields.

Image placement in a worksheet is wholly best-effort, and CSV reads images not at all. A slot holding one still writes an empty field, so the fields either side keep their positions.

Down the band, or across it.

npm install quario
Getting started
© 2026 quario · KvK 61815977