blog post image
Andrew Lock avatar

Andrew Lock

~8 min read

Using ImageSharp to resize images in ASP.NET Core - a comparison with CoreCompat.System.Drawing

Currently, one of the significant missing features in .NET Core and .NET Standard are the System.Drawing APIs that you can use, among other things, for server-side image processing in ASP.NET Core. Bertrand Le Roy gave a great run down of the various alternatives available in Jan 2017, each with different pros and cons.

I was reading a post by Dmitry Sikorsky yesterday describing how to use one of these libraries, the CoreCompat.System.Drawing package, to resize an image in ASP.NET Core. This package is designed to mimic the existing System.Drawing APIs (it's a .NET Core port, of the Mono port, of System.Drawing!) so if you need a drop in replacement for System.Drawing then it's a good place to start.

I'm going to need to start doing some image processing soon, so I wanted to take a look at how the code for working with CoreCompat.System.Drawing would compare to using the ImageSharp package. This is a brand new library that is designed from the ground up to be cross-platform by using only managed-code. This means it will probably not be as performant as libraries that use OS-specific features, but on the plus side, it is completely cross platform.

For the purposes of this comparison, I'm going to start with the code presented by Dmitry in his post and convert it to use ImageSharp.

The sample app

This post will as based on the code from Dimitri's post, so it uses the same sample app. This contains a single controller, the ImageController, which you can use to crop and resize an image from a given URL.

For example, a request might look like the following:

/image?url=https://assets-cdn.github.com/images/modules/logos_page/GitHub-Mark.png&sourcex=120&sourcey=100&sourcewidth=360&sourceheight=360&destinationwidth=100&destinationheight=100

This will downloaded the GitHub logo from https://assets-cdn.github.com/images/modules/logos_page/GitHub-Mark.png:

Github logo

It will then crop it using the rectangle specified by sourcex=120&sourcey=100&sourcewidth=360&sourceheight=360, and resize the output to 100×100. Finally, it will render the result in the response as a jpeg.

Screenshot of app

This is the same functionality Dimitri described, I will just convert his code to use ImageSharp instead.

Installing ImageSharp

The first step is to add the ImageSharp package to your project. Currently, this is not quite as smooth as it will be, as it is not yet published on Nuget, but instead just to a MyGet feed. This is only a temporary situation while the code-base stabilises - it will be published to NuGet at that point - but at the moment it is a bit of a barrier to adding it to your project.

Note, ImageSharp actually is published on NuGet, but that package is currently just a placeholder for when the package is eventually published. Don't use it!

To install the package from the MyGet feed, add a NuGet.config file to your solution folder, specifying the location of the feed:

<?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>

You can now add the ImageSharp package to your csproj file, and run a restore. I specified the version 1.0.0-* to fetch the latest version from the feed (1.0.0-alpha7 in my case).

<PackageReference Include="ImageSharp" Version="1.0.0-*" />

When you run dotnet restore you should see that the CLI has used the ImageSharp MyGet feed, where it lists the config files used:

$ dotnet restore
  Restoring packages for C:\Users\Sock\Repos\andrewlock\AspNetCoreImageResizingService\AspNetCoreImageResizingService\AspNetCoreImageResizingService.csproj...
  Installing ImageSharp 1.0.0-alpha7-00006.
  Generating MSBuild file C:\Users\Sock\Repos\andrewlock\AspNetCoreImageResizingService\AspNetCoreImageResizingService\obj\AspNetCoreImageResizingService.csproj.nuget.g.props.
  Writing lock file to disk. Path: C:\Users\Sock\Repos\andrewlock\AspNetCoreImageResizingService\AspNetCoreImageResizingService\obj\project.assets.json
  Restore completed in 2.76 sec for C:\Users\Sock\Repos\andrewlock\AspNetCoreImageResizingService\AspNetCoreImageResizingService\AspNetCoreImageResizingService.csproj.

  NuGet Config files used:
      C:\Users\Sock\Repos\andrewlock\AspNetCoreImageResizingService\NuGet.Config
      C:\Users\Sock\AppData\Roaming\NuGet\NuGet.Config
      C:\Program Files (x86)\NuGet\Config\Microsoft.VisualStudio.Offline.config

  Feeds used:
      https://www.myget.org/F/imagesharp/api/v3/index.json
      https://api.nuget.org/v3/index.json
      C:\Program Files (x86)\Microsoft SDKs\NuGetPackages\

  Installed:
      1 package(s) to C:\Users\Sock\Repos\andrewlock\AspNetCoreImageResizingService\AspNetCoreImageResizingService\AspNetCoreImageResizingService.csproj

Adding the NuGet.config file is a bit of a pain, but it's a step that will hopefully go away soon, when the package makes its way onto NuGet.org. On the plus side, you only need to add a single package to your project for this example.

In contrast, to add the CoreCompat.System.Drawing packages you have to include three different packages when writing cross-platform code - the library itself and the run time components for both Linux and OS X:

<PackageReference Include="CoreCompat.System.Drawing" Version="1.0.0-beta006" />
<PackageReference Include="runtime.linux-x64.CoreCompat.System.Drawing" Version="1.0.0-beta009" />
<PackageReference Include="runtime.osx.10.10-x64.CoreCompat.System.Drawing" Version="1.0.1-beta004" />

Obviously, if you are running on only a single platform, then this probably won't be an issue for you, but it's something to take into consideration.

Loading an image from a stream

Now the library is installed, we can start converting the code. The first step in the app is to download the image provided in the URL.

Note that this code is very much sample only - downloading files sent to you in query arguments is probably not advisable, plus you should probably be using a static HttpClient, disposing correctly etc!

For the CoreCompat.System.Drawing library, the code doing the work reads the stream into a Bitmap, which is then set to the Image object.

Image image = null;
HttpClient httpClient = new HttpClient();
HttpResponseMessage response = await httpClient.GetAsync(url);
Stream inputStream = await response.Content.ReadAsStreamAsync();

using (Bitmap temp = new Bitmap(inputStream))
{
    image = new Bitmap(temp);
}

While for ImageSharp we have the following:

Image image = null;
HttpClient httpClient = new HttpClient();
HttpResponseMessage response = await httpClient.GetAsync(url);
Stream inputStream = await response.Content.ReadAsStreamAsync();

image = Image.Load(inputStream);

Obviously the HttpClient code is identical here, but there is less faffing required to actually read an image from the response stream. The ImageSharp API is much more intuitive - I have to admit I always have to refresh my memory on how the System.Drawing Imageand Bitmap classes interact! Definitely a win to ImageSharp I think.

It's worth noting that the Image classes in these two examples are completely different types, in different namespaces, so are not interoperable in general.

Cropping and Resizing an image

Once the image is in memory, the next step is to crop and resize it to create our output image. The CropImage function for the CoreCompat.System.Drawing is as follows:

private Image CropImage(Image sourceImage, int sourceX, int sourceY, int sourceWidth, int sourceHeight, int destinationWidth, int destinationHeight)
{
  Image destinationImage = new Bitmap(destinationWidth, destinationHeight);
  Graphics g = Graphics.FromImage(destinationImage);

  g.DrawImage(
    sourceImage,
    new Rectangle(0, 0, destinationWidth, destinationHeight),
    new Rectangle(sourceX, sourceY, sourceWidth, sourceHeight),
    GraphicsUnit.Pixel
  );

  return destinationImage;
}

This code creates the destination image first, generates a Graphics object to allow manipulating the content, and then draws a region from the first image onto the second, resizing as it does so.

This does the job, but it's not exactly simple to follow - if I hadn't told you, would you have spotted that the image is being resized as well as cropped? Maybe, given we set the destinationImage size, but possibly not if you were just looking at the DrawImage function.

In contrast, the ImageSharp version of this method would look something like the following:

private Image<Rgba32> CropImage(Image sourceImage, int sourceX, int sourceY, int sourceWidth, int sourceHeight, int destinationWidth, int destinationHeight)
{
    return sourceImage
        .Crop(new Rectangle(sourceX, sourceY, sourceWidth, sourceHeight))
        .Resize(destinationWidth, destinationHeight);
}

I think you'd agree, this is much easier to understand! Instead of using a mapping from one coordinate system to another, handling both the crop and resize in one operation, it has two well-named methods that are easy to understand.

One slight quirk in the ImageSharp version is that this method returns an Image<Rgba32> when we gave it an Image. The definition for this Image object is:

public sealed class Image : Image<Rgba32> { }

so the Image is-an Image<Rgba32>. This isn't a big issue, I guess it would just be nice if you were working with the Image class to get back an Image from the manipulation functions. I still count this as a win for ImageSharp.

Saving the image to a stream

The final part of the app is to save the cropped image to the response stream and return it to the browser.

The CoreCompat.System.Drawing version of saving the image to a stream looks like the following. We first download the image, crop it and then save it to a MemoryStream. This stream can then be used to create a FileResponse object in the browser (check the example source code or Dimitri's post for details.

Image sourceImage = await this.LoadImageFromUrl(url);

Image destinationImage = this.CropImage(sourceImage, sourceX, sourceY, sourceWidth, sourceHeight, destinationWidth, destinationHeight);
Stream outputStream = new MemoryStream();

destinationImage.Save(outputStream, ImageFormat.Jpeg);

The ImageSharp equivalent is very similar. It just involves changing the type of the destination image to be Image<Rgba32> (as mentioned in the previous section), and updating the last line, in which we save the image to a stream.

Image sourceImage = await this.LoadImageFromUrl(url);

Image<Rgba32> destinationImage = this.CropImage(sourceImage, sourceX, sourceY, sourceWidth, sourceHeight, destinationWidth, destinationHeight);
Stream outputStream = new MemoryStream();

destinationImage.Save(outputStream, new JpegEncoder());

Instead of using an Enum to specify the output formatting, you pass an instance of an IImageEncoder, in this case the JpegEncoder. This approach is more extensible, though it is slightly less discoverable then the System.Drawing approach.

Note, there are many different overloads to Image<T>.Save() that you can use to specify all sorts of different encoding options etc.

Wrapping up

And that's it. Everything you need to convert from CoreCompat.System.Drawing to ImageSharp. Personally, I really like how ImageSharp is shaping up - it has a nice API, is fully managed cross-platform and even targets .NET Standard 1.1 - no mean feat! It may not currently hit the performance of other libraries that rely on native code, but with all the improvements and progress around Spans<T>, it may be able to come close to parity down the line.

If you're interested in the project, do check it out on GitHub and consider contributing - it will be great to get the project to an RTM state.

Thanks are due to James South for creating the ImageSharp project, and also to Dmitry Sikorsky for inspiring me to write this post! You can find the source code for his project on GitHub here, and the source for my version here.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?