In a minimal API endpoint, pass the upload’s stream to Image.Thumbnail and save the result. That one call turns phone photos upright, shrinks them to a maximum width, converts to sRGB and removes EXIF, so no GPS position is stored. Wrap it in a try for ImageException to answer 400 when the upload isn’t an image.
dotnet new web -o Uploads && cd Uploads dotnet add package RecursiveAI.OneImage --version 1.10.4 curl -L -o earth.jpg \ https://images-assets.nasa.gov/image/as17-148-22727/as17-148-22727~orig.jpg repo=https://raw.githubusercontent.com/recurser/exif-orientation-examples curl -L -o phone.jpg $repo/master/Portrait_6.jpg
OneImage needs the .NET 10 SDK. Replace Program.cs with:
using OneImage; var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); string folder = Path.Combine(app.Environment.ContentRootPath, "uploads"); Directory.CreateDirectory(folder); app.MapPost("/photos", async (IFormFile file) => { try { await using Stream upload = file.OpenReadStream(); // Upright, at most 1600 px wide, sRGB, EXIF and GPS removed. Image photo = Image.Thumbnail(upload, 1600); string path = Path.Combine(folder, $"{Guid.NewGuid():N}.webp"); await photo.SaveAsync(path); long bytes = new FileInfo(path).Length; return Results.Ok(new { photo.Width, photo.Height, bytes }); } catch (ImageException ex) // not an image, damaged, or over the size limits { return Results.BadRequest(ex.Message); } }).DisableAntiforgery(); app.Run();
Start it:
dotnet run --urls http://localhost:5000and post a photo, a phone photo stored sideways, and a file that isn’t an image, from a second terminal:
for f in earth.jpg phone.jpg Program.cs do echo "$f: $(curl -s -w ' (%{http_code})' -F "file=@$f" http://localhost:5000/photos)" done
earth.jpg: {"width":1600,"height":1600,"bytes":176776} (200) phone.jpg: {"width":1200,"height":1800,"bytes":174880} (200) Program.cs: "The data is not in a recognized image format." (400)
The 4579 px photo is stored 1600 px wide as WebP. The phone photo, stored as 1800×1200 with an EXIF orientation of 6, comes out upright and isn’t enlarged. Size limits are on by default and are checked from the file headers before large buffers are allocated, so an oversized upload fails with an ImageException too.
Crop a user-picked square
Avatar pickers send the square the user dragged in the browser with the file. Browsers show photos upright, and so does OneImage, so the coordinates line up. This program has only the avatar endpoint; in your app it sits next to the first one:
using OneImage; var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapPost("/avatar", async (IFormFile file, int x, int y, int size) => { try { await using Stream upload = file.OpenReadStream(); Image photo = Image.Load(upload); // upright, like the preview the user saw // Keep the square inside the image instead of failing on a stray pixel. x = Math.Clamp(x, 0, photo.Width - 1); y = Math.Clamp(y, 0, photo.Height - 1); size = Math.Min(size, Math.Min(photo.Width - x, photo.Height - y)); Image avatar = photo.Crop(x, y, size, size) .Resize(width: 256, height: 256) .StripMetadata(); var webp = new MemoryStream(); await avatar.SaveAsync(webp, WebPFormat.Instance); // streams name the format webp.Position = 0; return Results.File(webp, "image/webp"); } catch (ImageException ex) { return Results.BadRequest(ex.Message); } }).DisableAntiforgery(); app.Run();
dotnet run --urls http://localhost:5000curl -s -o avatar.webp -w '%{http_code} %{content_type}, %{size_download} bytes\n' \ -F [email protected] 'http://localhost:5000/avatar?x=100&y=300&size=800'
200 image/webp, 2452 bytesLimits
- Uploads are untrusted. Keep the
try: damaged data can surface atSaveAsync, not only at load. LowerLoadOptions.Limitsif you accept smaller images than the defaults. - ASP.NET Core’s request size limit (about 28.6 MB by default in Kestrel) applies before OneImage sees the file.
To make many sizes on request instead of one at upload, see Resize images on the fly. For the options of Thumbnail, see Create a thumbnail.