In my last post, I showed a way to crop and resize an image downloaded from a URL, using the ImageSharp library. This was in response to a post in which Dmitry Sikorsky used the CoreCompat.System.Drawing library to achieve the same thing. The purpose of that post was simply to compare the two libraries from an API perspective, and to demonstrate ImageSharp in the application.
The code in that post was not meant to be used in production, and contained a number of issues such as not disposing objects correctly, creating a fresh HttpClient
for every request, and happily downloading files from any old URL!
In this post I'll show a few tweaks to make the code from the last post a little more production worthy. In particular, rather than downloading a file from any URL provided in the querystring, we'll load the file from the web root folder on disk, if the file exists.
Add the NuGet.config file
As mentioned in my previous post, ImageSharp is currently only published on MyGet, not NuGet, so you'll need to add a NuGet.config file to ensure you can restore the ImageSharp library.
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="ImageSharp Nightly" value="https://www.myget.org/F/imagesharp/api/v3/index.json" />
</packageSources>
</configuration>
Once you've added the config file, you can add the ImageSharp library to the project.
Using a catch-all route parameter
The first step I wanted to take was to move from passing the URL to the target image as a querystring parameter to part of the URL path. As a part of this, we'll switch from allowing URLs to be absolute paths to relative paths.
Previously, you would pass the URL to resize something like the following:
/image?url=https://assets-cdn.github.com/images/modules/logos_page/GitHub-Mark.png?width=200&height=100
Instead, our updated route will look something like the following, where the path to the image to resize is images/clouds.jpg
:
/resize/200/100/images/clouds.jpg
All that's required is to introduce a [Route]
attribute with a appropriate parameters for the dimensions and a catch-all parameter, for example:
[Route("/resize/width/height/{*url}")]
public IActionResult ResizeImage(string url, int width, int height)
{
/* Method implementation */
}
This gives us urls that are easier to read and parse around (plus will give us another benefit, as you'll see in the next post.
Validating the requested file exists
Before we try and load the file from disk, we first need to make sure that a valid file has been requested. To do so, we'll use the FileInfo
class and the IFileProvider
interface.
public class HomeController : Controller
{
private readonly IFileProvider _fileProvider;
[Route("/image/{width}/{height}/{*url}")]
public IActionResult ResizeImage(string url, int width, int height)
{
if (width <= 0 || height <= 0)
{
return BadRequest();
}
var imagePath = PathString.FromUriComponent("/" + url);
var fileInfo = _fileProvider.GetFileInfo(imagePath);
if (!fileInfo.Exists) { return NotFound(); }
/* Load image, resize and return */
}
}
First, we perform some simple parameter validation to make sure the requested dimenstions aren't less that zero, and if that fails, we return a 400
result.
I'm going to treat the value 0 as a special case for now - if you pass zero in either width or height then we'll ignore that value, and use the original image's dimension.
Assuming the width and height are valid, we try and get the FileInfo
using the injected IFileProvider
, and if it deems the file doesn't exist, we return a 404.
So the first question is, where does the implementation of IFileProvider
come from?
WebRootFileProvider
vs ContentRootFileProvider
The IHostingEnvironment
exposes two IFileProvider
s:
ContentRootFileProvider
WebRootFileProvider
These file providers allow serving files from the ContentRootPath
and WebRootPath
respectively. By default, the ContentRootPath
points to the root of the project folder, while WebRootPath
points to the wwwroot
folder.
For this example, we only want to serve files from the wwwroot
folder - serving files from anywhere else would be a security risk - so we use the WebRootFileProvider
property, by accessing it from an IHostingEnvironment
injected into the consturctor:
public HomeController(IHostingEnvironment env)
{
_fileProvider = env.WebRootFileProvider;
}
Resizing the image
Once we have validated the file exists, we can continue with the rest of the action method. This part is very similar to the previous post, just tweaked a little using suggestions from James South. We use the FileInfo
object to obtain a Stream
for the file we want to reload, load it into memory.
Once we have loaded the image, we can resize it. For this example, we'll just use the values provided in the URL, and we'll always save the image as a jpeg, so we can use the SaveAsJpeg
extension method:
[Route("/image/{width}/{height}/{*url}")]
public IActionResult ResizeImage(string url, int width, int height)
{
if (width < 0 || height < 0 ) { return BadRequest(); }
var imagePath = PathString.FromUriComponent("/" + url);
var fileInfo = _fileProvider.GetFileInfo(imagePath);
if (!fileInfo.Exists) { return NotFound(); }
var outputStream = new MemoryStream();
using (var inputStream = fileInfo.CreateReadStream())
using (var image = Image.Load(inputStream))
{
image
.Resize(widthToUse, heightToUse)
.SaveAsJpeg(outputStream);
}
outputStream.Seek(0, SeekOrigin.Begin);
return File(outputStream, "image/jpg");
}
Note, if you pass 0 for either width or height, by default ImageSharp will preserve the original aspect ratio when resizing.
With this revised action method, we have an action closer to something we'd actually use in practice.
There's still some aspects that we would likely want to improve before we used in production. In particular, we would likely want some sort of caching of the final output, so we are not doing an expensive resize operation with every request. I'll look at fixing this in a follow up post.
Summary
This post showed a revised version of the "crop and resize" action method from my previous post. In this post, I stopped loading the image with HttpClient
and instead required that it already be located in the web app in the wwwroot
folder. The file was loaded using the IHostingEnvironment.WebRootFileProvider
property, and finally resized in a more fluent way, and ensuring we dispose the underlying arrays.