A box like 0x2048 passed the check, and the aspect ratio of a very wide source then sized the other side without a bound.
Thumbnails
The thumbnails service is a stateless image resizer. It exposes an imagor-compatible push endpoint that accepts an original image as a multipart upload and returns the resized thumbnail encoded in the requested format.
Warning: This service performs no authentication or authorization. By default it binds to
127.0.0.1:9186so only local processes (the webdav service) can reach it. If you changeTHUMBNAILS_HTTP_ADDRto a non-loopback address, put it behind your reverse proxy's auth layer or an internal network boundary. Anyone who can reach the endpoint can upload and retrieve arbitrary images through it.
Push Endpoint
The webdav service (which owns the complete thumbnail workflow) POSTs the source file to this endpoint and receives the processed image back.
| Route | Description |
|---|---|
POST /unsafe/{width}x{height}/filters:{filters} |
Fill the exact width x height (may upscale), the default |
POST /unsafe/fit-in/{width}x{height}/filters:{filters} |
Scale to fit within width x height, preserving aspect ratio and never upscaling |
POST /unsafe/stretch/{width}x{height}/filters:{filters} |
Resize to the exact width x height without preserving aspect ratio (distorts) |
A smart segment behind the box, POST /unsafe/{width}x{height}/smart/filters:{filters}, asks for the crop that keeps the region carrying the image rather than the middle, which is what webdav sends for every cropping resize. With libvips that is an attention crop; the pure Go build has nothing of the kind and crops the centre, as the thumbnails service has always done without libvips.
The path is imagor's. The filters segment is optional and may be empty
(filters:), and an image key behind it is accepted and ignored, because the
source arrives in the upload rather than being loaded by key. A bare box with
nothing behind it is the one place this service and imagor part: imagor reads the
last segment as the image key, so /unsafe/100x100 is a key and nothing is
resized, while this service treats it as the box. Inside the filters segment,
format and no_upscale are the filters this executor acts on, every other
filter is ignored. format itself is optional: without it the input's own format is
preserved, the same way imagor does it. An input this service cannot write
(tiff, bmp) becomes a JPEG; imagor writes tiff back in its own format and only
falls back to JPEG for the ones it cannot write either. Which formats those are
depends on the build: with libvips a webp source stays a webp, the pure Go build
answers it as a JPEG because it has no webp encoder.
What a build can read is its own backend's answer rather than a list: the library
that will decode the image reads the header first, so a source it cannot read
answers 406 before anything is decoded, and the maximum dimensions are checked
on that same header.
A side given as 0 is computed from the other one, so /unsafe/0x100/ keeps the
aspect ratio and /unsafe/0x0/ returns the source at its own size. A free axis
leaves nothing to crop to, so the operation makes no difference to it.
Like real imagor, the default fill resize upscales small sources to fill
the box exactly (a 100x100 source requested at 320x320 returns a 320x320
image). Adding the no_upscale() filter caps the result at the source size
instead (the same request returns 100x100). The fit-in route never upscales,
imagor would with the upscale() filter, this service ignores it.
Which route a preview request ends up on, and the processor parameter that
decides it, are the webdav service's business and documented in
its README.
The request body is a multipart/form-data upload with a single file field
named image, typed image/*; an untyped part is refused the way imagor's
upload loader refuses it. Output formats are jpg, png, gif and, with
libvips, webp; the pure Go build answers 406 for format(webp) because it
has no webp encoder. An animated gif asked for a still format is flattened to
its first frame, and a still source asked for format(gif) is palettised.
Reading an ICO needs libvips with its magick loader; the pure Go build answers
406 for one.
An animation is the one source whose frames are all decoded here, so what its
blocks declare is read before any of them is: a gif with more frames than
THUMBNAILS_MAX_ANIMATION_FRAMES, or with a frame declaring more than the
maximum dimensions, is refused with 422. A flat frame compresses by about three
orders of magnitude, so a file well within the input size limit can otherwise ask
for gigabytes of frame buffers. imagor has the same limit as
-vips-max-animation-frames but truncates the animation to that many frames,
where this service refuses the file, because image/gif decodes every frame or
none.
The answers are imagor's: 400 for a malformed request, which includes a
format nobody writes (anything but jpg, png, gif and webp); 406 for a
source it cannot read, for an upload part that is not typed image/*, and for a
format this build has no encoder for (webp without libvips); 422 for an image
above the configured maximum dimensions and for an animation above the frame
limit; 429 when the concurrency limit is reached; 413 for a body above
THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE.
webdav decides which size is asked for, keeps the thumbnail cache and holds the list of types a preview is attempted for. This service checks what it can see on the image itself: the dimensions of the source, the number of frames, the size of the body, and how many requests are worked on at once. webdav's MIGRATION.md says what an existing deployment has to change.
Configuration
| Environment variable | Description |
|---|---|
THUMBNAILS_HTTP_ADDR |
Bind address of the HTTP service (default 127.0.0.1:9186) |
THUMBNAILS_LOG_LEVEL |
Log level (panic, fatal, error, warn, info, debug, trace) |
THUMBNAILS_MAX_CONCURRENT_REQUESTS |
Maximum number of thumbnail requests decoded and resized in parallel. Default is 0 (unlimited). Requests arriving while the limit is reached get HTTP 429 with a Retry-After header |
THUMBNAILS_MAX_INPUT_WIDTH |
Maximum width of a source image, 7680 by default. A wider one is refused with HTTP 422, which webdav turns into its legacy 403 |
THUMBNAILS_MAX_INPUT_HEIGHT |
Maximum height of a source image, 7680 by default |
THUMBNAILS_MAX_OUTPUT_WIDTH |
Maximum width of a thumbnail that may be asked for, 2048 by default. A larger box is refused with HTTP 422 |
THUMBNAILS_MAX_OUTPUT_HEIGHT |
Maximum height of a thumbnail that may be asked for, 2048 by default |
THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE |
Maximum size of a posted image, 50MB by default, empty for unlimited. A larger body is refused with HTTP 413. OC_THUMBNAILS_MAX_INPUT_IMAGE_FILE_SIZE sets this and webdav's own limit together, which is what keeps webdav from sending a body this service then refuses |
THUMBNAILS_MAX_ANIMATION_FRAMES |
Maximum number of frames an animated image may have, 100 by default, 0 for unlimited. A file with more frames is refused with HTTP 422 before any frame is decoded. The memory a request can ask for is this limit times the maximum dimensions, so lower it when raising those |
The source and the thumbnail have ceilings of their own, for different reasons. How large a preview may be is webdav's decision, and its largest configured resolution is what it ever asks for; THUMBNAILS_MAX_OUTPUT_* is the net under that for anything else reaching this endpoint, because fill and stretch enlarge a small source to fill the box. How large a source may be is this service's decision, since it is the one that sees the pixels: webdav knows a file's type, size and checksum, not its dimensions.
A real imagor has one pair of knobs for both, -vips-max-width and -vips-max-height, which bound every image in its pipeline, the loaded source as well as the result. Set them to the source ceiling. They are also more forgiving than this service for formats libvips can shrink while loading: a 9000 pixel wide JPEG passes a 2048 limit there, where this service refuses it on the header.
Using libvips for Image Processing
To improve performance and to support a wider range of image formats, the thumbnails service is able to utilize the libvips library for image processing. Support for libvips needs to be enabled at buildtime and has a couple of implications:
- With libvips support enabled, it is not possible to create a statically linked OpenCloud binary.
- Therefore, the libvips shared libraries need to be available at runtime in the same release that was used to build the OpenCloud binary.
- When using the OpenCloud docker images, the libvips shared libraries are included in the image and are correctly embedded.
Support of libvips is disabled by default. To enable it, make sure libvips and its buildtime dependencies are installed in your build environment. For macOS users, add the build time dependencies via:
brew install vips pkg-config
export PKG_CONFIG_PATH="/usr/local/opt/libffi/lib/pkgconfig"
Then you just need to set the ENABLE_VIPS variable on the make command:
make -C opencloud build ENABLE_VIPS=1
Or include the enable_vips build tag in the go build command:
go build -tags enable_vips -o opencloud -o bin/opencloud ./cmd/opencloud
When building a docker image using the Dockerfile in the top-level directory of OpenCloud, libvips support is enabled and the libvips shared libraries are included in the resulting docker image.