How to create a dynamic image from multiple images using ImageProcessor

Posted written by Paul Seal on July 24, 2019 Umbraco

I recently launched a new website which I built in Umbraco called moneymatchpool.com

This website gathers all of the pool money matches that have been streamed on Facebook and YouTube and makes it easy for you to be able to search by player, venue and stream company.

For this site to be a success, the posts need to be shared on Facebook as that is where my target audience is.

When posts are shared on Facebook, they are far more interesting and attractive to the end user if they have an image with them.

I want it to show the 2 player profile images, the moneymatchpool.com logo and the logo of the video streaming company.

I needed to create a media handler which will generate this image dynamically based on the picked players and picked stream company.

This post shows you how I created a handler and used ImageProcessor, by James Jackson-South, to create the image. Well, ImageFactory to be precise.

This is what the header image looks like.

1

Here's how I did it

I added a folder called handlers and in that I added a new handler item called HeaderImage.ashx

Web.config Settings

In the web.config file I added the header image path to the umbracoReservedPaths app setting:

<add key="umbracoReservedPaths" value="~/umbraco,~/install/,~/HeaderImage.ashx" />

In the handlers section of the web.config file I added the handler:

<handlers accessPolicy="Read, Write, Script, Execute">
  <remove name="HeaderImage" />
  <add name="HeaderImage" verb="GET" path="HeaderImage.ashx" type="MMP.Web.handlers.HeaderImage, MMP.Web" />
</handlers>

Api Controller

I created an api controller for getting the profile and stream company images

using System.Collections.Generic;
using Umbraco.Web.WebApi;

namespace MMP.Core.Controllers.Api
{
    public class MediaApiController : UmbracoApiController
    {
        public List<string> GetMediaPathsByMatchId(int id)
        {
            List<string> mediaPaths = new List<string>();

            var match = Umbraco.TypedContent(id);

            var typedMatch = (MMP.Core.Models.Match)match;

            var homePlayer = typedMatch.HomePlayer;

            var awayPlayer = typedMatch.AwayPlayer;

            var stream = typedMatch.Stream;

            var typedHomePlayer = (MMP.Core.Models.Profile) homePlayer;
            var typedAwayPlayer = (MMP.Core.Models.Profile) awayPlayer;
            var typedStream = (MMP.Core.Models.Stream) stream;

            var homePlayerImageUrl = typedHomePlayer.ProfileImage?.Url ?? "/media/1035/silhouette.jpg";
            var awayPlayerImageUrl = typedAwayPlayer.ProfileImage?.Url ?? "/media/1035/silhouette.jpg";
            var streamImageUrl = typedStream != null ? typedStream.ProfileImage.Url : "";

            mediaPaths.Add(homePlayerImageUrl);
            mediaPaths.Add(awayPlayerImageUrl);
            mediaPaths.Add(streamImageUrl);

            return mediaPaths;
        }

        public string GetMediaPathById(int id)
        {
            var mediaItem = Umbraco.TypedMedia(id);
            return mediaItem.Url;
        }
    }
}

Basic handler

Ok first of all, I should say that normally you would have the actual logic in a separate library, but this is a personal project and I thought it was easier to leave it in the hanlder.

This code just returns a fallback image by loading it into ImageFactory and writing it out to the stream.

using ImageProcessor;
using ImageProcessor.Imaging.Formats;
using System.IO;
using System.Web;

namespace MMP.Web.handlers
{
    public class HeaderImage : IHttpHandler
    {

        public void ProcessRequest(HttpContext context)
        {
            context.Response.ContentType = "image/png";
            GenerateFallbackImage(context);
        }

        private static void GenerateFallbackImage(HttpContext context)
        {
            var fallbackImageUrl = "~/media/moneymatchpool-logo.png";
            var fallbackImagePath = context.Server.MapPath(fallbackImageUrl);

            byte[] photoBytes = File.ReadAllBytes(fallbackImagePath);
            ISupportedImageFormat format = new PngFormat() { Quality = 70 };
            using (MemoryStream inStream = new MemoryStream(photoBytes))
            {
                using (MemoryStream outStream = new MemoryStream())
                {
                    using (ImageFactory imageFactory = new ImageFactory(preserveExifData: true))
                    {
                        imageFactory.Load(inStream)
                            .Format(format)
                            .Save(outStream);
                    }

                    outStream.WriteTo(context.Response.OutputStream);
                }
            }
        }

        public bool IsReusable
        {
            get
            {
                return false;
            }
        }
    }
}

Image Layers

I wanted to load images as layers on top of a background image, so I created this method to load an image as an ImageLayer

private static ImageLayer GetImageLayer(string imagePath, int quality, Size size, bool isCircle)
{
    var imageLayer = new ImageLayer();

    int rounding = 0;
    if (isCircle)
    {
        rounding = size.Height / 2;
    }

    byte[] photoBytes = File.ReadAllBytes(imagePath);
    ISupportedImageFormat format = new PngFormat() { Quality = quality };
    using (MemoryStream inStream = new MemoryStream(photoBytes))
    {
        using (MemoryStream outStream = new MemoryStream())
        {
            if (isCircle)
            {
                using (ImageFactory imageFactory = new ImageFactory(preserveExifData: true))
                {
                    imageFactory.Load(inStream)
                        .Format(format)
                        .Resize(size)
                        .RoundedCorners(rounding)
                        .Save(outStream);
                }
            }
            else
            {
                using (ImageFactory imageFactory = new ImageFactory(preserveExifData: true))
                {
                    imageFactory.Load(inStream)
                        .Format(format)
                        .Resize(size)
                        .Save(outStream);
                }
            }

            imageLayer.Image = Image.FromStream(outStream, true);
        }
    }

    return imageLayer;
}

Generating the custom dynamic image

I then created the code for generating the custom image with all of the layers loaded onto it.

private static void GenerateCustomImage(HttpContext context)
{
    string matchId = context.Request.QueryString["id"];
    var domainAddress = context.Request.Url.GetLeftPart(UriPartial.Authority);
    var backgroundImagePath = context.Server.MapPath("~/Media/background.png");

    var playerOneUrl = "";
    var playerTwoUrl = "";
    var streamUrl = "";
    List<string> playerUrls;

    using (var httpClient = new HttpClient())
    {
        var response =
            httpClient.GetStringAsync(domainAddress + "/Umbraco/Api/MediaApi/GetMediaPathsByMatchId/?id=" + matchId);
        playerUrls = JsonConvert.DeserializeObject<List<string>>(response.Result);
    }

    playerOneUrl = playerUrls[0];
    playerTwoUrl = playerUrls[1];
    if (playerUrls.Count > 2)
    {
        streamUrl = playerUrls[2];
    }

    var playerOneImagePath = context.Server.MapPath(playerOneUrl);
    var playerTwoImagePath = context.Server.MapPath(playerTwoUrl);
    var streamImagePath = !string.IsNullOrWhiteSpace(streamUrl) ? context.Server.MapPath(streamUrl) : "";

    var playerOne = GetImageLayer(playerOneImagePath, 70, new Size(400, 400), false);
    playerOne.Position = new Point(260, 340);

    var playerTwo = GetImageLayer(playerTwoImagePath, 70, new Size(400, 400), false);
    playerTwo.Position = new Point(1260, 340);

    ImageLayer stream = null;
    if (!string.IsNullOrWhiteSpace(streamImagePath))
    {
        stream = GetImageLayer(streamImagePath, 70, new Size(300, 300), true);
        stream.Position = new Point(810, 240);
    }

    byte[] photoBytes = File.ReadAllBytes(backgroundImagePath);
    ISupportedImageFormat format = new PngFormat() { Quality = 70 };
    using (MemoryStream inStream = new MemoryStream(photoBytes))
    {
        using (MemoryStream outStream = new MemoryStream())
        {
            using (ImageFactory imageFactory = new ImageFactory(preserveExifData: true))
            {
                imageFactory.Load(inStream)
                    .Format(format)
                    .Overlay(playerOne)
                    .Overlay(playerTwo)
                    .Overlay(stream)
                    .Save(outStream);
            }

            outStream.WriteTo(context.Response.OutputStream);
        }
    }
}

Update the process request method

public void ProcessRequest(HttpContext context)
{
    context.Response.ContentType = "image/png";

    string matchId = context.Request.QueryString["id"];

    if (string.IsNullOrWhiteSpace(matchId))
    {
        GenerateFallbackImage(context);
    }
    else
    {
        try
        {
            GenerateCustomImage(context);
        }
        catch (Exception ex)
        {
            var error = ex;
            GenerateFallbackImage(context);
        }
    }
}

Here is the final handler code

using ImageProcessor;
using ImageProcessor.Imaging;
using ImageProcessor.Imaging.Formats;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Net.Http;
using System.Web;

namespace MMP.Web.handlers
{
    public class HeaderImage : IHttpHandler
    {

        public void ProcessRequest(HttpContext context)
        {
            context.Response.ContentType = "image/png";

            string matchId = context.Request.QueryString["id"];

            if (string.IsNullOrWhiteSpace(matchId))
            {
                GenerateFallbackImage(context);
            }
            else
            {
                try
                {
                    GenerateCustomImage(context);
                }
                catch (Exception ex)
                {
                    var error = ex;
                    GenerateFallbackImage(context);
                }
            }
        }

        private static void GenerateFallbackImage(HttpContext context)
        {
            var fallbackImageUrl = "~/media/moneymatchpool-logo.png";
            var fallbackImagePath = context.Server.MapPath(fallbackImageUrl);

            byte[] photoBytes = File.ReadAllBytes(fallbackImagePath);
            ISupportedImageFormat format = new PngFormat() { Quality = 70 };
            using (MemoryStream inStream = new MemoryStream(photoBytes))
            {
                using (MemoryStream outStream = new MemoryStream())
                {
                    using (ImageFactory imageFactory = new ImageFactory(preserveExifData: true))
                    {
                        imageFactory.Load(inStream)
                            .Format(format)
                            .Save(outStream);
                    }

                    outStream.WriteTo(context.Response.OutputStream);
                }
            }
        }

        private static void GenerateCustomImage(HttpContext context)
        {
            string matchId = context.Request.QueryString["id"];
            var domainAddress = context.Request.Url.GetLeftPart(UriPartial.Authority);
            var backgroundImagePath = context.Server.MapPath("~/Media/background.png");

            var playerOneUrl = "";
            var playerTwoUrl = "";
            var streamUrl = "";
            List<string> playerUrls;

            using (var httpClient = new HttpClient())
            {
                var response =
                    httpClient.GetStringAsync(domainAddress + "/Umbraco/Api/MediaApi/GetMediaPathsByMatchId/?id=" + matchId);
                playerUrls = JsonConvert.DeserializeObject<List<string>>(response.Result);
            }

            playerOneUrl = playerUrls[0];
            playerTwoUrl = playerUrls[1];
            if (playerUrls.Count > 2)
            {
                streamUrl = playerUrls[2];
            }

            var playerOneImagePath = context.Server.MapPath(playerOneUrl);
            var playerTwoImagePath = context.Server.MapPath(playerTwoUrl);
            var streamImagePath = !string.IsNullOrWhiteSpace(streamUrl) ? context.Server.MapPath(streamUrl) : "";

            var playerOne = GetImageLayer(playerOneImagePath, 70, new Size(400, 400), false);
            playerOne.Position = new Point(260, 340);

            var playerTwo = GetImageLayer(playerTwoImagePath, 70, new Size(400, 400), false);
            playerTwo.Position = new Point(1260, 340);

            ImageLayer stream = null;
            if (!string.IsNullOrWhiteSpace(streamImagePath))
            {
                stream = GetImageLayer(streamImagePath, 70, new Size(300, 300), true);
                stream.Position = new Point(810, 240);
            }

            byte[] photoBytes = File.ReadAllBytes(backgroundImagePath);
            ISupportedImageFormat format = new PngFormat() { Quality = 70 };
            using (MemoryStream inStream = new MemoryStream(photoBytes))
            {
                using (MemoryStream outStream = new MemoryStream())
                {
                    using (ImageFactory imageFactory = new ImageFactory(preserveExifData: true))
                    {
                        imageFactory.Load(inStream)
                            .Format(format)
                            .Overlay(playerOne)
                            .Overlay(playerTwo)
                            .Overlay(stream)
                            .Save(outStream);
                    }

                    outStream.WriteTo(context.Response.OutputStream);
                }
            }
        }

        private static ImageLayer GetImageLayer(string imagePath, int quality, Size size, bool isCircle)
        {
            var imageLayer = new ImageLayer();

            int rounding = 0;
            if (isCircle)
            {
                rounding = size.Height / 2;
            }

            byte[] photoBytes = File.ReadAllBytes(imagePath);
            ISupportedImageFormat format = new PngFormat() { Quality = quality };
            using (MemoryStream inStream = new MemoryStream(photoBytes))
            {
                using (MemoryStream outStream = new MemoryStream())
                {
                    if (isCircle)
                    {
                        using (ImageFactory imageFactory = new ImageFactory(preserveExifData: true))
                        {
                            imageFactory.Load(inStream)
                                .Format(format)
                                .Resize(size)
                                .RoundedCorners(rounding)
                                .Save(outStream);
                        }
                    }
                    else
                    {
                        using (ImageFactory imageFactory = new ImageFactory(preserveExifData: true))
                        {
                            imageFactory.Load(inStream)
                                .Format(format)
                                .Resize(size)
                                .Save(outStream);
                        }
                    }

                    imageLayer.Image = Image.FromStream(outStream, true);
                }
            }

            return imageLayer;
        }

        public bool IsReusable
        {
            get
            {
                return false;
            }
        }
    }
}