Identicons and visual hashing

How can we create graphical icons that are unique to each person? This post illustrates the process by using HTML canvas to generate a visual hash, also known as an "identicon".

Introduction

These days, it feels like every other website is asking for a profile image. However, not many people (me included) will go out of their way to actually provide one, and consequently, these websites must fill their user interface with boring blank icons.

Plain default icons

Several websites have solved this problem with default icons that are unique to each user. Examples are:

  • Github

Github gravatars

  • Stack Overflow (Gravatars)

Stackoverflow gravatars

Identicons

These graphics are called identicons. Wikipedia defines an identicon as

A visual representation of a hash value, usually of an IP address, that serves to identify a user of a computer system as a form of avatar while protecting the users’ privacy.

But how are these actually created? What exactly is a “visual representation” of a hash value? It’s actually surprisingly simple. This post will guide you through the process of making one, and will allow you to design your own unique unique-icon generator (meta!).

Note that while the code provided uses HTML canvas, the post aims to explain general concepts that are applicable in any framework.

Hashing

A hash function maps an input value to a fixed-size output. For example, let’s say we have a hash function HH that takes as input a string of arbitrary length, and outputs a 16-bit binary sequence. Then H(hello")H(``hello") might be something like “0100100111011110”.

Hash functions typically produce outputs that seem random regardless of the input. With such as function, H(Hello")H(``Hello") would return a completely different pattern even though it is only a single-letter change from the previous example.

There are many hash functions that are widely used, such as SHA, MD5, and CRC32. One way to use these functions in JavaScript using the crypto module, which typically is built-in within Node.js. Another is to install the crypto-js package.

const crypto = require('crypto');

let h1 = crypto.createHash('sha1').update('Hello').digest('hex');
console.log(h1); // f7ff9e8b7bb2e09b70935a5d785e0cc5d9d0abf0

let h2 = crypto.createHash('sha1').update('hello').digest('hex');
console.log(h2); // aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d

let h3 = crypto.createHash('sha1').update('h').digest('hex');
console.log(h3); // 27d5482eebd075de44389774fce28c69f45c8a75

SHA1 outputs a 160-bit hash, which can be represented by 40 hexadecimal digits. As you can see, the hash of “hello” and “Hello” are completely different. Also, the length of the input does not affect the length of the output: the hash of “h” is identically 160 bits. Using these properties of hash functions, we can generate a random-looking binary string for each username.

“Drawing” hashes

Now that we have a random binary string of fixed length, how can we represent it visually? Let’s say our string is “0100100111011110” (16 bits).

Here’s one possibility:

I represented each “0” a white square, and “1” as a gray square. It’s a classic way of visualizing binary. The code is shown below.

draw={(ctx, w, h) => {
  let hash = '0100100111011110';
  let blockWidth = w / 16;
  for (let i = 0; i < 16; i++) {
    if (hash[i] === '1') {
      ctx.fillStyle = 'gray';
      ctx.fillRect(blockWidth*i, 0, blockWidth, h);
    }
    ctx.fillStyle='lightgray';
    ctx.fillRect(blockWidth*i, 0, 1, h); // vertical lines
  }
}

The result isn’t too aesthetically attractive though, and the dimensions are far too wide to be used as a profile icon. To fix this, why don’t we reshape the 1×161 \times 16 sequence into a 4×44 \times 4 grid?

draw={(ctx, w, h) => {
  let hash = '0100100111011110';
  let blockWidth = w / 4;
  for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 4; j++) {
      if (hash[i*4 + j] === '1') {
        ctx.fillStyle = 'gray';
        ctx.fillRect(blockWidth*j, blockWidth*i, blockWidth, blockWidth);
      }
    }
  }
}

Adding symmetry

While the dimensions of our icon are now appropriate, it is still far from aesthetic. Github and Stackoverflow’s identicons use the simple technique of forcing symmetry in order to create visual interest. We can do the same by flipping our graphic around both the horizontal and vertical axes.

draw={(ctx, w, h) => {
  let hash = '0100100111011110';
  let blockWidth = w / 8;
  for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 4; j++) {
      if (hash[i*4 + j] === '1') {
        ctx.fillStyle = 'gray';
        ctx.fillRect(blockWidth*j, blockWidth*i, blockWidth, blockWidth);
        ctx.fillRect(w - blockWidth*(j+1), blockWidth*i, blockWidth, blockWidth); // Horizontal flip
        ctx.fillRect(blockWidth*j, h - blockWidth*(i+1), blockWidth, blockWidth); // Vertical flip
        ctx.fillRect(w - blockWidth*(j+1), h - blockWidth*(i+1), blockWidth, blockWidth); // Diagonal flip
      }
    }
  }
}

Adding color

Although our icon doesn’t look too bad, can we be satisfied that our icon is unique to each user? The answer is no, because our technique is fundamentally limited in the number of possible designs it can produce: 216=655362 ^ {16} = 65536. We will expand the output space by varying the color of the filled squares.

Color on screen can be defined by three values: R, G, and B, which each take on an intensity value between 0 and 255. Therefore, a simple way to “extract” color values from a binary string is to take three blocks of 8 bits.

Visual hash structure

The algorithm can now output 240=10995116277762^{40} = 1099511627776 unique pattern–color combinations, which is over one trillion. Plenty of possibilities to give everyone on Earth their own unique pattern! However, it should be noted that many of these outputs would look similar, or even virtually identical—after all, who can tell the difference between RGB(128, 128, 128) and RGB(128, 128, 129)?

I will leave it up to you to guarantee “more uniqueness”, but in the meantime showcase the final evolution of our example. Try feeding the generator different “seed” usernames to see different identicons!

import sha1 from 'crypto-js';

draw={(ctx, w, h, username) => {
  let hash = sha1(username);
  let blockWidth = w / 8;

  // Convert the hash, which is represented by 32-bit integers,
  // to a array of 0's and 1's.
  let arr = [];
  for (let word of hash.words) {
    for (let i=0; i < 32; i++) {
      arr.push(word >> i & 1);
    }
  }

  let r = 0, g = 0, b = 0;
  for (let i = 0; i < 8; i++) {
    r += arr[16+i] << i;
    g += arr[24+i] << i;
    b += arr[32+i] << i;
  }

  for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 4; j++) {
      if (arr[i*4 + j] === 1) {
        ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
        ctx.fillRect(blockWidth*j, blockWidth*i, blockWidth, blockWidth);
        ctx.fillRect(w - blockWidth*(j+1), blockWidth*i, blockWidth, blockWidth); // Horizontal flip
        ctx.fillRect(blockWidth*j, h - blockWidth*(i+1), blockWidth, blockWidth); // Vertical flip
        ctx.fillRect(w - blockWidth*(j+1), h - blockWidth*(i+1), blockWidth, blockWidth); // Diagonal flip
      }
    }
  }
}