# 0017. Image versions are generated on request

- **Status:** Accepted
- **Date:** 2025-07-01

## Context

Every uploaded picture is needed in several shapes: an admin thumbnail, a square, a card image, a location
tile, a comparison thumbnail, sometimes watermarked. Generating all of them at upload time means work that is
mostly wasted, and a new version means reprocessing the whole library.

## Decision

**Serve versions statically; generate them on the 404.**

The URL encodes everything needed to produce the file:

```
/contents/<Y>/<m>/<d>/<slug>-<id>-<His>[-<version>][-wms].<ext>
```

nginx serves it if it exists. If it does not, the request falls through to a URL rule
(`contents/<filename:.+\.(jpg|jpeg|png|bmp|gif)>` → `pictures/generate`), and
[`PicturesController::actionGenerate()`](../../controllers/PicturesController.php) parses the filename back
into its parts, resizes with `claviska/simpleimage`, writes the file and streams it.

The version registry is `Yii::$app->params['pictureVersions']` in
[`config/parts/params.php`](../../config/parts/params.php) — `width` / `height` / `cover` / `contain` /
`quality` per named version, referenced by model constants.

Generated files are queued into `PictureToOptimize` and losslessly compressed by a cron.

## Alternatives

- **Generate every version at upload.** Wasted work for versions never requested, and a new version means
  reprocessing everything.
- **Resize on every request through PHP.** No static serving at all; the web server would never handle an
  image.
- **An image CDN / imgproxy.** Another service on a single-server deployment, for a library this size.

## Consequences

- **After the first hit, nginx serves the file** — PHP is not involved.
- **Adding a version is a constant plus a registry entry**, and it materialises as pages ask for it. No
  backfill.
- **The filename parse is the security boundary.** The version name must be a key of the registry, the
  timestamp exactly six digits, the id numeric — otherwise `NotFoundHttpException`. Loosening that parse
  opens arbitrary resize work to anyone.
- **A version with both dimensions but neither `cover` nor `contain` throws.** The resize mode is chosen by
  which keys are present.
- **Only five extensions reach the generator.** WebP is served statically or 404s.
- **Deleting a `File` globs its versions away** — the mask replaces the extension with `*`, catching every
  version and watermark variant.
- **`APP_FILE_VERSIONS_ENABLED = false` collapses every version onto the original filename**, so all versions
  resolve to the same file.
- **The compression queue rows are deleted when the shell script is written, not when it succeeds.** If the
  script does not run, those images stay uncompressed and nothing re-queues them.
- The `His` timestamp in the filename ties the URL to the record, so replacing a picture invalidates the
  browser cache without a query string.
