Trying out imgproxy together with CMS 12 to resize images

I stumbled over the very interesting tool imgproxy and decided to try it out to optimize images on the fly from and behind an Optimizely CMS app.

Image manipulation such as format conversion or resizing with Optimizely CMS is in a bit of a limbo to me. I think this DXP feature request to support resizing on Cloudflare is what would make me the most happy.

In-process might also be a bit problematic after pretty much the only option for modern dotnet, ImageSharp from Six Labors, altered its license.

Other DAM options in some cases introduces new parts of the UI for selecting images and a heavier integration dependency in general which might feel too much for some sites.

In general there might also be support for modern image formats missing.

Out-of-process feels good to me and imgproxy promises to be extremely fast and feature rich with optimizing while not taking on too many related features such as caching.

It also seems proven and easy enough to host using some container based hosting.

Follow along to start testing it out locally using Docker Desktop and Alloy.

docker pull darthsim/imgproxy:v3.9.0

Generate some random strings and encode them to HEX, using some online tool in my case.

These are used to create signatures to avoid imgproxy misuse.

  • To use as key: 36605c60erdd
    Key encoded to HEX: 333636303563363065726464
  • To use as salt: 3977d9ceds
    Salt encoded to HEX: 33393737643963656473

Then use the HEX values when starting the container and also set some things to get the most modern formats if accepted and also some extra debug information headers.

docker run --name imgproxy --env IMGPROXY_KEY=333636303563363065726464 --env IMGPROXY_SALT=33393737643963656473 --env IMGPROXY_ENABLE_DEBUG_HEADERS=true --env IMGPROXY_SECRET=tjabba --env IMGPROXY_ENABLE_WEBP_DETECTION=true --env IMGPROXY_ENFORCE_WEBP=true --env IMGPROXY_ENABLE_AVIF_DETECTION=true --env IMGPROXY_ENFORCE_AVIF=true --env IMGPROXY_FORMAT_QUALITY=jpeg=95,avif=75,webp=85 -p 8080:8080 -it darthsim/imgproxy:v3.9.0

Now imgproxy is running on http://localhost:8080 and I have set configuration variables to prioritize AVIF, but also support WEBP over JPG.

If you want to change something you need to remove the container before starting again.

docker container rm imgproxy

Then set up an Alloy site to get a CMS site quickly that has some images.

dotnet new epi-alloy-mvc
dotnet add package EPiServer.Cms
  • Check that Alloy works and add an admin user. Also make it run on HTTP instead of HTTPS locally, I use http://localhost:5000, to avoid SSL issues from imgproxy Docker container to Kestrel on host machine.
  • Add code to ConfigureServices and use the HEX encoded key and salt strings. To get help with the URLs and the encoding I use support in this imgproxy-dotnet repository on GitHub. It's also on NuGet.
var imgProxyBuilder = ImgProxyBuilder.New
	.WithEndpoint("http://localhost:8080")
	.WithCredentials(
		key: "333636303563363065726464",
		salt: "33393737643963656473");
services.AddSingleton(imgProxyBuilder);

One of the environment variables I set was IMGPROXY_SECRET. I need to add that as an Authorization header when proxying images.

services.AddHttpClient("ImgProxy", httpClient =>
{
	httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer tjabba");
});

I think I will want to serve images proxied through the site and at some point use whatever caching that is available there.

A quick proof-of-concept of how such a proxy endpoint could work, not worrying about caching.

endpoints.MapGet("/imgproxy/{*path}", async context =>
{
    var path = context.Request.Path.Value;

    if (string.IsNullOrWhiteSpace(path))
    {
        return;
    }

    var remotePath = path[10..];

    var httpClientFactory = context.RequestServices.GetRequiredService<IHttpClientFactory>();
    var httpClient = httpClientFactory.CreateClient("ImgProxy");
    var requestMessage = new HttpRequestMessage(HttpMethod.Get, new Uri("http://localhost:8080/" + remotePath));

    foreach (var header in context.Request.Headers)
    {
        requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
    }

    using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted);
    context.Response.StatusCode = (int)responseMessage.StatusCode;

    foreach (var header in responseMessage.Headers)
    {
        context.Response.Headers.TryAdd(header.Key, header.Value.ToArray());
    }

    foreach (var header in responseMessage.Content.Headers)
    {
        context.Response.Headers.TryAdd(header.Key, header.Value.ToArray());
    }

    await responseMessage.Content.CopyToAsync(context.Response.Body);
});

I will probably get YARP installed to handle this better moving forward, or configure some proxy or CDN with caching support to connect to imgproxy directly.

Next up: Add another quick proof-of-concept in the start page view to generate a URL and see that the proxy works.

@using ImgProxy
@inject ImgProxyBuilder imgProxyBuilder

@{
    var additionalOptions = new ImgProxyOption[]
    {
        new ResizeOption(ResizingTypes.Fill, 210, 300),
        new GravityOption(GravityTypes.NorthEast),
    };

    // Use an image coming along with the Alloy content
    var url = imgProxyBuilder.Build("http://host.docker.internal:5000/globalassets/pexels-office.jpg", additionalOptions, encode: false);
    var localProxiedUrl = url.Replace("http://localhost:8080/", "/imgproxy/");
}

<img src="@localProxiedUrl" alt="" style="border: 3px solid red; margin-bottom: 20px;" />

At this time localProxiedUrl is the following.

/imgproxy/HO0fvgcPH61OWiOCXtVL97xjn_ZABs6hkVI92eeLev8/resize:fill:300:400:0:0/gravity:noea/plain/http://host.docker.internal:5000/globalassets/pexels-office.jpg

If you use encode: true the URL comes out as this.

/imgproxy/MW51RkZ1IiQ0_HxMYmHUYC_4Sz9wmWiOS-UVwa3sVn8/resize:fill:210:300:0:0/gravity:noea/aHR0cDovL2hvc3QuZG9ja2VyLmludGVybmFsOjUwMDAvZ2xvYmFsYXNzZXRzL3BleGVscy1vZmZpY2UuanBn

And... It works!

Alloy start page screenshot showing the image cropped from the top right

Inspecting the headers of the resized image shows that my Firefox can handle AVIF and got that format.

HTTP/1.1 200 OK
Content-Length: 8921
Content-Type: image/avif
Date: Wed, 19 Oct 2022 22:15:42 GMT
Server: imgproxy
Cache-Control: public, max-age=31536000
Expires: Thu, 19 Oct 2023 22:15:42 GMT
Vary: Accept
X-Origin-Content-Length: 138319
X-Origin-Height: 853
X-Origin-Width: 1280
X-Request-ID: NtuC2AROmq2SYDQ2Ro_tm
X-Result-Height: 300
X-Result-Width: 210
Content-Disposition: inline; filename="pexels-office.avif"

I think the quality looks good but of course this is where a lot of tweaking might be needed to find the best mix of quality and file size. If you pay for imgproxy this is supposedly handled well and more automatic by the Pro features.

Comments?

Published and tagged with these categories: Optimizely, CMS, ASP.NET