---
title: "The overlay contract"
manual: "LIA Imageserver"
version: "2.3"
permalink: "https://docs.typo3.org/permalink/lia/lia_imageserver:developer-overlay@2.3"
source: "Developer/Overlay.rst"
modified: "2026-09-16T09:22:57+00:00"
---

# The overlay contract

`\LIA\LiaImageserver\Processing\Overlay` describes one instruction:
*composite a second image onto this one*. It lives in this extension rather
than in a delivery backend because every backend has to agree on it. An
imgix-style backend maps it onto watermark parameters; a local compositor
turns it into pixel operations. Neither owns the vocabulary, and a producer
must not have to guess which spelling a particular backend expects.

## Fields

| Field | Type | Meaning |
| --- | --- | --- |
| `imageIdentifier` | `?string` | Storage-relative path, for a backend that fetches the overlay by URL. |
| `imagePath` | `?string` | `EXT:` path, for a backend that opens the file locally. |
| `assetVersion` | `?string` | Opaque fingerprint of the asset's **content**; nothing renders it. |
| `alignment` | `OverlayAlignment` | Full anchor grid: `top` / `middle` / `bottom` × `left` / `center` / `right`. Default: `OverlayAlignment::BottomRight`. |
| `padding` | `int` | Pixels from the edge. Default: `5`. |
| `widthRatio` | `?float` | Fraction of the **base** image width, `0.01` to `1.0`. Absent means the overlay keeps its own size. |

At least one asset reference is required, and carrying only one is
legitimate: each backend uses the reference it can work with and skips the
overlay when its own is absent. A reference that is present but blank is
rejected: backends tell "absent" from "given" by asking for `null`, so an
empty string would reach URL building and cost the whole image rather than the
overlay. Producers build the object and hand over `toInstruction()`;
backends receive the array and call `fromInstruction()`.

**Producer and backend side**

```php
use LIA\LiaImageserver\Processing\Overlay;
use LIA\LiaImageserver\Processing\OverlayAlignment;

// Producer
$instructions[Overlay::INSTRUCTION_NAME] = (new Overlay(
    imageIdentifier: '/watermarks/logo.png',
    imagePath: 'EXT:my_extension/Resources/Public/logo.svg',
    assetVersion: '65dc2851fcf0',
    alignment: OverlayAlignment::BottomRight,
    padding: 20,
    widthRatio: 0.2,
))->toInstruction();

// Backend
$overlay = Overlay::fromInstruction($value);
$overlay->alignment->vertical();    // 'bottom'
$overlay->alignment->horizontal();  // 'right'
```

## Why the instruction stays an array

On the wire the instruction stays a plain array. That is not incidental: it is
handed to parameter mapping on one path and folded into a `ProcessedFile`
checksum on the other, so it has to remain serialisable. This is also why
absent values are omitted rather than emitted as `null`. The object is
the validating boundary at both ends, not the transport.

## Why assetVersion exists

`assetVersion` exists because of that second path. A locally compositing
backend reaches its rendering through a `ProcessedFile` looked up by a
checksum over this instruction, and `imagePath` is a stable `EXT:` path:
replace the file behind it and the instruction stays byte-identical, so the
old rendering keeps being served and the compositor is never asked. A URL
backend does not have that problem when its `imageIdentifier` is
content-addressed, which is exactly why this cannot be left to the backends.
The asymmetry sits in their caches, the knowledge sits at the producer, and
the checksum is formed before any backend runs. Any string will do as long as
it changes with the content; producers whose asset cannot change may omit it.

## Errors and sizing

A malformed instruction throws rather than being coerced or dropped: a backend
that guesses at one turns a producer's bug into a rendering that is subtly
wrong instead of loudly broken.

`widthRatio` is measured against the base image, so the resolution an overlay
asset needs is the largest rendered base width times the ratio. A backend
requests the asset as stored and does not cap it.
