Tech-stack research: self-hosted image proxy
Researched 2026-09-26 against live sources. Summarised; see links per claim.
1. Go + libvips vs Rust + libvips vs pure Rust
Section titled “1. Go + libvips vs Rust + libvips vs pure Rust”Go + libvips (bindings are mature and used in production)
- imgproxy has its own hand-written cgo shim (
vips.c/.h/.go,source.c) and its own C loaders for BMP and ICO. https://github.com/imgproxy/imgproxy/tree/master/vips - vipsgen (cshum, MIT) generates type-safe Go bindings from libvips GObject introspection, ~300 operations, streams via
io.Reader/io.Writer, pre-generated for libvips 8.16/8.17/8.18. imagor (Apache-2.0, v1.9.6 Aug 2026) runs on it. https://github.com/cshum/vipsgen https://github.com/cshum/imagor - govips needs libvips 8.14+ and Go 1.23+; history of segfaults/leaks; docs say set
MALLOC_ARENA_MAX=2or use jemalloc. https://github.com/davidbyttow/govips - bimg is legacy.
- Concurrency fit is excellent: goroutines on net/http, libvips has its own thread pool (
VIPS_CONCURRENCY). Go 1.26 cut cgo call overhead ~30%. https://go.dev/doc/go1.26 - cgo memory risks: C crashes kill the process; glibc arena fragmentation is real. imagor and govips ship
MALLOC_ARENA_MAX=2+ jemalloc preload. https://github.com/cshum/imagor/blob/master/Dockerfile - Startup is milliseconds.
Rust + libvips (usable, bindings young)
- rs-vips 0.7.0 (MIT) released 24 Sep 2026, fork of olxgroup bindings;
VipsImagebecame Send/Sync only in 0.7.0; ~2.6k total downloads. https://docs.rs/rs-vips/latest/rs_vips/ - No production image server at imgproxy/imagor scale. Same perf (libvips does the work), same unsafe C boundary.
Pure Rust (not viable for this format list)
- zune-image, image-rs, fast_image_resize, jxl-oxide are fast but none covers HEIC, CR3/RAW, PDF, PSD, animated WebP/AVIF encode. FFI still required. No pure-Rust demand-driven streaming pipeline equivalent to libvips.
Recommendation: Go + libvips, thin cgo layer generated by vipsgen plus a hand-written shim for hot paths.
2. libvips version, loaders and licenses
Section titled “2. libvips version, loaders and licenses”Stable: libvips 8.18.6 (25 Aug 2026). 8.18 added UltraHDR (libultrahdr), camera RAW via libraw (dcrawload), Oklab/Oklch. https://github.com/libvips/libvips/releases https://www.libvips.org/2025/12/04/What's-new-in-8.18.html
| Lib | Enables | License |
|---|---|---|
| libvips | core | LGPL-2.1+ |
| libheif | HEIC/AVIF | LGPL-3 |
| libde265 | HEIC decode | LGPL |
| x265 | HEIC encode | GPL → use kvazaar (BSD) |
| aom / dav1d | AV1 enc / dec | BSD + AOM patent / BSD |
| libjxl | JPEG XL | BSD-3 |
| libspng / libpng | PNG | BSD-2 / libpng |
| cgif + quantizr | GIF save | MIT / MIT (libimagequant newer versions are GPL-3, avoid) |
| libwebp | WebP incl. animated | BSD |
| librsvg | SVG | LGPL |
| poppler-glib | GPL-2+ → avoid | |
| PDFium | BSD-3/Apache-2 (https://github.com/bblanchon/pdfium-binaries) | |
| libraw | RAW | LGPL-2.1 or CDDL-1.0 |
| ImageMagick | PSD/BMP/ICO fallback | ImageMagick (Apache-style); security caveat with untrusted input |
| OpenJPEG, OpenEXR, libultrahdr, highway | JP2, EXR, UltraHDR, SIMD | BSD/MIT/Apache |
Shipping LGPL .so files in an image is fine (dynamic linking + source offer). Do not ship x265 or poppler. imgproxy-docker-base uses kvazaar + dav1d, no x265, -Dmodules=disabled (no libraw/poppler/magick). https://github.com/imgproxy/imgproxy-docker-base/blob/master/build-deps.sh
libvips has no native loader for BMP, ICO or PSD: magick, or own loaders like imgproxy.
3. Camera RAW previews
Section titled “3. Camera RAW previews”- Embedded JPEG preview (
libraw_unpack_thumb) is orders of magnitude faster than demosaic (reported 20,696 ms → 40 ms on a Sony ARW). https://github.com/Bristopher/RawThumbProvider https://www.libraw.org/docs/API-CXX.html - Full demosaic (
dcraw_process, what vipsdcrawloaddoes) takes seconds;dcrawloadhas no embedded-preview option, onlybitdepth. Supports ARW, CR2, CR3, CRW, DNG, NEF, NRW, ORF, PEF, RAF, RW2, SRW, X3F… https://www.libvips.org/API/8.18/ctor.Image.dcrawload.html - LibRaw 0.22.0 (13 Jan 2026): Canon R1/R5 II, DNG 1.7 JXL, 1,284 cameras, CR3 hardening. 0.22.1 current. https://www.libraw.org/news/libraw-0-22-0-release
- exiv2 is GPL-2 → avoid.
- No mature Go libraw binding; ~100 lines of cgo (
libraw_open_buffer→libraw_unpack_thumb→libraw_dcraw_make_mem_thumb).
Strategy: cgo shim tries unpack_thumb first; if the embedded JPEG ≥ requested size, feed to vips_jpegload_buffer and apply RAW EXIF orientation; else fall back to vips_dcrawload half-size.
4. Smart crop, saliency, faces (CPU only)
Section titled “4. Smart crop, saliency, faces (CPU only)”- libvips
smartcropinteresting=attention|entropy|centre|low|high|all. https://www.libvips.org/API/8.18/method.Image.smartcrop.html - pigo: pure Go PICO cascade, frontal faces only. https://github.com/esimov/pigo
- ONNX Runtime 1.30 has official Go bindings; YOLO-style face model runs 10–30 ms on CPU at 640 px.
onnxruntime-puregoavoids cgo. https://github.com/microsoft/onnxruntime/releases https://github.com/shota3506/onnxruntime-purego - Ultralytics YOLO weights are AGPL → ship YuNet (OpenCV Zoo) or permissive SCRFD/RetinaFace; let users mount their own.
imgproxy Pro paid features ($49/mo): advanced smart crop (faces), object-oriented crop/blur/draw, auto-quality by DSSIM and by file size, best-format, GIF→MP4, video thumbnails, PDF/PSD thumbnails, advanced watermarks (per-request URL, text, shadow, rotation), chained pipelines, colour adjustments, advanced compression, and (since v4.0.0, 2026-05-13) camera RAW loading behind IMGPROXY_ENABLE_RAW_FORMATS plus an internal result cache. https://imgproxy.net/features/ (verified in .refs/imgproxy/CHANGELOG.md and imagetype/raw.go)
5. Reusable base images
Section titled “5. Reusable base images”| Image | Size (amd64/arm64) | Contents | Missing |
|---|---|---|---|
darthsim/imgproxy (base ghcr.io/imgproxy/imgproxy-base, MIT, Noble) |
75 / 72 MB | vips 8.18.6, libheif 1.23.4 (kvazaar+dav1d+libde265, aom), libjxl 0.12, cgif 0.5.4, librsvg 2.63, quantizr, highway | libraw, PDF, magick |
ghcr.io/cshum/imagor-base (MIT) / shumc/imagor 106/100 MB |
vips 8.18.6, libraw 0.22.1, libjxl, libheif, x265 (GPL), poppler (GPL), optional magick/mozjpeg/ffmpeg 7.1.1 | GPL bits → don’t reuse as-is | |
lovell/sharp-libvips |
static prebuilt, Apache-compatible, heif AVIF only | jxl, libraw, PDF | |
weserv/images (BSD-3) |
C++ nginx module | not a base image |
Strategy: fork imgproxy-docker-base versions.sh/build-deps.sh (MIT), add libraw 0.22.x, pdfium-binaries, optional restricted ImageMagick. Use imagor-base as reference for libraw.
6. Video thumbnails
Section titled “6. Video thumbnails”- go-astiav: idiomatic cgo, targets FFmpeg n8.0 only. https://github.com/asticode/go-astiav
- ffmpeg-statigo: static GPL build → avoid. ffmpeg-go: CLI wrapper.
- imagorvideo does best-frame selection by RMSE histogram. https://docs.studio.imagor.net/ecosystem/
- Size: imagorvideo 148 MB vs imagor 106 MB → FFmpeg full adds ~40 MB compressed. Decoder-only LGPL build (
--disable-gpl, h264/hevc/vp9/av1 decoders, mp4/webm/mov demuxers) estimated 10–15 MB.
Recommended stack
Section titled “Recommended stack”- Language: Go 1.27 (1.27.1 current as of 2026-09-26; pin
go 1.27.0,toolchain go1.27.1),CGO_ENABLED=1, buildx matrix, native arm64 runners preferred. - Binding: vipsgen-generated bindings + own small cgo shim for hot paths (load from buffer/source, thumbnail, save to target, libraw thumbnail loader). Pin libvips 8.18.x.
VIPS_CONCURRENCY, request semaphore sized to cores,vips_cache_set_max(0),MALLOC_ARENA_MAX=2+ jemalloc. - Base image: in-house deps on
ubuntu:noble→/opt/xtandard/lib, multi-stage copy to slim runtime. Publishghcr.io/xantiagoma/xtandard-image-base:vips8.18.6-<flavor>-rN. - License policy: LGPL/BSD/MIT/Apache only. No x265, poppler, exiv2, GPL ffmpeg, AGPL weights.
| Format | Library |
|---|---|
| JPEG | libjpeg-turbo 3.2 (optional mozjpeg encode) |
| PNG | libspng decode, libpng encode, quantizr palette |
| GIF | vips gifload (libnsgif) + cgif + quantizr |
| WebP | libwebp 1.6 |
| AVIF | libheif + dav1d + aom (or SVT-AV1) |
| HEIC | libheif + libde265 + kvazaar |
| JXL | libjxl 0.12 |
| TIFF | libtiff 4.7 |
| BMP/ICO | own loaders (as imgproxy), magick fallback |
| SVG | librsvg 2.63 with own sanitisation |
PDFium via vips pdfload |
|
| PSD/PSB | ImageMagick 7 restricted by policy.xml, or embedded composite preview |
| RAW | libraw 0.22.1: unpack_thumb first, dcrawload fallback |
| UltraHDR | libultrahdr |
| Smart crop | vips attention default; faces/objects via ONNX Runtime Go + permissive model, lazy-loaded |
| Video | go-astiav + FFmpeg 8.0 LGPL decoder-only; best frame by histogram |
| Auto-quality | DSSIM/SSIMULACRA2 + binary search on quality |