Resizing with Workers

There are two ways of using image resizing. One is the default URL scheme, which provides an easy, declarative way of specifying image dimensions and other options. The other way is to use a JavaScript API in a Worker. Workers give powerful programmatic control over every image request.

  • You can use a custom URL scheme, e.g. instead of specifying pixel dimensions in image URLs, use preset names such as thumbnail and large.
  • Hide the actual location of the original image. You can store images in an external S3 bucket or a hidden folder on your server without exposing that information in URLs.
  • Implement content negotiation to adapt image sizes, formats and quality dynamically based on the device and condition of the network.

The resizing feature is accessed via the options of a fetch() subrequest inside a Worker.

Fetch options

The fetch() function accepts parameters in the second argument inside {cf: {image: {…}}} object. The parameters are:

width
Maximum width in image pixels. The value must be an integer.
height
Maximum height in image pixels.
fit

Resizing mode as a string. It affects interpretation of width and height:

scale-down

Similar to contain, but the image is never enlarged. If the image is larger than given width or height, it will be resized. Otherwise its original size will be kept.
contain
Resizes to maximum size that fits within the given width and height. If only a single dimension is given (e.g. only width), the image will be shrunk or enlarged to exactly match that dimension. Aspect ratio is always preserved.
cover
Resizes (shrinks or enlarges) to fill the entire area of width and height. If the image has an aspect ratio different from the ratio of width and height, it will be cropped to fit.
gravity

When cropping with fit: "cover", this defines the side or point that should be left uncropped. The value is either a string "left", "right", "top", "bottom" or "center" (the default), or an object {x, y} containing focal point coordinates in the original image expressed as fractions ranging from 0.0 (top or left) to 1.0 (bottom or right), 0.5 being the center. {fit: "cover", gravity: "top"} will crop bottom or left and right sides as necessary, but won’t crop anything from the top. {fit: "cover", gravity: {x:0.5, y:0.2}} will crop each side to preserve as much as possible around a point at 20% of the height of the source image.

quality

Quality setting from 1-100 (useful values are in 60-90 range). Lower values make images look worse, but load faster. The default is 85. It applies only to JPEG and WebP images. It doesn’t have any effect on PNG.

format

Output format to generate. It can be:

  • webp — generate images in Google WebP format. Set quality to 100 to get the WebP-lossles format.
  • json — instead of generating an image, outputs information about the image, in JSON format. The JSON object will contain image size (before and after resizing), source image’s MIME type, file size, etc.

In your worker, where you’d fetch the image using fetch(request), add options like this:

fetch(imageURL, {
    cf: {
        image: {
            fit: "scale-down",
            width: 800,
            height: 600,
        }
    }
});

Configuring a worker

Create a new script in Workers section of the Dashboard. Do not set up the image resizing worker for the entire domain. Scope your worker script to a path dedicated to serving assets, such as /images/*, /assets/*, etc.

It’s best to keep the path handled by the worker separate from the path to original (unresized) images to avoid request loops caused by the image resizing worker calling itself. For example, store your images in example.com/originals/ directory, and handle resizing via example.com/thumbnails/* path that fetches images from the /originals/ directory.

Lack of preview in the Dashboard

Image resizing is not simulated in the preview of Worker scripts in the editor in the Dashboard.

The script preview of the Worker editor ignores fetch() options, and will always fetch unresized images. To see the effect of image resizing you must deploy the worker script and use it outside of the editor. We apologize for the inconvenience.

Error handling

When an image can’t be resized, e.g. because the image doesn’t exist, or resizing parameters were invalid, the response will have an HTTP status indicating an error (e.g. 400, 404, 502, etc.).

By default, the error will be forwarded to the browser, but you can decide how to handle errors. For example, you can redirect the browser to the original, unresized image instead:

const response = await fetch(imageURL, options);
if (response.ok) {
  return response;
} else {
  return response.redirect(imageURL, 307);
}

Keep in mind that if the original images on your server are very large, it may be better not to display failing images at all, than to fall back to overly large images that could use too much bandwidth, memory, or break page layout.

Or you can replace failed images with a placeholder image:

const response = await fetch(imageURL, options);
if (response.ok) {
  return response;
} else {
  return fetch("https://img.example.com/kittens.jpg"); // change to a URL on your server
}

Preventing request loops

To perform resizing and optimizations, the worker must be able to fetch the original, unresized image from your origin server. In case the path handled by your worker overlaps with the path where images are stored on your server, you must detect which requests should not be resized and go straight to the server. When image-resizing string is present in the Via header that’s a request coming from another worker, and should be directed straight to the origin server, like this:

addEventListener('fetch', event => {
  // if this request is coming from image resizing worker,
  // avoid causing an infinite loop by resizing it again:
  if (/image-resizing/.test(event.request.headers.get("via"))) {
    return fetch(event.request);
  }
  // now you can safely use image resizing here
}

An example worker

Assuming you set up a worker on https://example.com/image-resizing to handle URLs like this: https://example.com/image-resizing?width=80&image=https://example.com/uploads/avatar1.jpg

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

/**
 * Fetch and log a request
 * @param {Request} request
 */
async function handleRequest(request) {
  // Parse request URL to get access to query string
  let url = new URL(request.url);

  // Cloudflare-specific options are in the cf object.
  let options = { cf: { image: {} } }

  // Copy parameters from query string to request options.
  // You can implement various different parameters here.
  if (url.searchParams.has("fit")) options.cf.image.fit = url.searchParams.get("fit");
  if (url.searchParams.has("width")) options.cf.image.width = url.searchParams.get("width");
  if (url.searchParams.has("height")) options.cf.image.height = url.searchParams.get("height");
  if (url.searchParams.has("quality")) options.cf.image.quality = url.searchParams.get("quality");

  // Get URL of the original (full size) image to resize.
  // You could adjust the URL here, e.g. prefix it with a fixed address of your server,
  // so that user-visible URLs are shorter and cleaner.
  const imageURL = url.searchParams.get("image");

  // Build a request that passes through request headers,
  // so that automatic format negotiation can work.
  const imageRequest = new Request(imageURL, {
    headers: request.headers,
  });

  // Returning fetch() with resizing options will pass through response with the resized image.
  return fetch(imageRequest, options);
}

When testing image resizing, please deploy the script first. Resizing won’t be active in the on-line editor in the Dashboard.