Tech Bool Tech Bool
Image processing in Cloudflare Workers

Image processing in Cloudflare Workers

January 22, 2025
4 min read
Table of Contents

I was looking for an alternative to Sharp since it does not work on Cloudflare Workers as it uses Node.js APIs, which are not yet supported in Cloudflare’s workerd runtime. Then I came across Photon, a high-performance Rust image processing library, which compiles to WebAssembly. Photon offers a variety of functions for resizing, transforming, watermarking, and more. There is official npm package available for it, but it was not working with Wrangler when I tried it last time, and also it was missing some new functions.

Therefore, I forked the repository and compiled it to WebAssembly using wasm-pack to make it compatible with Cloudflare Workers. I have also published it as npm package @cf-wasm/photon.

Implementation

You can use this package in an existing workers project or create a new project by running:

npm create cloudflare@latest -- cf-workers-photon
cd cf-workers-photon

Then install @cf-wasm/photon by running:

npm i @cf-wasm/photon

Now you can import functions exported by this package and use them in your code.

Here is an example which uses resize function to resize an image and respond with a WebP format.

src/index.ts
import { PhotonImage, SamplingFilter, resize } from "@cf-wasm/photon";
 
export default {
  async fetch() {
    // url of image to fetch
    const imageUrl = "https://avatars.githubusercontent.com/u/314135";
 
    // fetch image and get the Uint8Array instance
    const inputBytes = await fetch(imageUrl)
      .then((res) => res.arrayBuffer())
      .then((buffer) => new Uint8Array(buffer));
 
    // create a PhotonImage instance
    const inputImage = PhotonImage.new_from_byteslice(inputBytes);
 
    // resize image using photon
    const outputImage = resize(
      inputImage,
      inputImage.get_width() * 0.5,
      inputImage.get_height() * 0.5,
      SamplingFilter.Nearest
    );
 
    // get webp bytes
    const outputBytes = outputImage.get_bytes_webp();
 
    // for other formats
    // png  : outputImage.get_bytes();
    // jpeg : outputImage.get_bytes_jpeg(quality);
 
    // call free() method to free memory
    inputImage.free();
    outputImage.free();
 
    // return the Response instance
    return new Response(outputBytes, {
      headers: {
        "Content-Type": "image/webp"
      }
    });
  }
} satisfies ExportedHandler;

Astro with cloudflare adapter

If you are using Astro framework with @astrojs/cloudflare adapter, you need to manually initialize photon with wasm module before using any exported function.

Here is an example for an endpoint.

src/pages/photon.ts
import {
  PhotonImage,
  resize,
  SamplingFilter,
  initPhoton
} from "@cf-wasm/photon/others";
import photonModule from "@cf-wasm/photon/photon.wasm";
import type { APIRoute } from "astro";
 
// initialize photon with wasm module before using any exported function
// also check if it is not already initialized
if (!initPhoton.initialized) {
  await initPhoton({ module_or_path: photonModule });
}
 
const imageUrl = "https://avatars.githubusercontent.com/u/314135";
 
export const GET: APIRoute = async () => {
  // fetch image and get the Uint8Array instance
  const inputBytes = await fetch(imageUrl)
    .then((res) => res.arrayBuffer())
    .then((buffer) => new Uint8Array(buffer));
 
  // create a PhotonImage instance
  const inputImage = PhotonImage.new_from_byteslice(inputBytes);
 
  // resize image using photon
  const outputImage = resize(
    inputImage,
    inputImage.get_width() * 0.5,
    inputImage.get_height() * 0.5,
    SamplingFilter.Nearest
  );
 
  // get webp bytes
  const outputBytes = outputImage.get_bytes_webp();
 
  // for other formats
  // png  : outputImage.get_bytes();
  // jpeg : outputImage.get_bytes_jpeg(quality);
 
  // call free() method to free memory
  inputImage.free();
  outputImage.free();
 
  // return the Response instance
  return new Response(outputBytes, {
    headers: {
      "Content-Type": "image/webp"
    }
  });
};

Handling Large Images

Cloudflare Workers is capable of handling large image files, but it’s important to be mindful of worker memory limits (typically 128MB). If your image exceeds this limit, consider adding image size checks or chunking the image into smaller parts before processing it. Additionally, Cloudflare Workers are great at handling dynamic image resizing—perfect for serving images at different resolutions based on device or network conditions.

Conclusion

By leveraging Cloudflare Workers and the power of WebAssembly with Photon, you can efficiently process images at the edge, optimizing them on the fly and serving them in various formats like WebP, JPEG, or PNG. This provides a scalable, fast, and cost-effective solution for image manipulation, and the example above demonstrates how easy it is to implement this powerful functionality in your serverless projects.

Whether you’re building an image-heavy site or need dynamic resizing for a diverse set of devices, Cloudflare Workers combined with Photon can help you achieve high performance and low latency with minimal overhead.