Image Conversion

Overview

On-the-fly image-to-WebP conversion via a Rust Lua filter. Content negotiation is based on the Accept header: when a client accepts image/webp and the response is an eligible image (JPEG, PNG, or GIF first-frame), the response is buffered, decoded, re-encoded as WebP, and emitted with updated Content-Type and Vary headers.

How It Works

The same URL serves WebP or the original image transparently. No rewrite rules or src link replacement is needed — the browser’s Accept header determines which format is served.

Client (Chrome) -> Accept: image/webp -> WebP response
Client (IE)     -> Accept: image/*   -> JPEG response

Encoding Strategy

SourceEncodingReason
PNGLossless WebPPNG is lossless, so lossless WebP preserves quality
JPEGLossy WebPJPEG is already lossy, lossy WebP at quality 80 typically gives better ratio
GIFLossy WebP (first frame)Only the first frame is converted (see Animated GIF below)

Size Guard

If the WebP output is not smaller than the original, the original bytes are served instead. This ensures conversion never increases the response size.

Configuration

Global Toggle

SettingDefaultDescription
img_2_webp_enabledoffMaster switch for image conversion

Per-Backend Options

SettingDefaultDescription
img_2_webp_enabledoffEnable for this backend
img_2_webp_quality80WebP quality for lossy encoding (1-100)
img_2_webp_max_size10MBSkip conversion if image is larger
img_2_webp_max_dim4096Skip conversion if either dimension exceeds this
img_2_webp_source_typesjpeg,png,gifSource types to convert

Caching Integration

The filter emits Vary: Accept so caches create separate entries for WebP and original responses. HAProxy’s memory cache auto-enables process-vary when image conversion is active. The disk cache caches the converted WebP output.

Vary header is essential

Without Vary: Accept, a cache might serve a WebP response to a client that doesn’t support WebP. coreX Manager handles this automatically — you don’t need to configure anything extra.

Filter Ordering

The image conversion filter is emitted after compression so the WebP output is not re-compressed. The full filter order is:

cache -> resp_transform -> compression -> image_conversion

FCGI Limitation

The image conversion filter is skipped for FastCGI backends due to an HAProxy 3.4 bug.

Animated GIF

Animated GIF handling

The current version converts only the first frame of animated GIFs to a static WebP. Animated GIF to animated WebP is future work. If you serve animated GIFs, consider excluding gif from img_2_webp_source_types to avoid losing animation.

Step-by-Step: Enable Image Conversion

  1. Navigate to Settings > Global Options
  2. Enable img_2_webp_enabled
  3. Save

Step-by-Step: Configure Per-Backend

  1. Navigate to Core > Backends and open the backend serving images
  2. Scroll to Image Conversion
  3. Enable img_2_webp_enabled
  4. Set img_2_webp_quality to 80 (good balance)
  5. Set img_2_webp_max_size to 10MB (skip very large images)
  6. Set img_2_webp_max_dim to 4096 (skip very large dimensions)
  7. Set img_2_webp_source_types to jpeg,png (exclude gif if you have animated GIFs)
  8. Save and Apply

Verification

  1. Test with WebP support:

    curl -k -H "Accept: image/webp" -sI https://localhost/photo.jpg | grep -i content-type

    Expect content-type: image/webp.

  2. Test without WebP support:

    curl -k -H "Accept: image/*" -sI https://localhost/photo.jpg | grep -i content-type

    Expect content-type: image/jpeg (original format).

  3. Check Vary header:

    curl -k -sI https://localhost/photo.jpg | grep -i vary

    Expect vary: Accept.

  4. Compare sizes:

    curl -k -H "Accept: image/webp" -s https://localhost/photo.jpg | wc -c
    curl -k -H "Accept: image/*" -s https://localhost/photo.jpg | wc -c

    The WebP response should be smaller (if not, the original is served due to the size guard).

  5. Test with a browser:

    • Open your site in Chrome or Firefox
    • Open DevTools > Network
    • Filter by images
    • Verify JPEG/PNG images are served as WebP

Next Steps