---
title: "Images and splits — quario"
description: "Placing a host-supplied image in a quario band, laying cells across the width with a split item, and lining several splits up with a grid."
url: "https://getquario.com/docs/diving-deeper/images-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 image
bytes in the flow, which is how a logo or a chart reaches the page. A **grid** is a run of
splits on one set of columns, which is how a statement’s label and amount lines
stay aligned.

## 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 an SVG by its root element. It reads the
picture’s size from the same bytes. A raster header gives the pixel
dimensions, and an SVG gives its root’s `width` and `height`, or else its
`viewBox`.
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 aren’t PNG, JPEG or SVG, is a located render error naming the item.

**Past the size, the engine vouches 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 passes those bytes to its own machinery.

**An SVG stays bytes, never markup.** An SVG is markup, and author- or
data-supplied markup reaching an HTML fragment would breach the escaping rule
that protects every interpolated value. So no target writes it out as markup.
The HTML target puts it in an `<img>` as a `data:image/svg+xml` URI, where a
browser runs no script and loads nothing the SVG names. The PDF target draws it
as vector paths. A chart your service already produces as SVG reaches the page
as vectors, with no conversion to pixels on the way.

**Not every target can draw an SVG.** A worksheet drawing holds a raster
picture only, and Word shows an SVG only beside a raster copy. So the XLSX and
DOCX targets refuse one with a render error that names the item, rather than
skipping it without a word. The engine learns the format only once `source`
evaluates, so [`required`](https://getquario.com/docs/diving-deeper/validating.md) can’t ask for SVG support. A
service that renders one definition to a workbook as well sends PNG bytes
there.

**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.

**A grid states the geometry once.** Twenty label-and-amount lines written as
splits restate the same two widths twenty times. One typo moves one amount out
of line. A grid declares its columns once and lays every row on them. Each row
reaches a target as a split. The event stream doesn’t change, and every target
that draws a split draws a grid.

## API walkthrough

### Placing an image

```json title="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)`).

An SVG has to state its own size, as absolute `width` and `height` on its root
or else as a `viewBox`. A bare number or `px` is pixels, and `pt`, `in`, `cm`
and `mm` convert at 96 dpi. A percentage depends on a container the file
doesn’t know, so it isn’t a size. The PDF target draws only the shapes the SVG
declares itself. An `<image>`, `<use>` or `<script>` inside it draws nothing,
and the render fetches nothing. SVG `<text>` draws in a base-14 sans face,
whatever family it names.

**`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

```json title="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. A split whose slots all declare a width and leave some of it
unclaimed draws a warning naming the trailing share. A table’s columns already
did the same, and the report still renders.

**The split’s own style is the layer under its slots**, so a bold split renders
bold slots unless a slot says otherwise. It ranks above the band’s role default
too. A report-header split declaring `"size": 8, "bold": false` renders its
slots at 8 points and not bold, where the band alone would make them headlines. The box stays on the split and
doesn’t 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.

### Laying several lines on one set of columns

```json
{
  "type": "grid",
  "columns": [{ "width": 70 }, { "width": 30, "style": { "align": "right", "format": "currency" } }],
  "rows": [
    { "slots": [{ "type": "text", "value": "Opening" }, { "type": "text", "value": "{{ $.input.opening }}" }] },
    {},
    { "style": { "bold": true },
      "slots": [{ "type": "text", "value": "Closing" }, { "type": "text", "value": "{{ $.input.closing }}" }] }
  ]
}
```

**`columns`** takes two or more, each an optional `width` and an optional
`style`. The width shares follow a split’s rules. A column’s `style` is the
layer under each slot in that position, and a slot’s own declarations win.

**`rows`** takes one or more, and a row is a split without its `type`. It keeps
every rule a split has: full content width, no nesting, never broken across a
page. Its `style` and `visible` belong to that line alone, which is where a
rule above a total or a gap before the first line goes.

**A row may name fewer slots than there are columns.** The rest stay empty, so
`{}` is a spacer line and a one-slot row is a label with nothing beside it. A
slot in a grid takes no `width`, because its column already has one.

### Reaching across table columns

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

```json title="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 each

A grid reaches every target as its rows, each one a split. 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.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://getquario.com/"},{"@type":"ListItem","position":2,"name":"Docs","item":"https://getquario.com/docs"},{"@type":"ListItem","position":3,"name":"Images and splits","item":"https://getquario.com/docs/diving-deeper/images-and-splits"}]}
```
