Image Mutation Pipeline - Reshaping and Overlay Images
This post builds an image processing pipeline that turns a picture of a user or character into an avatar, then surrounds it with a frame as an overlay.
Requirements
Instead of System.Drawing, this uses ImageSharp, the most
popular image processing library for .NET Core. It's fast and highly
optimised, and although it's in beta, I've used it in production for a
year without problems.
The pipeline is a chain of operations on
IImageProcessingContext, letting ImageSharp do the heavy
lifting.
There are two assets, a base picture and an overlay to surround it:
Project setup
- Create a .NET Core console app.
- Add this package reference to your
.csprojfile:<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta0007" />. If you install through NuGet, make sure it's the.Drawingpackage, or you won't have theSixLabors.Shapesnamespace. - Add the assets to your project, and set Copy to Output Directory to Copy if newer in the Properties panel.
Code
The first step reshapes the user image into something like a diamond. The comments in the code explain each step, which is easier to follow than annotations outside it:
static void Main(string[] args)
{
// directory to output results
System.IO.Directory.CreateDirectory("output");
// wrap operations in using statements to ensure that Dispose is called
using (var img = Image.Load("me.png"))
{
// clone the original image, since we need this for future runs of the application
using (Image destRound = img.Clone(x => x.AvatarShaping(new Size(225, 225), 150)))
{
destRound.Save("output/result.png");
}
}
}
/// <summary>
/// Here's we're implementing the IImageProcessingContext interface and extending it via an extension method,
/// This implements a full image mutating pipeline operating on IImageProcessingContext
/// </summary>
/// <param name="processingContext">the processingContext</param>
/// <param name="size">Size of the image</param>
/// <param name="cornerRadius">corner radius desired, higher will result in a smaller fill</param>
/// <returns></returns>
private static IImageProcessingContext AvatarShaping(this IImageProcessingContext processingContext, Size size, float cornerRadius)
{
return processingContext.Resize(new ResizeOptions
{
Size = size,
Mode = ResizeMode.Crop
}).ApplyRoundedCorners(cornerRadius); // the next part of our pipeline
}
// This method can be seen as an inline implementation of an `IImageProcessor`:
// (The combination of `IImageOperations.Apply()` + this could be replaced with an `IImageProcessor`)
private static IImageProcessingContext ApplyRoundedCorners(this IImageProcessingContext ctx, float cornerRadius)
{
Size size = ctx.GetCurrentSize();
IPathCollection corners = BuildCorners(size.Width, size.Height, cornerRadius);
var graphicOptions = new GraphicsOptions(true)
{
// AlphaCompositionMode = PixelAlphaCompositionMode.DestOut // enforces that any part of this shape that has color is punched out of the background
};
// mutating in here as we already have a cloned original
// use any color (not Transparent), so the corners will be clipped
return ctx.Fill(graphicOptions, Rgba32.LimeGreen, corners);
}
private static IPathCollection BuildCorners(int imageWidth, int imageHeight, float cornerRadius)
{
// first create a square
var rect = new RectangularPolygon(-0.5f, -0.5f, cornerRadius, cornerRadius);
// then cut out of the square a circle so we are left with a corner
// Changing the offsets will change how much of the shape is cut out
IPath cornerTopLeft = rect.Clip(new EllipsePolygon(cornerRadius - 0.5f, cornerRadius - 0.5f, cornerRadius));
// corner is now a corner shape positions top left
//lets make 3 more positioned correctly, we can do that by translating the original around the center of the image
float rightPos = imageWidth - cornerTopLeft.Bounds.Width + 1;
float bottomPos = imageHeight - cornerTopLeft.Bounds.Height + 1;
// move it across the width of the image - the width of the shape
// cut the remaining 3 corners
IPath cornerTopRight = cornerTopLeft.RotateDegree(90).Translate(rightPos, 0);
IPath cornerBottomLeft = cornerTopLeft.RotateDegree(-90).Translate(0, bottomPos);
IPath cornerBottomRight = cornerTopLeft.RotateDegree(180).Translate(rightPos, bottomPos);
return new PathCollection(cornerTopLeft, cornerBottomLeft, cornerTopRight, cornerBottomRight);
}That produces this image:

Increasing cornerRadius gets closer to a diamond, but
shows less of the picture. Here it is at 200:

For a circle, set cornerRadius to 50% of the image
size.
The clipped areas are currently filled with solid green, which is useful for testing but not the result we want. Uncomment this line to punch them out instead:
AlphaCompositionMode = PixelAlphaCompositionMode.DestOut
Next comes the overlay, which is simpler. It goes in another nested
using statement, so the earlier output is still saved. That
makes it easier to see each stage and make changes while building the
pipeline.
Add this inside the destRound using, after
destRound.Save("output/result.png");:
using (var overlay = Image.Load("frame.png"))
{
using (Image overlayClone = destRound.Clone(x => x.GenerateOverlay(overlay)))
{
overlayClone.Save("output/complete.png", new PngEncoder()
{
ColorType = PngColorType.RgbWithAlpha // needed to instruct the encoder transparency is required
});
}
}GenerateOverlay is the next step in the pipeline:
private static IImageProcessingContext GenerateOverlay(this IImageProcessingContext processingContext, Image overlay)
{
return processingContext.DrawImage(overlay, new Point(0, 0), new GraphicsOptions());
}It draws the overlay on top of the reshaped image, and the result is saved to disk.
The final result

This is a proof of concept. The Clone calls, for
example, add overhead and don't belong in production code.