ASP.NET Core Image Resizing Middleware
The .NET Framework people have it easy when it comes to image resizing. You can't go past the excellent ImageResizer Package.
Unfortunately, at the time of writing this, I was unable to find a mature-enough cross-platform .net core equivalent.
Even forgetting about resizing and caching images on the fly from a URL, the options are still limited. This is because the Full .NET Framework uses the System.Drawing library, which wraps the Windows-only GDI library.
The options appear limited to one of:
- ImageSharp - looks superb, but it hasn't officially shipped yet, and is still considered alpha stage by the authors.
- SkiaSharp - based on google's Skia library, maintained by the Xamarin team. Installable from Nuget. They don't ship Linux/Mac binaries but you can compile it from source pretty easily. It also seems to be a little faster than ImageSharp.
- ImageFlow - will have everything I need and should be a drop-in replacement once shipped. It's incomplete at the time of writing and is presently in demo stage.
I went with SkiaSharp for this purely because it seems to be the most mature solution, though it has a few bugs of its own.
The Problem
I want to be able to, at a minimum:
- Resize images on the web server via a URL
- Optionally rotate them according to their EXIF headers (e.g. for phone camera uploads)
- Change the output format
- Change the quality
- Crop, pad, stretch to fit new size, or just set either the width or height and let it automatically determine the other dimension, or fit it within a bounding box.
- Preserve transparency if present
- Cache the output
The Solution
Middleware! I've talked about .net core's middleware before, it's pretty neat compared to the old way of doing things. This diagram from the docs illustrates the concept succintly:
All we need to do is write our own middleware. Our middleware should:
- Be injected into the pipeline ahead of the static file handler (to prevent the static file handler from handling image resize requests)
- Make a cursory inspection of the URL to determine if it's an image, and if not, pass it on
- Make a further inspection to determine if it's an image that actually exists on disk, with valid resize parameters, and if so, handle it, and prevent the rest of the pipeline from taking part in the request.
- Since resizing is an expensive operation, try to serve the request from a cache first, otherwise resize it and cache the output.
Doesn't sound too complex... let's get coding.
Dev Environment
- The dev environment, all tools & code will run on Mac, PC or Linux
- We are using Visual Studio Code and a command prompt / terminal window.
- Make sure you have the following Visual Studio Code Extensions (click "Extensions" in the sidebar): C#, C# Extensions
Solution Setup
In a new folder execute the following commands:
The above commands will create our ImageResizer library (to hold our middleware), an MVC web app with default scaffolding, add them both to a solution, add a reference to the ImageResizer library to our Web project, and then open the whole lot.
Once it's open just accept all the prompts that VS code presents you with, which will restore missing packages etc.
The images in the default web app at the time of writing are SVG, which aren't suitable for us. For testing purposes I've been using these images as they allow me to test the orientation headers.
Throw some images into your wwwroot/images folder.
The Middleware
The ImageResizer has a few dependencies, which can be added using a terminal window in the ImageResizer folder, with:
Middleware
Create a new C# source file called ImageResizerMiddleware.cs and put our scaffolding in... let's just make sure it's handling resize requests first:
The above code checks if the request is for an image, and if it is, it extracts the query string into a struct, logs it, and then passes onto the next middleware in the chain.
Builder Extensions
So that we can easily add this middleware to our pipeline, we should write 2 very small extension methods to allow us to "AddImageResizer" and "UseImageResizer" in our MVC app. Let's do that now.
ImageResizerMiddlewareExtensions.cs
We are adding MemoryCache here, since it will be a dependency for our Middleware... we can use the IServiceCollection to add other dependencies when we expand our Middleware in future. We're simply wrapping the UseMiddleware statement into something a bit neater.
Wire it up to MVC
Easy to wire up. Just 3 lines of code. In the Web project's Startup.cs file:
Test it
Before we test, we should make sure our hosting environment is set to "Development" so that we get the information logs in our terminal.
On Windows (Vista and up)
On Mac
then add this line to the file:
export ASPNETCORE_ENVIRONMENT=development
You will need to close and re-open your terminal window of choice for this change to take effect.
From the Web folder, execute
You should see it start up on port 5000 in "Development" environment. Now just navigate to a PNG or JPG image in your solution, with a query string for resizing, and you should see something like (without the actual resize):
Great it's logging to console and letting our request through to the static file handler.
Add some logic
As at the time of writing, SkiaSharp seems to have a few issues. Notably, when loading an indexed 8-bit image (such as an optimized PNG), you encounter a few issues with resizing.
To overcome this, we need to write our own image loader that converts it to 32-bit.
Here's the image loader:
Cropping
One of the resize parameters "mode", can be used to crop to fit. We'll need a helper method to crop our image.
Padding
Optionally, we'll want to "pad" our image instead.
Dealing with EXIF Rotation
In SkiaSharp, the EXIF rotation comes through as an "Origin" value. You don't generally need to deal with these for images that you produce, but you need to deal with it for images that others might be uploading because modern phones store their image rotations this way.
There are 8 different orientations, represented in SkiaSharp as an Origin value:
Not only can the image be rotated, but also flipped, or some combination of the 2.
SkiaSharp provides a canvas for us and 2D matrix transformations that would be ideally suited to the task of fixing the rotation, but after some experimenting with less than desirable outcomes, I decided simply to copy the pixels.
The code for rotate and flip:
That's it for helper functions, almost.
Load and resize our image
Now we simply need to get our image data from disk (or cache), and apply our resize params to it. For this example, I'm loading from disk, and using an IMemoryCache, but I'd recommend abstracting the file loading and caching implementations to allow for a variety of sources (such as loading from S3, a remote URL, or caching in an IDistributedCache or on some file store.)
Get image data
Should really have some exception handling, it could be a bit leaky.
For the cache key, I'm using a long based on the image last write time, the resize params, and the image path. The "unchecked" statement is to allow our cache key to overflow safely. Instead of throwing an exception, it just "rolls around" to the other extremity.
We just check the cache, return that, else, we load our bitmap, resize it, optionally rotate it using the EXIF header, crop, pad, or "max" (which is best fit), encode it in our format of choice, cache that, then return that data.
Wire it all up
Lastly, we need to call our GetImageData method from the middleware's Invoke method. I've also added a "404" check. LastWriteTimeUtc returns midnight, January 1, 1601 if the file isn't present.
Inside Invoke:
Test it
A quick test with padding, an EXIF rotation and a format change:
And another one, without the EXIF rotation, cropped to fit a square:
In Summary
Given that options on .net core at the time of writing are limited, I feel like this solution is as good as any, particularly once it's got a bit more meat on it's bones focused on performance and extensibility.
The source code is available on bitbucket to do with as you wish. Happy coding!