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 IFileProviders:

  • 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.

Content root vs web root

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.