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.

enter image description here

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;
            }
        }
    }
}

Paul Seal

Umbraco MVP and .NET Web Developer from Derby (UK) who specialises in building Content Management System (CMS) websites using MVC with Umbraco as a framework. Paul is passionate about web development and programming as a whole. Apart from when he's with his wife and son, if he's not writing code, he's thinking about it or listening to a podcast about it.

Proudly sponsored by

Moriyama

  • Moriyama build, support and deploy Umbraco, Azure and ASP.NET websites and applications.
AppVeyor

  • CI/CD service for Windows, Linux and macOS
  • Build, test, deploy your apps faster, on any platform.
elmah.io

  • elmah.io is the easy error logging and uptime monitoring service for .NET.
  • Take back control of your errors with support for all .NET web and logging frameworks.
uSync Complete

  • uSync.Complete gives you all the uSync packages, allowing you to completely control how your Umbraco settings, content and media is stored, transferred and managed across all your Umbraco Installations.
uSkinned

  • More than a theme for Umbraco CMS, take full control of your content and design with a feature-rich, award-nominated & content editor focused website platform.
UmbHost

  • Affordable, Geo-Redundant, Umbraco hosting which gives back to the community by sponsoring an Umbraco Open Source Developer with each hosting package sold.