Azure Function Image Compression — Generate Multiple Image Sizes

In this article we are going to learn how to use an Azure Function to automatically compress and resize uploaded images into multiple sizes like 16x16, 32x32, 64x64, 128x128, and so on — without doing any of that work inside your main API.

Here's a situation I've seen in a lot of projects. User uploads a profile picture. It's 4MB, 3000x3000 pixels. The mobile app needs a 64x64 thumbnail. The web app needs 256x256. The admin panel shows the original. So now you have three components each loading a 4MB image and resizing it in the browser or app. Terrible for performance, terrible for data usage.

The right way to handle this is to generate all the sizes you need right after upload. Store each size as a separate file in Blob Storage. Each component loads exactly the size it needs. Fast, clean, efficient.

Azure Functions are perfect for this. When a file lands in Blob Storage, a trigger fires automatically, the function generates all the resized versions, and saves them back. Your main API doesn't do any of this work.

This tutorial shows how to:

  • Set up a Blob Storage trigger on an Azure Function
  • Install and use the ImageSharp library for image resizing in .NET
  • Generate multiple image sizes from a single uploaded image
  • Save all resized versions back to Azure Blob Storage
  • Organise resized images in a clean folder structure
  • Test the function locally before deploying to Azure

Why Azure Function for Image Processing

Image resizing is CPU-intensive. If you do it inside your Web API, every upload request blocks a thread while the image is being processed. With a 4MB image generating 6 different sizes, that can take a second or two. Scale that to 100 concurrent uploads and your API is grinding.

Azure Function with a Blob trigger runs completely separately. Upload API saves the original file and returns immediately. Function picks it up in the background, does all the resizing, saves the results. User gets a fast response and the processing happens without touching your API at all.


Step 1 : Create the Azure Function Project

If you don't have Azure Functions Core Tools installed, install it first :

npm install -g azure-functions-core-tools@4 --unsafe-perm true

Create a new Azure Functions project :

func init ImageResizerFunction --worker-runtime dotnet-isolated
cd ImageResizerFunction
func new --name ResizeImages --template "Azure Blob Storage trigger"

This creates a .NET isolated process Function project with a Blob trigger template. The isolated process model is the current recommended approach for .NET Azure Functions.

Open the project in Visual Studio or VS Code.


Step 2 : Install ImageSharp for Image Resizing

ImageSharp is the best image processing library for .NET right now. It's fully managed code — no native dependencies, works perfectly on Linux and Azure Functions.

Install it :

dotnet add package SixLabors.ImageSharp
dotnet add package Azure.Storage.Blobs

Step 3 : Configure Blob Storage Connection

Open local.settings.json and add your storage account connection string :

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "BlobStorageConnection": "DefaultEndpointsProtocol=https;AccountName=youraccount;AccountKey=yourkey;EndpointSuffix=core.windows.net",
    "OriginalContainer": "uploads",
    "ResizedContainer": "resized"
  }
}

Create two containers in your Azure Storage account :

  • uploads — where original images get uploaded
  • resized — where the function saves all the resized versions

Step 4 : Write the Image Resize Function

Open the generated function file and replace everything with this :

using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;

public class ResizeImages
{
    private readonly ILogger<ResizeImages> _logger;

    // Define all the sizes you want to generate
    private static readonly (int Width, int Height, string Suffix)[] ImageSizes =
    {
        (16, 16, "16x16"),
        (32, 32, "32x32"),
        (64, 64, "64x64"),
        (128, 128, "128x128"),
        (256, 256, "256x256"),
        (512, 512, "512x512")
    };

    public ResizeImages(ILogger<ResizeImages> logger)
    {
        _logger = logger;
    }

    [Function("ResizeImages")]
    public async Task Run(
        [BlobTrigger("uploads/{name}", Connection = "BlobStorageConnection")] Stream imageStream,
        string name,
        FunctionContext context)
    {
        _logger.LogInformation($"Processing image: {name}");

        // Only process image files
        var extension = Path.GetExtension(name).ToLower();
        if (!new[] { ".jpg", ".jpeg", ".png", ".webp", ".gif" }.Contains(extension))
        {
            _logger.LogInformation($"Skipping non-image file: {name}");
            return;
        }

        var connectionString = Environment.GetEnvironmentVariable("BlobStorageConnection");
        var resizedContainer = Environment.GetEnvironmentVariable("ResizedContainer") ?? "resized";

        var blobServiceClient = new BlobServiceClient(connectionString);
        var containerClient = blobServiceClient.GetContainerClient(resizedContainer);
        await containerClient.CreateIfNotExistsAsync();

        // Load the original image once
        using var originalImage = await Image.LoadAsync(imageStream);
        var fileNameWithoutExt = Path.GetFileNameWithoutExtension(name);

        foreach (var (width, height, suffix) in ImageSizes)
        {
            try
            {
                using var resizedImage = originalImage.Clone(ctx =>
                    ctx.Resize(new ResizeOptions
                    {
                        Size = new Size(width, height),
                        Mode = ResizeMode.Crop,
                        Position = AnchorPositionMode.Center
                    })
                );

                using var outputStream = new MemoryStream();
                await resizedImage.SaveAsJpegAsync(outputStream);
                outputStream.Position = 0;

                // Save as: resized/filename/filename_64x64.jpg
                var blobName = $"{fileNameWithoutExt}/{fileNameWithoutExt}_{suffix}.jpg";
                var blobClient = containerClient.GetBlobClient(blobName);

                await blobClient.UploadAsync(outputStream, new BlobUploadOptions
                {
                    HttpHeaders = new BlobHttpHeaders { ContentType = "image/jpeg" },
                    Overwrite = true
                });

                _logger.LogInformation($"Saved {suffix} version: {blobName}");
            }
            catch (Exception ex)
            {
                _logger.LogError($"Failed to generate {suffix} for {name}: {ex.Message}");
            }
        }

        _logger.LogInformation($"Finished processing {name}. Generated {ImageSizes.Length} sizes.");
    }
}

A few things worth explaining here.

The ImageSizes array at the top is where you define all the sizes you want. Add or remove entries here to change what gets generated. No other code needs to change.

ResizeMode.Crop with AnchorPositionMode.Center — this crops from the center to fit the target dimensions exactly. So a 3000x2000 landscape image resized to 64x64 gets the center portion cropped, not stretched. If you want to maintain aspect ratio and pad with white space instead, change to ResizeMode.Pad.

The try/catch around each size means if one size fails, the others still get generated. You don't lose all sizes because one had an issue.

The blob naming pattern {filename}/{filename}_{suffix}.jpg creates a folder per image in Blob Storage. So uploading profile-photo.jpg creates :

resized/
  profile-photo/
    profile-photo_16x16.jpg
    profile-photo_32x32.jpg
    profile-photo_64x64.jpg
    profile-photo_128x128.jpg
    profile-photo_256x256.jpg
    profile-photo_512x512.jpg

Clean and easy to find.


Step 5 : Register Dependencies in Program.cs

using Microsoft.Extensions.Hosting;

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .Build();

host.Run();

If you need to inject additional services like a database context or custom services, add them here with ConfigureServices() just like a regular .NET app.


Step 6 : Test Locally with Azurite

Azurite is a local Azure Storage emulator. Instead of using a real Azure storage account during development, Azurite runs locally and emulates Blob Storage.

Install Azurite :

npm install -g azurite

Start it :

azurite --silent --location ./azurite-data

In local.settings.json, AzureWebJobsStorage is already set to UseDevelopmentStorage=true which points to Azurite automatically.

Now run the function :

func start

Use Azure Storage Explorer (free desktop app from Microsoft) to connect to the local Azurite instance. Create an uploads container, upload an image file into it, and watch the function trigger. After a few seconds you'll see the resized container appear with all the generated sizes inside.


Step 7 : Deploy to Azure

Make sure your function app exists in Azure portal. If not, create one :

Go to Azure portal → Create a resource → Function App → select .NET 8 Isolated, Consumption plan, same region as your storage account.

Deploy from Visual Studio — right click the Functions project → Publish → Azure → Function App → select your function app.

Or deploy via Azure DevOps pipeline or GitHub Actions with the Azure Functions Deploy task.

After deploying, go to your Function App in Azure portal → Configuration → Application settings. Add :

BlobStorageConnection  =  your full connection string
OriginalContainer      =  uploads
ResizedContainer       =  resized

Then go to Function App → Functions → ResizeImages → Monitor to watch function executions and logs in real time.


Handling Non-Square Images

Right now the function crops images to square. That works for profile pictures and icons. But for banner images or thumbnails where you want to keep the aspect ratio, adjust the ResizeOptions :

// Keep aspect ratio — no cropping, fit within bounds
ctx.Resize(new ResizeOptions
{
    Size = new Size(width, height),
    Mode = ResizeMode.Max  // Scales down maintaining aspect ratio
})

Or if you want the image to fill the dimensions and allow some cropping only if necessary :

Mode = ResizeMode.Min  // Scale to fill minimum dimension

Pick the mode that matches your use case. Profile pictures → Crop. Product images → Max to avoid distortion.


Common Issues

Function triggers but image stream is empty

This can happen if the stream position isn't at the beginning. The Blob trigger gives you the stream at position 0 by default, but if something reads it before your code does, add imageStream.Seek(0, SeekOrigin.Begin) at the start of the function.

Function not triggering when image is uploaded

Check the container name in the BlobTrigger attribute matches exactly — "uploads/{name}". Container names are case-sensitive. Also check the Connection parameter value matches the key in local.settings.json.

Out of memory on large images

Processing a 20MB RAW image in memory can cause issues on the Consumption plan which has limited memory. Add a file size check at the start and reject files over a certain size, or switch to a Premium plan with more memory.

Generated images look blurry

ImageSharp's default resampling is Bicubic which is good quality. If you need sharper results for small sizes like 16x16, try Lanczos3 resampler :

ctx.Resize(new ResizeOptions
{
    Size = new Size(width, height),
    Mode = ResizeMode.Crop,
    Sampler = KnownResamplers.Lanczos3
})

Summary

You learned how to use an Azure Function to automatically generate multiple image sizes from a single upload. You covered :

  • Creating a .NET isolated Azure Function project with a Blob Storage trigger
  • Installing SixLabors.ImageSharp for image processing
  • Configuring local.settings.json with storage connection strings
  • Writing the resize function with configurable sizes array and per-size error handling
  • Using ResizeMode.Crop for square images and ResizeMode.Max for maintaining aspect ratio
  • Organising resized images in a clean folder structure in Blob Storage
  • Testing locally with Azurite and Azure Storage Explorer
  • Deploying to Azure and configuring app settings

The pattern works for any kind of image variant generation — icons, thumbnails, previews, watermarked copies. Change the ImageSizes array and the resize logic and the same function handles any scenario.

I hope you like this article...

Happy coding! 🚀

Post a Comment

0 Comments