RGB/HSL Conversions in JavaScript

This is a JavaScript project to convert colour values from red, green and blue (RGB) to hue, saturation and lightness (HSL) and back again. It runs on Node.js and uses the Jimp "JavaScript Image Manipulation Program" which is actually an npm library.

RGB & HSL

Most people will be familiar with representing colours in RGB format using either decimal or hexadecimal numbers, for example in CSS you might use

Using rgb values in CSS

.redonyellow
{
    color: #FF0000;
    background-color: rgb(255, 255, 0);
}

Many people will also be aware of HSL which is a cylindrical coordinate representation of a colour, and is described fully (along with another similar system called HSV) in this Wikipedia article. You can also use HSL in CSS, for example

Using HSL values in CSS

.redonyellow
{
    color: hsl(0, 100%, 50%);
    background-color: hsl(60, 100%, 50%);
}

Coding

This project consists of the two JavaScript files listed below. The files can be downloaded as a zip from the Downloads page, or cloned/downloaded from Github.

  • rgbhsl.js

  • rgbhsldemo.js

Source Code Links

ZIP File
GitHub

The rgbhsl.js file is a Node module with the following functions:

  • CalculateLightness

  • CalculateSaturation

  • CalculateHue

  • CalculateRGBfromHSL

rgbhsl.js

exports.CalculateLightness = function(R, G, B)
{
    let Max = 0.0
    let Min = 0.0

    let fR = R / 255.0;
    let fG = G / 255.0;
    let fB = B / 255.0;

    if(fR >= fG && fR >= fB)
        Max = fR;
    else if(fG >= fB && fG >= fR)
        Max = fG;
    else if(fB >= fG && fB >= fR)
        Max = fB;

    if(fR <= fG && fR <= fB)
        Min = fR;
    else if(fG <= fB && fG <= fR)
        Min = fG;
    else if(fB <= fG && fB <= fR)
        Min = fB;

    let Lightness = (Min + Max) / 2.0;

    return Lightness;
}


exports.CalculateSaturation = function(R, G, B)
{
    let Max = 0.0;
    let Min = 0.0;

    let fR = R / 255.0;
    let fG = G / 255.0;
    let fB = B / 255.0;

    if(fR >= fG && fR >= fB)
        Max = fR;
    else if(fG >= fB && fG >= fR)
        Max = fG;
    else if(fB >= fG && fB >= fR)
        Max = fB;

    if(fR <= fG && fR <= fB)
        Min = fR;
    else if(fG <= fB && fG <= fR)
        Min = fG;
    else if(fB <= fG && fB <= fR)
        Min = fB;

    let Lightness = exports.CalculateLightness(R, G, B);

    let Saturation;

    if(Max == Min)
    {
        Saturation = -1.0;
    }
    else
    {
        if(Lightness < 0.5)
        {
            Saturation = (Max - Min) / (Max + Min);
        }
        else
        {
            Saturation = (Max - Min) / (2.0 - Max - Min);
        }
    }

    return Saturation;
}


exports.CalculateHue = function(R, G, B)
{
    let Max = 0.0;
    let Min = 0.0;

    let fR = R / 255.0;
    let fG = G / 255.0;
    let fB = B / 255.0;

    if(fR >= fG && fR >= fB)
        Max = fR;
    else if(fG >= fB && fG >= fR)
        Max = fG;
    else if(fB >= fG && fB >= fR)
        Max = fB;

    if(fR <= fG && fR <= fB)
        Min = fR;
    else if(fG <= fB && fG <= fR)
        Min = fG;
    else if(fB <= fG && fB <= fR)
        Min = fB;

    let Hue;

    if(Max == Min)
    {
        Hue = -1.0;
    }
    else
    {
        if(Max == fR)
        {
            Hue = (fG - fB) / (Max - Min);
        }
        else if(Max == fG)
        {
            Hue = 2.0 + (fB - fR) / (Max - Min);
        }
        else if(Max == fB)
        {
            Hue = 4.0 + (fR - fG) / (Max - Min);
        }

        Hue *= 60.0;

        if(Hue < 0.0)
        {
            Hue += 360.0;
        }
    }

    return Hue;
}


exports.CalculateRGBfromHSL = function(H, S, L)
{
    let colour = { R: 0, G: 0, B: 0 };

    if(H == -1.0 && S == -1.0)
    {
        colour.R = L * 255.0;
        colour.G = L * 255.0;
        colour.B = L * 255.0;
    }
    else
    {
        let temporary_1;

        if(L < 0.5)
            temporary_1 = L * (1.0 + S);
        else
            temporary_1 = L + S - L * S;

        let temporary_2;

        temporary_2 = 2.0 * L - temporary_1;

        let hue = H / 360.0;

        let temporary_R = hue + 0.333;
        let temporary_G = hue;
        let temporary_B = hue - 0.333;

        if(temporary_R < 0.0)
            temporary_R += 1.0;
        if(temporary_R > 1.0)
            temporary_R -= 1.0;

        if(temporary_G < 0.0)
            temporary_G += 1.0;
        if(temporary_G > 1.0)
            temporary_G -= 1.0;

        if(temporary_B < 0.0)
            temporary_B += 1.0;
        if(temporary_B > 1.0)
            temporary_B -= 1.0;

        // RED
        if((6.0 * temporary_R) < 1.0)
        {
            colour.R = (temporary_2 + (temporary_1 - temporary_2) * 6.0 * temporary_R) * 255.0;
        }
        else if((2.0 * temporary_R) < 1.0)
        {
            colour.R = temporary_1 * 255.0;
        }
        else if((3.0 * temporary_R) < 2.0)
        {
            colour.R = (temporary_2 + (temporary_1 - temporary_2) * (0.666 - temporary_R) * 6.0) * 255.0;
        }
        else
        {
            colour.R = temporary_2 * 255.0;
        }

        // GREEN
        if((6.0 * temporary_G) < 1.0)
        {
            colour.G = (temporary_2 + (temporary_1 - temporary_2) * 6.0 * temporary_G) * 255.0;
        }
        else if((2.0 * temporary_G) < 1.0)
        {
            colour.G = temporary_1 * 255.0;
        }
        else if((3.0 * temporary_G) < 2.0)
        {
            colour.G = (temporary_2 + (temporary_1 - temporary_2) * (0.666 - temporary_G) * 6.0) * 255.0;
        }
        else
        {
            colour.G = temporary_2 * 255.0;
        }

        // BLUE
        if((6.0 * temporary_B) < 1.0)
        {
            colour.B = (temporary_2 + (temporary_1 - temporary_2) * 6.0 * temporary_B) * 255.0;
        }
        else if((2.0 * temporary_B) < 1.0)
        {
            colour.B = temporary_1 * 255.0;
        }
        else if((3.0 * temporary_B) < 2.0)
        {
            colour.B = (temporary_2 + (temporary_1 - temporary_2) * (0.666 - temporary_B) * 6.0) * 255.0;
        }
        else
        {
            colour.B = temporary_2 * 255.0;
        }
    }

    colour.R = Math.round(Math.abs(colour.R));
    colour.G = Math.round(Math.abs(colour.G));
    colour.B = Math.round(Math.abs(colour.B));

    return colour;
}

Usually I would give a commentary on the code at this stage but as it consists entirely of very dense calculations you might prefer to just regard these four functions as "black boxes". If you do wish to understand the processes then working through the code will probably be more effective than any textual description.

Now let's look at rgbhsldemo.js where we try out the above module.

rgbhsldemo.js

RGBtoHSLtoRGB = function()
{
    const Jimp = require("jimp");
    const RGBHSL = require('./rgbhsl.js');

    Jimp.read("./canterbury.jpg", function (err, photo)
    {
        if (err)
        {
            throw err;
        }
        else
        {
             let clone = photo.clone();

             let hue;
             let saturation;
             let lightness;
             let rgb;

             for(let i = 0, l = photo.bitmap.data.length; i < l; i+= 4)
             {
                 hue = RGBHSL.CalculateHue(photo.bitmap.data[i], photo.bitmap.data[i+1], photo.bitmap.data[i+2]);
                 saturation = RGBHSL.CalculateSaturation(photo.bitmap.data[i], photo.bitmap.data[i+1], photo.bitmap.data[i+2]);
                 lightness  = RGBHSL.CalculateLightness(photo.bitmap.data[i], photo.bitmap.data[i+1], photo.bitmap.data[i+2]);
                 rgb = RGBHSL.CalculateRGBfromHSL(hue, saturation, lightness);

                 clone.bitmap.data[i] = rgb.R;
                 clone.bitmap.data[i+1] = rgb.G;
                 clone.bitmap.data[i+2] = rgb.B;
                 clone.bitmap.data[i+3] = photo.bitmap.data[i+3];
             }

            clone.write("canterbury_copy.jpg");
        }
    });
}


RGBtoHSLtoRGB();

This code is rather more friendly. After requiring Jimp and our rgbhsl.js file we call a Jimp method to open an image. I will write a post at a later date on Jimp but the code here which opens/clones/saves an image should be self explanatory so let's just look at the for loop.

The object returned by Jimp contains an array of bytes in bitmap.data, each block of four bytes containing the RGBA values for a pixel. (A represents alpha or opacity which is ignored here.)

We iterate this data - note the i+= 4 in the for loop - and firstly call our three functions to get H, S and L values. We then pass these to a fourth function to get back to RGB. In a real application we would edit the HSL values before converting them back but here I am just creating an identical copy of the image.

Finally we set the corresponding RGB values in the clone, thereby building up the image pixel by pixel. After the loop we just save the clone under a new name.

Now we can go ahead and run the program. I'll use this image but you will probably want to use your own. This is a small 600x400 image but using larger images can be very slow.

This is the terminal command to run the program.

Run

node rgbhsldemo.js

After the program finishes you'll get a new image file which is identical to the original. You might like to experiment with changing HSL values before converting back to RGB. If you do any calculations on them make sure they don't stray outside their minimum or maximum values. I'll explore this in more detail in a later post.