I recently added a CSS background color to my cover images, using the dominant color from the cover image itself. It is a nice effect, and gives the appearance of faster load time — should the image take some time to complete.

For my transparent cover images; I made the background very opaque, providing a soft touch of color. I think it looks nice 🙂

Here is how I did it. 👇

Table of contents

Getting the colors

First I needed to get the dominant color for each image. And it turns out that isn’t as clear-cut as I thought. What is the definition of dominant color? Is it the most vibrant? Most eye-catching? Most used?

This post on Super User got me started.

I first tried with an Imagemagick histogram:

convert image.jpg -format %c -depth 8 histogram:info:histogram_image.txt
sort -n histogram_image.txt | tail -1

This worked well for some images, not for others. Using only the frequency of pixel colors returned lot of black and dark colors. As it is stated in the Super User answer:

[…] Frequency of specific colour pixels might not correspond to a human perception of predominant color of course. […]

Also mentioned in that answer was dcolor, which is what I decided to use.

I made a simple Python script — get the dominant color of all my cover images, and store it in a json file in my data folder:

import os, glob, json, subprocess

def get_color(filename):
    result = subprocess.run(["dcolors", "-k1", "-r100x100", filename],
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT)
    return result.stdout.decode("utf-8").rstrip()

colors = {}

with open('colors.json') as json_file:
    colors = json.load(json_file)

files_grabbed = [sorted(glob.glob(e, recursive=True)) for e in [
    '/home/hebron/dev/cavelab-blog/content/**/cover.jpg',
    '/home/hebron/dev/cavelab-blog/content/**/cover.png'
]]

for files in files_grabbed:
    for img in files:
        imgKey = img.replace('/home/hebron/dev/cavelab-blog/content/', '')
        if imgKey in colors:
            continue
        colors[imgKey] = get_color(img)
        print('{}: {}'.format(colors[imgKey], imgKey))

with open('colors.json', 'w') as outfile:
    json.dump(colors, outfile, indent=4)

For each image, it stores a single color in rgb — but only if the path is not already defined. The resulting json file looks like this:

{
    "posts/2021/zfs-ssd-pool-problems/cover.jpg": "32,30,31",
    "posts/2021/zfs-on-arch-linux-no-luck/cover.png": "2,23,44"
}

Implement in Hugo

Next I just needed to add a bit of code to my post-cover.html partial:

{{- $page := . -}}
{{- $src := $page.Resources.GetMatch "cover.*" -}}

{{/* Image processing happens here */}}

{{- $bgColors := "" -}}
{{- with $page.Site.Data.colors -}}
  {{- $imgPath := path.Join (path.Dir $page.Page.File.Path) $src.Name -}}
  {{- with (index . $imgPath) -}}
    {{- if eq $src.MediaType.SubType "png" -}}
      {{- $bgColors = printf "background-color:rgba(%s,0.1);" . -}}
    {{- else -}}
      {{- $bgColors = printf "background-color:rgb(%s);" . -}}
    {{- end -}}
  {{- end -}}
{{- end -}}

<figure class="post-cover" {{ if gt (len $bgColors) 0 -}} style="{{ $bgColors | safeCSS }}" {{- end }}>
  {{/* Picture element */}}
</figure>

This the logic (in simple terms):

  • If a key with the relative path to the image exists in colors.json
  • If cover image is png: add background color with 0.1 opacity
  • If cover image is not png: add solid background color
  • If the $bgColors variable is not empty, insert it as an element style on the post-cover figure

The result

The individual background colors make for a more pleasant browsing experience when on a slow connection — or, at least I think so 🙂

This video shows browsing on a landscape phone, on a “regular” 3G internet connection (according to Firefox):

Simulated browsing with a landscape phone, on a slow internet connection

And here is a screenshot showing the cover image for this page, with an opaque background (with light theme):

Cover image with opaque background, in dominant color

Having the color codes; it is easy to extend this for figures, gallery images, etc. I might do that in the future 🥳 🖖