Recursive AI

Resize images on the fly in ASP.NET Core

All oneimage recipes

Add the RecursiveAI.OneImage.Web middleware before the static files. Every image under wwwroot can then be requested in another size or format, as in /photos/earth.jpg?width=400: it is made on the first request and served from a disk cache after that. Two lines in Program.cs set it up:

Terminal
dotnet new web -o ImageServer && cd ImageServer
dotnet add package RecursiveAI.OneImage.Web --version 1.10.4
mkdir -p wwwroot/photos
curl -L -o wwwroot/photos/earth.jpg \
  https://images-assets.nasa.gov/image/as17-148-22727/as17-148-22727~orig.jpg

OneImage needs the .NET 10 SDK. Replace Program.cs with:

C#
using OneImage.Web;
 
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOneImage();
 
var app = builder.Build();
app.UseOneImage();      // before UseStaticFiles, so it sees image requests first
app.UseStaticFiles();
app.Run();

Start it:

Terminal
dotnet run --urls http://localhost:5000

and request a few sizes of NASA’s “Blue Marble” photograph (4579×4579, 1.9 MB) from a second terminal:

Terminal
base=http://localhost:5000/photos/earth.jpg
fmt="%{http_code} %{content_type}, %{size_download} bytes\n"
for q in "width=400" "w=300&h=300&fit=cover" "width=800&format=webp&q=80" \
         "" "width=abc"
do
  curl -s -o /dev/null -w "?$q: $fmt" "$base?$q"
done
Text
?width=400: 200 image/jpeg, 22885 bytes
?w=300&h=300&fit=cover: 200 image/jpeg, 13927 bytes
?width=800&format=webp&q=80: 200 image/webp, 69596 bytes
?: 200 image/jpeg, 1915552 bytes
?width=abc: 400 text/plain; charset=utf-8, 26 bytes

A request without commands falls through to the static file middleware untouched, and an invalid value is a 400, never guessed at. Each result goes through the thumbnail pipeline: a large JPEG is decoded at reduced scale, phone photos are turned upright, and EXIF, XMP and IPTC (which can hold a GPS position) are removed. Results carry an ETag, Last-Modified and a 7-day Cache-Control.

CommandValues
width, height (w, h)Pixels; with one of them, the other follows the aspect ratio
fitcontain (default), cover, stretch, pad
anchor, focusWhat a crop keeps: an edge, a corner, smart, or a point such as focus=0.3,0.6
formatjpg, png, webp, gif, bmp
quality (q)1 to 100, for JPEG and WebP
bgPadding color, and the color transparency is flattened onto

Presets and signed URLs for public sites

On a public site, anyone can ask for any size, and every distinct size becomes a cache entry. Allow only named presets, or sign the URLs your pages generate. With presets only:

C#
using OneImage.Web;
 
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOneImage(o =>
{
    o.Presets["thumb"] = "width=200&height=200&fit=cover&format=webp";
    o.Presets["hero"] = "width=1600&format=webp&quality=80";
    o.PresetsOnly = true;   // ?preset=thumb works; ?width=123 is ignored
});
 
var app = builder.Build();
app.UseOneImage();
app.UseStaticFiles();
app.Run();
Terminal
dotnet run --urls http://localhost:5000
Terminal
base=http://localhost:5000/photos/earth.jpg
fmt="%{http_code} %{content_type}, %{size_download} bytes\n"
for q in "preset=thumb" "preset=hero" "width=123" "preset=huge"
do
  curl -s -o /dev/null -w "?$q: $fmt" "$base?$q"
done
Text
?preset=thumb: 200 image/webp, 5356 bytes
?preset=hero: 200 image/webp, 213684 bytes
?width=123: 200 image/jpeg, 1915552 bytes
?preset=huge: 400 text/plain; charset=utf-8, 15 bytes

?width=123 is ignored, so the original is served, and an unknown preset is a 400. To keep free-form commands but accept only URLs you generated, set o.HmacSecretKey (at least 32 random bytes, from configuration) and build URLs with the ImageUrlSigner service: unsigned or altered URLs get a 403. The OneImage.Web docs show it, with the Razor tag helper.

Limits

  • Output is capped at 4096 px a side and 16.7 million pixels by default (MaxWidth, MaxHeight), and sources at 64 MiB.
  • The cache is a folder (oneimage-cache in the content root, outside wwwroot); set ConfigureCache(c => c.CacheRootPath = ...) for another location. It isn’t shared between servers.
  • Coming from ImageSharp.Web? AddOneImage(o => o.UseImageSharpDefaults()) accepts its query spellings (rmode, ranchor, bgcolor…), so stored URLs keep working. rmode=manual isn’t supported, and signed URLs need new tokens. See the ImageSharp migration guide.

The OnBeforeSaveAsync hook can watermark every image as it is served. When you know the sizes up front, resizing once at upload is simpler: see Resize uploaded images.