DEV Community

Jesper Mayntzhusen
Jesper Mayntzhusen

Posted on

Set page color based on an img in Umbraco

The problem

Currently working on a new website that shows a product catalogue. Part of the design is that each page has a background color that is based on the main product image. Here is how I've solved it.

The code

We will calculate the avg color of an image when it is added to one of the product page nodes - so in the content saving event:

~/Events/ContentServiceEvents.cs

using System;
using System.Drawing;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Services.Implement;
using Umbraco.Web;

namespace Test.Events {
    public class ContentServiceEvents : IComponent {
        private readonly IUmbracoContextFactory _umbracoContextFactory;

        public ContentServiceEvents(IUmbracoContextFactory umbracoContextFactory) {
            _umbracoContextFactory = umbracoContextFactory;
        }

        public void Initialize() {
            ContentService.Saving += ContentService_Saving;
        }

        public void Terminate() {
            ContentService.Saving -= ContentService_Saving;
        }

        private void ContentService_Saving(Umbraco.Core.Services.IContentService sender, Umbraco.Core.Events.ContentSavingEventArgs e) {
            // When content is saved, we loop through each saved node
            foreach (IContent node in e.SavedEntities) {

                // We only continue if it's a product page
                if (node.ContentType.Alias == "productPage") {

                    // We only continue if the saved node has the product image set
                    if (node.GetValue("productImage") != null) {

                        // We only continue if the color isn't already set
                        if (string.IsNullOrWhiteSpace(node.GetValue<string>("imageBgColor"))) {
                            using (var context = _umbracoContextFactory.EnsureUmbracoContext()) {

                                // MediaPicker v2 saves as a media udi, so we get it to grab the media object
                                var udi = node.GetValue<Udi>("productImage");
                                var img = context.UmbracoContext.Media.GetById(udi);

                                // From https://stackoverflow.com/a/20757431/16493772
                                // This is for external media storage like Azure blob storage or AWS S3 buckets
                                // We download the image based on the media url to create a bitmap we use to calculate the avg color
                                System.Net.WebRequest request = System.Net.WebRequest.Create(img.Url(mode: UrlMode.Absolute));
                                System.Net.WebResponse response = request.GetResponse();
                                System.IO.Stream responseStream = response.GetResponseStream();
                                Bitmap bm = new Bitmap(responseStream);
                                var color = AverageColor(bm);

                                // Finally we set the avg color in a property - since this is a content saving event
                                // we don't need to save exeplicitly, it'll do that at the end
                                node.SetValue("imageBgColor", color);
                            }
                        }
                    }                                
                }
            }
        }

        // From https://stackoverflow.com/a/2546080/16493772 - Edited to get around a bug where the values would overflow the max Int32 value and become negative,
        // changed ints to longs then converting back to int in the end.
        // In short - it goes through every pixel of the img, finds its RGBA value, add them all together and divide by the number of pixels to find the average color of the image.
        public string AverageColor(Bitmap bmp) {            
            long width = bmp.Width;
            long height = bmp.Height;
            long red = 0;
            long green = 0;
            long blue = 0;
            long alpha = 0;
            for (int x = 0; x < width; x++)
                for (int y = 0; y < height; y++) {
                    var pixel = bmp.GetPixel(x, y);
                    red += pixel.R;
                    green += pixel.G;
                    blue += pixel.B;
                    alpha += pixel.A;
                }

            Func<long, long> avg = c => c / (width * height);

            red = avg(red);
            green = avg(green);
            blue = avg(blue);
            alpha = avg(alpha);

            var color = Color.FromArgb(Convert.ToInt32(alpha), Convert.ToInt32(red), Convert.ToInt32(green), Convert.ToInt32(blue));

            var hexColor = "#" + color.R.ToString("X2") + color.G.ToString("X2") + color.B.ToString("X2");

            return hexColor;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

~/Composers/SiteComposer.cs

using Test.Events;
using Umbraco.Core;
using Umbraco.Core.Composing;

namespace Test.Composers {

    [RuntimeLevel(MinLevel = RuntimeLevel.Run)]
    public class SiteComposer : IUserComposer {

        public void Compose(Composition composition) {            
            composition.Components().Append<ContentServiceEvents>();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Let me know if you found this interesting, or if you know of any improvements I could make!

Checking each pixel of the image is not super performant, so there may be better ways to get the color. For 8mb images this can take ~10 seconds on the first save on my current site.

Top comments (4)

Collapse
 
jannikanker profile image
Jannik Anker

Cool idea! Butโ€ฆ Wouldnโ€™t it make more sense to find the dominant color instead of the average? AFAIK digital cameras on auto exposure settings will expose for an average color of 50% grey (I think - itโ€™s some percentage of grey for sure). Google suggests something like this: stackoverflow.com/questions/301034...

Collapse
 
jemayn profile image
Jesper Mayntzhusen

For my specific use case the images were quite uniform - it was fabric for furniture, so it worked perfectly fine. You may be right that there are better ways for more varied images ๐Ÿ™‚

Collapse
 
ctimmerman profile image
Cees Timmerman

10 seconds is a lot for that. Try resizing the image to 32x32 first.

Collapse
 
jemayn profile image
Jesper Mayntzhusen

Indeed, a colleague recommended the same, so cropping first then running it.