Skip to content

RESOURCES / BLOG

Using Cloudinary Custom WASM Functions for Image Transformations

Cloudinary offers powerful image and video transformation capabilities. When you need to apply a custom filter, execute pixel-level logic, or perform proprietary image manipulation not covered by the built-in effects, Cloudinary Custom Functions is available. This feature allows you to extend Cloudinary’s core capabilities by injecting your custom logic into the image processing pipeline. Cloudinary has different types of custom functions to suit your creative needs.

In this post, we’ll focus on the implementation of Cloudinary Custom Functions with WebAssembly (WASM). This approach allows you to inject your own compiled code written in languages like Rust or C/C++ directly into Cloudinary’s image processing pipeline for high-performance, pixel-level control.

We’ll explore what these WASM-based custom functions are, how to write and export the required functions, how to compile and upload the WASM module, and how to use your custom function in a transformation URL.

A custom WASM function in Cloudinary is a user-defined transformation module that runs inside a secure, sandboxed environment. These functions operate directly on raw pixel data in RGBA format. This makes them ideal for implementing custom filters such as sepia or specialized color effects, creating proprietary branding overlays, executing artistic or AI-assisted transformations, or applying logic based on metadata or external variables.

To be compatible with Cloudinary’s execution engine, the WASM file must implement a specific contract using a fixed interface.

Every custom WASM module must export three functions that form the interface contract with Cloudinary:

  • The alloc(size: i32) -> i32 function allocates a block of memory within the WASM instance. Cloudinary uses this to pass pixel and metadata buffers into your module.
  • The dealloc(ptr: i32, size: i32) function releases previously allocated memory. WebAssembly doesn’t have automatic memory management, so this manual step helps to prevent memory leaks.
  • The transform(width: i32, height: i32, pixel_ptr: i32, meta_ptr: i32, meta_size: i32) -> i32 function is the core processing logic. Cloudinary calls it with the image dimensions, a pointer to the RGBA buffer, and an optional JSON metadata block. It must return a pointer to a new buffer that starts with the new width and height, followed by the transformed pixels.

Let’s walk through a basic Rust implementation of a grayscale filter, compiling it to WebAssembly, and preparing it for Cloudinary.

First, we’ll set up a new Rust library project. Open your terminal or command prompt and run:

cargo new grayscale_filter --lib
cd grayscale_filter
Code language: JavaScript (javascript)

These commands create a new directory named grayscale_filter with the basic Rust project structure inside and switch to this directory.

Next, we need to tell Rust to compile our code into a WebAssembly dynamic library. Open the Cargo.toml file within your grayscale_filter directory and add the following lines under the [lib] section:

[lib]
crate-type = ["cdylib"]
Code language: JavaScript (javascript)

The crate-type = ["cdylib"] setting is crucial for WebAssembly. It instructs Rust to compile your code as a C-compatible dynamic library. This ensures the output .wasm file is a standalone binary that directly exposes the functions marked pub extern "C", making them accessible to external hosts like Cloudinary without any Rust-specific runtime dependencies or name mangling.

Now, let’s write the core logic for our grayscale filter. Open the src/lib.rs file and replace its contents with the following Rust code:

#[unsafe(no_mangle)]
pub extern "C" fn alloc(size: usize) -> *mut u8 {
    let mut buf = Vec::with_capacity(size);
    let ptr = buf.as_mut_ptr();
    std::mem::forget(buf);
    ptr
}

#[unsafe(no_mangle)]
pub extern "C" fn dealloc(ptr: *mut u8, size: usize) {
    unsafe { let _ = Vec::from_raw_parts(ptr, size, size); }
}

#[unsafe(no_mangle)]
pub extern "C" fn transform(width: i32, height: i32, ptr: *mut u8, _meta_ptr: *mut u8, _meta_size: i32) -> *mut u8 {
    // Calculate the total number of bytes in the RGBA pixel buffer
    let len = (width * height * 4) as usize;
    // Create a mutable slice from the raw pixel pointer
    let pixels = unsafe { std::slice::from_raw_parts_mut(ptr, len) };

    // Iterate over chunks of 4 bytes (RGBA for each pixel)
    for chunk in pixels.chunks_mut(4) {
        // Calculate the average of R, G, and B channels
        let avg = ((chunk[0] as u16 + chunk[1] as u16 + chunk[2] as u16) / 3) as u8;
        // Set all R, G, B channels to the average value for grayscale effect
        chunk[0] = avg; // Red
        chunk[1] = avg; // Green
        chunk[2] = avg; // Blue
        // Alpha (chunk[3]) remains unchanged
    }

    // Prepare the output buffer. It needs to start with new width/height (4 bytes each)
    // followed by the transformed pixel data.
    let mut out = Vec::<u8>::with_capacity(8 + len);
    // Add the new width and height as big-endian 32-bit integers
    out.extend(& (width  as u32).to_be_bytes());
    out.extend(& (height as u32).to_be_bytes());
    // Add the transformed pixel data
    out.extend_from_slice(pixels);

    // Get a raw pointer to the start of our output vector
    let out_ptr = out.as_mut_ptr();
    // Prevent Rust from deallocating the Vec when it goes out of scope,
    // as Cloudinary will now manage this memory.
    std::mem::forget(out);
    out_ptr
}
Code language: PHP (php)

This code implements the three required functions: alloc, dealloc, and transform. The transform function takes the RGBA pixel data, processes each pixel to convert it to grayscale, and then returns a pointer to a new buffer containing the modified image dimensions and pixels. Each function is marked with #[unsafe(no_mangle)] pub extern "C" to ensure it’s publicly accessible, retains its original name, and uses a C-compatible calling convention, which is essential for Cloudinary’s WASM runtime to invoke it.

Now, we’ll compile our Rust code into a WebAssembly module. First, ensure you have the wasm32-unknown-unknown compilation target added to your Rust toolchain:

rustup target add wasm32-unknown-unknown

This target tells Rust to compile your code to 32-bit WebAssembly without assuming any operating system or runtime interface. This is essential because Cloudinary executes your code in a very minimal environment.

Next, compile your project in release mode for optimization:

cargo build --release --target wasm32-unknown-unknown

The resulting .wasm file will be located in the target/wasm32-unknown-unknown/release/ directory. For this example, it will likely be named grayscale_filter.wasm.

Once you’ve compiled your module, you need to upload it to your Cloudinary account as a raw and authenticated resource. Cloudinary rejects public uploads of custom functions due to security considerations.

Here’s an example curl command to upload your WASM module. Make sure to replace <cloud_name>, the @target/.../grayscale_filter.wasm file path, <ts> for timestamp, <your_api_key>, and <your_signature> with your actual Cloudinary information:

curl -X POST https://api.cloudinary.com/v1_1/<cloud_name>/raw/upload \
  -F file=@target/wasm32-unknown-unknown/release/grayscale_filter.wasm \
  -F public_id=custom/grayscale-filter \
  -F resource_type=raw \
  -F type=authenticated \
  -F api_key=<your_api_key> \
  -F timestamp=<ts> \
  -F signature=<your_signature>
Code language: HTML, XML (xml)

Important: Your signature must be properly generated using your Cloudinary API secret and the other upload parameters. For more details on how to manually generate a signature, see Cloudinary’s Generating authentication signatures documentation.

To invoke your custom transformation, use the fn_wasm action in a standard Cloudinary image URL. When referencing your WASM module’s public_id, remember that any slashes in the public_id (e.g., custom/grayscale-filter) must be replaced with colons in the URL (e.g., custom:grayscale-filter).

Here’s an example:

https://res.cloudinary.com/<cloud_name>/image/upload/fn_wasm:custom:grayscale-filter.wasm/sample.jpg
Code language: HTML, XML (xml)

You can also combine it with other transformations like resizing or cropping:

https://res.cloudinary.com/<cloud_name>/image/upload/fn_wasm:custom:grayscale-filter.wasm/w_500,c_fill/sample.jpg
Code language: HTML, XML (xml)

This URL tells Cloudinary to apply your grayscale filter module as part of the image processing pipeline.

As you build and deploy your custom WASM functions, a few best practices will help make them stable and efficient.

To manage updates and maintain old links, consider using versioned paths for your modules, such as custom/grayscale-v1. Such versioning helps ensure that old links continue to work smoothly even when you update your functions.

Also, when you’re directly handling memory inside your WASM functions, be extremely careful with pointer safety. A small mistake can lead to transformation failures or corrupted output, so handle memory with care.

For more flexibility, you can pass configuration data, such as filter strength or blending options, as JSON through the meta_ptr. Cloudinary automatically adds this data based on your transformation settings, giving you dynamic control over your custom functions.

Lastly, for the best performance, try to keep your modules small and predictable. It’s usually best to avoid outside dependencies, dynamic memory use, or randomness unless they are absolutely necessary for what your function needs to do.

Cloudinary Custom WASM Functions provide fine-grained control for developers who need precise, fast, and flexible image transformation. The combination of WebAssembly’s speed and Cloudinary’s powerful API enables you to create custom filters, unique improvements, or full visual pipelines that fit your app’s exact needs. Sign up for a free Cloudinary account to get started.

Start Using Cloudinary

Sign up for our free plan and start creating stunning visual experiences in minutes.

Sign Up for Free