Docs / Request model

Request model v0.2.0

Every transformation is a plain GET request. The path picks a source file under data.dir, and the query string describes what you want back. picturium checks the signature, finds the file, parses the parameters, looks at validators and its caches, and then either serves a cached result or renders a new one.

Request flow
  1. GET URL
  2. Verify signature
  3. Resolve source
    or fallback
  4. Parse effective
    parameters
  5. Check validators
    and server cache
  6. Acquire worker
    and render
  7. HTTP response

original=true serves the resolved source unchanged; a matching validator returns 304 before rendering.

If you just need the list of parameters and aliases, head to the Quick reference.

URL grammar

GET /<path-under-data.dir>?<name>=<value>&<name>=<value>

With data.dir = "data", this request reads data/catalog/chair.jpg:

/catalog/chair.jpg?w=1200&h=800&fit=cover&f=webp&q=high

A few rules worth knowing:

  • data.dir itself never appears in the URL. picturium resolves the real path and refuses anything that ends up outside the data directory.
  • Parameter names are case-sensitive: w and width work, W doesn’t. Named values aren’t, so fit=Contain and fit=contain mean the same thing.
  • The common parameters have short aliases: w / width, h / height, f / format, q / quality, and so on.
  • Grouped parameters join case-sensitive key:value pairs with |, like crop=w:400|h:300|g:attention.
  • URL-encode anything user-supplied: paths, filenames, watermark text. Otherwise a stray &, =, | or # can change the meaning of the query.
  • Unknown parameters are silently ignored, but a bad value for a known one returns 400. If a parameter seems to do nothing, check its spelling against the parameter index.

The rules for individual transforms live on their own pages: Sizing & layout, Cropping, Filters & effects, Watermarking, and Output format & quality.

Signed URLs

Signing is off by default. If you turn it on, use a long random secret:

[security]
signature_enabled = true
signature_secret = "replace-with-a-long-random-secret"

With signing on, every file request needs a token query parameter holding a hex-encoded HMAC-SHA256 digest. You sign the path without its leading slash, then ?, then the raw query string exactly as it will be sent, minus the token= pair:

payload = "catalog/chair.jpg?w=1200&f=webp"
token   = hex(HMAC-SHA256(signature_secret, payload))

/catalog/chair.jpg?w=1200&f=webp&token=<token>

When the URL has no other parameters, sign just the path, with no trailing ?:

payload = "catalog/chair.jpg"

/catalog/chair.jpg?token=<token>

The server checks the query string byte for byte, so keep the parameter order, repeated parameters and percent-encoding identical between signing and sending.

A missing, malformed or wrong token gets 403 Invalid signature with Cache-Control: no-store. The home page, /public/* and /health are separate routes and don’t need a signature. Keep the secret on the server and never ship it to the browser. openssl rand -hex 48 makes a good one; the environment variable names are in the [security] config reference.

Caching & force

There are two separate caches to think about:

  1. Browsers and CDNs cache the HTTP response by URL, following cache.cache_control.
  2. picturium keeps rendered responses and conversion intermediates in its own memory and disk cache.

picturium’s cache key isn’t the URL. It’s built from the picturium version and configuration, the source path, the source file’s modification time and size, the effective parameters, and the resolved output format. So reordering parameters still hits the cache. If several requests miss the same key at once, only one of them renders and the rest wait for its result.

The two cache-busting parameters do different jobs:

cache=<value>

Changes the URL, so browsers and CDNs treat it as a new resource.

Ignored. The cached render and its ETag stay the same.

force=true

Sends Cache-Control: no-store and never answers with 304.

Skips the cache, renders again (intermediates included), and replaces the cached entries.

force isn’t part of the cache key, so the ETag doesn’t change. For raw files and original=true there’s nothing generated to replace; force just skips the conditional checks and adds no-store.

Cache sizes, eviction and clearing the disk cache are covered in Configuration & operations. How document conversions are cached is explained in Documents & vectors.

HTTP caching

Successful responses use cache.cache_control, which defaults to:

public, max-age=604800, must-revalidate

Rendered responses carry an ETag derived from the server cache key and a Last-Modified taken from the source file. Send either one back on the next request:

If-None-Match: "6bc3a2a6683f12d4"
If-Modified-Since: Sat, 29 Aug 2026 12:00:00 GMT

If both are present, If-None-Match wins. A matching ETag, If-None-Match: *, or a source that hasn’t changed since the given date gets a 304 with an empty body. force=true skips these checks.

Responses where the format came from f=auto include Vary: Accept; responses with an explicit format don’t. Gzipped SVGs served as-is use Vary: Accept-Encoding. The automatic format selection section explains which media types are recognised.

Errors & fallback

fallback is another path under data.dir, not a remote URL. picturium tries it when the requested file can’t be found, or when a PDF exceeds one of the PDF limits (pdf.max_file_size, pdf.max_pages, pdf.max_image_pixels):

/catalog/missing.jpg?w=800&fallback=placeholders/product.jpg

The fallback goes through the same path checks and gets the same transforms as the original request. Apart from the PDF limits, it isn’t used when decoding, conversion, rendering or encoding fails, so treat it as a placeholder for missing or oversized files rather than a catch-all error handler.

400

A known parameter has an invalid value, or a page selection matches no page in the document or is larger than pdf.max_pages.

403

URL signing is on and the token is missing or wrong.

404

The source file doesn't exist and no usable fallback was given.

415

The file type can't be processed and isn't allowlisted, or the requested PDF / SVG output can't be produced from this source.

500

Processing, conversion or encoding failed.

503

All workers are busy and the queue is full.

The 403, 404, 415, 500 and 503 responses all use Cache-Control: no-store. Don’t confuse original=true with fallback: it needs the requested file to exist and returns it untouched. See delivery and original responses.

Health & capacity

GET /health doesn’t need a signature. Out of the box it only tells you the status:

{ "status": "healthy" }

Set security.health_token and send it as Authorization: Bearer <token> or /health?token=<token> to get the version, live worker and queue counters, and memory statistics as well. A wrong token returns 401. If you send both, the header wins. Use the header when you can: a token in the URL ends up in access logs and in picturium’s own debug log.

{
  "status": "healthy",
  "name": "picturium",
  "version": "0.2.0",
  "total_workers": 8,
  "available_workers": 6,
  "queue_size": 100,
  "available_queue_size": 100,
  "memory": {
    "rss_bytes": 412000256,
    "peak_rss_bytes": 530579456,
    "vips_bytes": 52428800,
    "vips_peak_bytes": 104857600,
    "vips_allocations": 120,
    "vips_open_files": 3,
    "vips_cached_operations": 100,
    "cache_bytes": 201326592,
    "cache_capacity_bytes": 268435456
  }
}

rss_bytes is what the operating system reports for the whole process. The vips_* fields only cover memory libvips tracks itself, and cache_* describes the in-memory response cache.

server.workers = 0 means twice the number of CPUs. Each render holds one worker. Extra requests wait in one of server.queue_size queue slots, and once every worker and slot is taken, new requests get 503 Too many requests straight away.

healthy

There's spare capacity.

warning

Free queue slots have dropped below 10% of total_workers.

unhealthy

available_workers and available_queue_size are both zero.

The endpoint answers with HTTP 200 in all three cases, so if you want a monitor to fail on warning or unhealthy, have it read the status field. Tuning the limits is covered in Configuration & operations.