Browser / CDN Changes the URL, so browsers and CDNs treat it as a new resource.
picturium server cache Ignored. The cached render and its ETag stay the same.
Docs / Request model
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.
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.
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.w and width work, W doesn’t. Named values
aren’t, so fit=Contain and fit=contain mean the same thing.w / width, h / height,
f / format, q / quality, and so on.key:value pairs with |, like
crop=w:400|h:300|g:attention.&, =, | or # can change the meaning of the query.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.
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.
There are two separate caches to think about:
cache.cache_control.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:
Browser / CDN Changes the URL, so browsers and CDNs treat it as a new resource.
picturium server cache Ignored. The cached render and its ETag stay the same.
Browser / CDN Sends Cache-Control: no-store and never answers with 304.
picturium server cache 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.
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.
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.
A known parameter has an invalid value, or a page selection matches no page in the document or is larger than pdf.max_pages.
URL signing is on and the token is missing or wrong.
The source file doesn't exist and no usable fallback was given.
The file type can't be processed and isn't allowlisted, or the requested PDF / SVG output can't be produced from this source.
Processing, conversion or encoding failed.
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.
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.
There's spare capacity.
Free queue slots have dropped below 10% of total_workers.
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.