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;
}
}
}
~/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>();
}
}
}
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)
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...
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 🙂
10 seconds is a lot for that. Try resizing the image to 32x32 first.
Indeed, a colleague recommended the same, so cropping first then running it.