Tag: Converting

Converting and Optimizing Images From the Command Line

Images take up to 50% of the total size of an average web page. And if images are not optimized, users end up downloading extra bytes. And if they’re downloading extra bytes, the site not only takes that much more time to load, but users are using more data, both of which can be resolved, at least in part, by optimizing the images before they are downloaded.

Researchers around the world are busy developing new image formats that possess high visual quality despite being smaller in size compared to other formats like PNG or JPG. Although these new formats are still in development and generally have limited browser support, one of them, WebP, is gaining a lot of attention. And while they aren’t really in the same class as raster images, SVGs are another format many of us have been using in recent years because of their inherently light weight.

There are tons of ways we can make smaller and optimized images. In this tutorial, we will write bash scripts that create and optimize images in different image formats, targeting the most common formats, including JPG, PNG, WebP, and SVG. The idea is to optimize images before we serve them so that users get the most visually awesome experience without all the byte bloat.

Our targeted directory of images

Our directory of optimized images

This GitHub repo has all the images we’re using and you’re welcome to grab them and follow along.

Set up

Before we start, let’s get all of our dependencies in order. Again, we’re writing Bash scripts, so we’ll be spending time in the command line.

Here are the commands for all of the dependencies we need to start optimizing images:

sudo apt-get update sudo apt-get install imagemagick webp jpegoptim optipng npm install -g svgexport svgo

It’s a good idea to know what we’re working with before we start using them:

OK, we have our images in the original-images directory from the GitHub repo. You can follow along at commit 3584f9b.

Note: It is strongly recommended to backup your images before proceeding. We’re about to run programs that alter these images, and while we plan to leave the originals alone, one wrong command might change them in some irreversible way. So back anything up that you plan to use on a real project to prevent cursing yourself later.

Organize images

OK, we’re technically set up. But before we jump into optimizing all the things, we should organize our files a bit. Let’s organize them by splitting them up into different sub-directories based on their MIME type. In fact, we can create a new bash to do that for us!

The following code creates a script called organize-images.sh:

#!/bin/bash  input_dir="$ 1"  if [[ -z "$ input_dir" ]]; then   echo "Please specify an input directory."   exit 1 fi  for img in $ ( find $ input_dir -type f -iname "*" ); do   # get the type of the image   img_type=$ (basename `file --mime-type -b $ img`)    # create a directory for the image type   mkdir -p $ img_type    # move the image into its type directory   rsync -a $ img $ img_type done

This might look confusing if you’re new to writing scripts, but what it’s doing is actually pretty simple. We give the script an input directory where it looks for images. the script then goes into that input directory, looks for image files and identifies their MIME type. Finally, it creates subdirectories in the input folder for each MIME type and drops a copy of each image into their respective sub-directory.

Let’s run it!

bash organize-images.sh original-images

Sweet. The directory looks like this now. Now that our images are organized, we can move onto creating variants of each image. We’ll tackle one image type at a time.

Convert to PNG

We will convert three types of images into PNG in this tutorial: WebP, JPEG, and SVG. Let’s start by writing a script called webp2png.sh, which pretty much says what it does: convert WebP files to PNG files.

#!/bin/bash  # directory containing images input_dir="$ 1"  if [[ -z "$ input_dir" ]]; then   echo "Please specify an input directory."   exit 1 fi  # for each webp in the input directory for img in $ ( find $ input_dir -type f -iname "*.webp" ); do   dwebp $ img -o $ {img%.*}.png done

Here’s what happening:

  • input_dir="$ 1": Stores the command line input to the script
  • if [[ -z "$ input_dir" ]]; then: Runs the subsequent conditional if the input directory is not defined
  • for img in $ ( find $ input_dir -type f -iname "*.webp" );: Loops through each file in the directory that has a .webp extension.
  • dwebp $ img -o $ {img%.*}.png: Converts the WebP image into a PNG variant.

And away we go:

bash webp2png.sh webp

We now have our PNG images in the webp directory. Next up, let’s convert JPG/JPEG files to PNG with another script called jpg2png.sh:

#!/bin/bash  # directory containing images input_dir="$ 1"  if [[ -z "$ input_dir" ]]; then   echo "Please specify an input directory."   exit 1 fi  # for each jpg or jpeg in the input directory for img in $ ( find $ input_dir -type f -iname "*.jpg" -o -iname "*.jpeg" ); do   convert $ img $ {img%.*}.png done

This uses the convert command provided by the ImageMagick package we installed. Like the last script, we provide an input directory that contains JPEG/JPG images. The script looks in that directory and creates a PNG variant for each matching image. If you look closely, we have added -o -iname "*.jpeg" in the find. This refers to Logical OR, which is the script that finds all the images that have either a .jpg or .jpeg extension.

Here’s how we run it:

bash jpg2png.sh jpeg

Now that we have our PNG variants from JPG, we can do the exact same thing for SVG files as well:

#!/bin/bash  # directory containing images input_dir="$ 1"  # png image width width="$ 2"  if [[ -z "$ input_dir" ]]; then   echo "Please specify an input directory."   exit 1 elif [[ -z "$ width" ]]; then   echo "Please specify image width."   exit 1 fi  # for each svg in the input directory for img in $ ( find $ input_dir -type f -iname "*.svg" ); do   svgexport $ img $ {img%.*}.png $ width: done

This script has a new feature. Since SVG is a scalable format, we can specify the width directive to scale our SVGs up or down. We use the svgexport package we installed earlier to convert each SVG file into a PNG:

bash svg2png.sh svg+xml

Commit 76ff80a shows the result in the repo.

We’ve done a lot of great work here by creating a bunch of PNG files based on other image formats. We still need to do the same thing for the rest of the image formats before we get to the real task of optimizing them.

Convert to JPG

Following in the footsteps of PNG image creation, we will convert WebP, JPEG, and SVG into JPG. Let’s start by writing a script called png2jpg.sh that converts PNG to SVG:

#!/bin/bash  # directory containing images input_dir="$ 1"  # jpg image quality quality="$ 2"  if [[ -z "$ input_dir" ]]; then   echo "Please specify an input directory."   exit 1 elif [[ -z "$ quality" ]]; then   echo "Please specify image quality."   exit 1 fi  # for each png in the input directory for img in $ ( find $ input_dir -type f -iname "*.png" ); do   convert $ img -quality $ quality% $ {img%.*}.jpg done

You might be noticing a pattern in these scripts by now. But this one introduces a new power where we can set a -quality directive to convert PNG images to JPG images. Rest is the same.

And here’s how we run it:

bash png2jpg.sh png 90

Woah. We now have JPG images in our png directory. Let’s do the same with a webp2jpg.sh script:

#!/bin/bash  # directory containing images input_dir="$ 1"  # jpg image quality quality="$ 2"  if [[ -z "$ input_dir" ]]; then   echo "Please specify an input directory."   exit 1 elif [[ -z "$ quality" ]]; then   echo "Please specify image quality."   exit 1 fi  # for each webp in the input directory for img in $ ( find $ input_dir -type f -iname "*.webp" ); do   # convert to png first   dwebp $ img -o $ {img%.*}.png    # then convert png to jpg   convert $ {img%.*}.png -quality $ quality% $ {img%.*}.jpg done

Again, this is the same thing we wrote for converting WebP to PNG. However, there is a twist. We cannot convert WebP format directly into a JPG format. Hence, we need to get a little creative here and convert WebP to PNG using dwebp and then convert PNG to JPG using convert. That is why, in the for loop, we have two different steps.

Now, let’s run it:

bash webp2jpg.sh jpeg 90

Voilà! We have created JPG variants for our WebP images. Now let’s tackle SVG to JPG:

#!/bin/bash  # directory containing images input_dir="$ 1"  # jpg image width width="$ 2"  # jpg image quality quality="$ 3"  if [[ -z "$ input_dir" ]]; then   echo "Please specify an input directory."   exit 1 elif [[ -z "$ width" ]]; then   echo "Please specify image width."   exit 1 elif [[ -z "$ quality" ]]; then   echo "Please specify image quality."   exit 1 fi  # for each svg in the input directory for img in $ ( find $ input_dir -type f -iname "*.svg" ); do     svgexport $ img $ {img%.*}.jpg $ width: $ quality% done

You might bet thinking that you have seen this script before. You have! We used the same script for to create PNG images from SVG. The only addition to this script is that we can specify the quality directive of our JPG images.

bash svg2jpg.sh svg+xml 512 90

Everything we just did is contained in commit 884c6cf in the repo.

Convert to WebP

WebP is an image format designed for modern browsers. At the time of this writing, it enjoys roughly 90% global browser support, including with partial support in Safari. WebP’s biggest advantage is it’s a much smaller file size compared to other mage formats, without sacrificing any visual quality. That makes it a good format to serve to users.

But enough talk. Let’s write a png2webp.sh that — you guessed it — creates WebP images out of PNG files:

#!/bin/bash  # directory containing images input_dir="$ 1"  # webp image quality quality="$ 2"  if [[ -z "$ input_dir" ]]; then   echo "Please specify an input directory."   exit 1 elif [[ -z "$ quality" ]]; then   echo "Please specify image quality."   exit 1 fi  # for each png in the input directory for img in $ ( find $ input_dir -type f -iname "*.png" ); do   cwebp $ img -q $ quality -o $ {img%.*}.webp done

This is just the reverse of the script we used to create PNG images from WebP files. Instead of using dwebp, we use cwebp.

bash png2webp.sh png 90

We have our WebP images. Now let’s convert JPG images. The tricky thing is that there is no way to directly convert a JPG files into WebP. So, we will first convert JPG to PNG and then convert the intermediate PNG to WebP in our jpg2webp.sh script:

#!/bin/bash  # directory containing images input_dir="$ 1"  # webp image quality quality="$ 2"  if [[ -z "$ input_dir" ]]; then   echo "Please specify an input directory."   exit 1 elif [[ -z "$ quality" ]]; then   echo "Please specify image quality."   exit 1 fi  # for each webp in the input directory for img in $ ( find $ input_dir -type f -iname "*.jpg" -o -iname "*.jpeg" ); do   # convert to png first   convert $ img $ {img%.*}.png    # then convert png to webp   cwebp $ {img%.*}.png -q $ quality -o $ {img%.*}.webp done

Now we can use it like this to get our WebP variations of JPG files:

bash jpg2webp.sh jpeg 90

Commit 6625f26 shows the result.

Combining everything into a single directory

Now that we are done converting stuff, we’re one step closer to optimize our work. But first, we’re gong to bring all of our images back into a single directory so that it is easy to optimize them with fewer commands.

Here’s code that creates a new bash script called combine-images.sh:

#!/bin/bash  input_dirs="$ 1" output_dir="$ 2"  if [[ -z "$ input_dirs" ]]; then   echo "Please specify an input directories."   exit 1 elif [[ -z "$ output_dir" ]]; then   echo "Please specify an output directory."   exit 1 fi  # create a directory to store the generated images mkdir -p $ output_dir  # split input directories comma separated string into an array input_dirs=($ {input_dirs//,/ })  # for each directory in input directory for dir in "$ {input_dirs[@]}" do   # copy images from this directory to generated images directory   rsync -a $ dir/* $ output_dir/ done

The first argument is a comma-separated list of input directories that will transfer images to a target combined directory. The second argument is defines that combined directory.

bash combine-images.sh jpeg,svg+xml,webp,png generated-images

The final output can be seen in the repo.

Optimize SVG

Let us start by optimizing our SVG images. Add the following code to optimize-svg.sh:

#!/bin/bash  # directory containing images input_dir="$ 1"  if [[ -z "$ input_dir" ]]; then echo "Please specify an input directory." exit 1 fi  # for each svg in the input directory for img in $ ( find $ input_dir -type f -iname "*.svg" ); do   svgo $ img -o $ {img%.*}-optimized.svg done

We’re using the SVGO package here. It’s got a lot of options we can use but, to keep things simple, we’re just sticking with the default behavior of optimizing SVG files:

bash optimize-svg.sh generated-images
This gives us a 4KB saving on each image. Let’s say we were serving 100 SVG icons — we just saved 400KB!

The result can be seen in the repo at commit 75045c3.

Optimize PNG

Let’s keep rolling and optimize our PNG files using this code to create an optimize-png.sh command:

#!/bin/bash  # directory containing images input_dir="$ 1"  if [[ -z "$ input_dir" ]]; then   echo "Please specify an input directory."   exit 1 fi  # for each png in the input directory for img in $ ( find $ input_dir -type f -iname "*.png" ); do   optipng $ img -out $ {img%.*}-optimized.png done

Here, we are using the OptiPNG package to optimize our PNG images. The script looks for PNG images in the input directory and creates an optimized version of each one, appending -optimized to the file name. There is one interesting argument, -o, which we can use to specify the optimization level. The default value is 2 **and values range from 0 to 7. To optimize our PNGs, we run:

bash optimize-png.sh generated-images
PNG optimization depends upon the information stored in the image. Some images can be greatly optimized while some show little to no optimization.

As we can see, OptiPNG does a great job optimizing the images. We can play around with the -o argument to find a suitable value by trading off between image quality and size. Check out the results in commit 4a97f29.

Optimize JPG

We have reached the final part! We’re going to wrap things up by optimizing JPG images. Add the following code to optimize-jpg.sh:

#!/bin/bash  # directory containing images input_dir="$ 1"  # target image quality quality="$ 2"  if [[ -z "$ input_dir" ]]; then   echo "Please specify an input directory."   exit 1 elif [[ -z "$ quality" ]]; then   echo "Please specify image quality."   exit 1 fi  # for each jpg or jpeg in the input directory for img in $ ( find $ input_dir -type f -iname "*.jpg" -o -iname "*.jpeg" ); do   cp $ img $ {img%.*}-optimized.jpg   jpegoptim -m $ quality $ {img%.*}-optimized.jpg done

This script uses JPEGoptim. The problem with this package is that it doesn’t have any option to specify the output file. We can only optimize the image file in place. We can overcome this by first creating a copy of the image, naming it whatever we like, then optimizing the copy. The -m argument is used to specify image quality. It is good to experiment with it a bit to find the right balance between quality and file size.

bash optimize-jpg.sh generated-images 95

The results are shows in commit 35630da.

Wrapping up

See that? With a few scripts, we can perform heavy-duty image optimizations right from the command line, and use them on any project since they’re installed globally. We can set up CI/CD pipelines to create different variants of each image and serve them using valid HTML, APIs, or even set up our own image conversion websites.

I hope you enjoyed reading and learning something from this article as much as I enjoyed writing it for you. Happy coding!


The post Converting and Optimizing Images From the Command Line appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,

Converting Color Spaces in JavaScript

A challenge I faced in building an image “emojifier” was that I needed to change the color spaces of values obtained using getImageData() from RGB to HSL. I used arrays of emojis arranged by brightness and saturation, and they were HSL-based for the best matches of average pixel colors with the emojis.

In this article, we’ll study functions that will be useful for converting both opaque and alpha-enabled color values. Modern browsers currently support the color spaces RGB(A), hex, and HSL(A). The functions and notations for these are rgb(), rgba(), #rgb/#rrggbb, #rgba/#rrggbbaa, hsl(), and hsla(). Browsers have always supported built-in names like aliceblue as well.

Balls with color values being inserted into a machine and coming out as HSL

Along the way, we’ll encounter use of some color syntaxes provided by a new Level 4 of the CSS Colors Module. For example, we now have hex with alpha as we mentioned (#rgba/#rrggbbaa) and RGB and HSL syntaxes no longer require commas (values like rgb(255 0 0) and hsl(240 100% 50%) became legal!).

Browser support for CSS Colors Level 4 isn’t universal as of this writing, so don’t expect new color syntaxes to work in Microsoft browsers or Safari if trying them in CSS.

RGB to Hex

Converting RGB to hex is merely a change of radices. We convert the red, green, and blue values from decimal to hexadecimal using toString(16). After prepending 0s to single digits and under, we can concatenate them and # to a single return statement.

function RGBToHex(r,g,b) {   r = r.toString(16);   g = g.toString(16);   b = b.toString(16);    if (r.length == 1)     r = "0" + r;   if (g.length == 1)     g = "0" + g;   if (b.length == 1)     b = "0" + b;    return "#" + r + g + b; }

RGB in String

Alternatively, we can use a single string argument with the red, green and blue separated by commas or spaces (e.g. "rgb(255,25,2)", "rgb(255 25 2)"). Substring to eliminate rgb(, split what’s left by the ), then split that result’s first item by whichever the separator (sep) is. r, g, and b shall become local variables now. Then we use + before the split strings to convert them back to numbers before obtaining the hex values.

function RGBToHex(rgb) {   // Choose correct separator   let sep = rgb.indexOf(",") > -1 ? "," : " ";   // Turn "rgb(r,g,b)" into [r,g,b]   rgb = rgb.substr(4).split(")")[0].split(sep);    let r = (+rgb[0]).toString(16),       g = (+rgb[1]).toString(16),       b = (+rgb[2]).toString(16);    if (r.length == 1)     r = "0" + r;   if (g.length == 1)     g = "0" + g;   if (b.length == 1)     b = "0" + b;    return "#" + r + g + b; }

In addition, we can allow strings with channel values as percentages by adding the loop after redefining rgb. It’ll strip the %s and turn what’s left into values out of 255.

function RGBToHex(rgb) {   let sep = rgb.indexOf(",") > -1 ? "," : " ";   rgb = rgb.substr(4).split(")")[0].split(sep);    // Convert %s to 0–255   for (let R in rgb) {     let r = rgb[R];     if (r.indexOf("%") > -1)       rgb[R] = Math.round(r.substr(0,r.length - 1) / 100 * 255);       /* Example:       75% -> 191       75/100 = 0.75, * 255 = 191.25 -> 191       */   }    ... }

Now we can supply values like either of these:

  • rgb(255,25,2)
  • rgb(255 25 2)
  • rgb(50%,30%,10%)
  • rgb(50% 30% 10%)

RGBA to Hex (#rrggbbaa)

Converting RGBA to hex with the #rgba or #rrggbbaa notation follows virtually the same process as the opaque counterpart. Since the alpha (a) is normally a value between 0 and 1, we need to multiply it by 255, round the result, then convert it to hexadecimal.

function RGBAToHexA(r,g,b,a) {   r = r.toString(16);   g = g.toString(16);   b = b.toString(16);   a = Math.round(a * 255).toString(16);    if (r.length == 1)     r = "0" + r;   if (g.length == 1)     g = "0" + g;   if (b.length == 1)     b = "0" + b;   if (a.length == 1)     a = "0" + a;    return "#" + r + g + b + a; }

To do this with one string (including with percentages), we can follow what we did earlier. Also note the extra step of splicing out a slash. Since CSS Colors Level 4 supports the syntax of rgba(r g b / a), this is where we allow it. Alpha values can now be percentages! This removes the 0-1-only shackles we used to have. Therefore, the for loop cycling through rgba shall include a part to wipe the % from the alpha without multiplying by 255 (when R is 3 for alpha). Soon we can use values like rgba(255 128 0 / 0.8) and rgba(100% 21% 100% / 30%)!

function RGBAToHexA(rgba) {   let sep = rgba.indexOf(",") > -1 ? "," : " ";   rgba = rgba.substr(5).split(")")[0].split(sep);                    // Strip the slash if using space-separated syntax   if (rgba.indexOf("/") > -1)     rgba.splice(3,1);    for (let R in rgba) {     let r = rgba[R];     if (r.indexOf("%") > -1) {       let p = r.substr(0,r.length - 1) / 100;        if (R < 3) {         rgba[R] = Math.round(p * 255);       } else {         rgba[R] = p;       }     }   } }

Then, where the channels are converted to hex, we adjust a to use an item of rgba[].

function RGBAToHexA(rgba) {   ...        let r = (+rgba[0]).toString(16),       g = (+rgba[1]).toString(16),       b = (+rgba[2]).toString(16),       a = Math.round(+rgba[3] * 255).toString(16);    if (r.length == 1)     r = "0" + r;   if (g.length == 1)     g = "0" + g;   if (b.length == 1)     b = "0" + b;   if (a.length == 1)     a = "0" + a;    return "#" + r + g + b + a; }

Now the function supports the following:

  • rgba(255,25,2,0.5)
  • rgba(255 25 2 / 0.5)
  • rgba(50%,30%,10%,0.5)
  • rgba(50%,30%,10%,50%)
  • rgba(50% 30% 10% / 0.5)
  • rgba(50% 30% 10% / 50%)

Hex to RGB

We know that the length of hex values must either be 3 or 6 (plus #). In either case, we begin each red (r), green (g), and blue (b) value with "0x" to convert them to hex. If we provide a 3-digit value, we concatenate the same value twice for each channel. If it’s a 6-digit value, we concatenate the first two for red, next two for green, and last two for blue. To get the values for the final rgb() string, we prepend the variables with + to convert them from strings back to numbers, which will yield the decimals we need.

function hexToRGB(h) {   let r = 0, g = 0, b = 0;    // 3 digits   if (h.length == 4) {     r = "0x" + h[1] + h[1];     g = "0x" + h[2] + h[2];     b = "0x" + h[3] + h[3];    // 6 digits   } else if (h.length == 7) {     r = "0x" + h[1] + h[2];     g = "0x" + h[3] + h[4];     b = "0x" + h[5] + h[6];   }      return "rgb("+ +r + "," + +g + "," + +b + ")"; }

Output RGB with %s

If we want to return rgb() using percentages, then we can modify the function to utilize an optional isPct parameter like so:

function hexToRGB(h,isPct) {   let r = 0, g = 0, b = 0;   isPct = isPct === true;    if (h.length == 4) {     r = "0x" + h[1] + h[1];     g = "0x" + h[2] + h[2];     b = "0x" + h[3] + h[3];        } else if (h.length == 7) {     r = "0x" + h[1] + h[2];     g = "0x" + h[3] + h[4];     b = "0x" + h[5] + h[6];   }        if (isPct) {     r = +(r / 255 * 100).toFixed(1);     g = +(g / 255 * 100).toFixed(1);     b = +(b / 255 * 100).toFixed(1);   }      return "rgb(" + (isPct ? r + "%," + g + "%," + b + "%" : +r + "," + +g + "," + +b) + ")"; }

Under the last if statement, using +s will convert r, g, and b to numbers. Each toFixed(1) along with them will round the result to the nearest tenth. Additionally, we won’t have whole numbers with .0 or the decades old bug that produces numbers like 0.30000000000000004. Therefore, in the return, we omitted the +s right before the first r, g, and b to prevent NaNs caused by the %s. Now we can use hexToRGB("#ff0",true) to get rgb(100%,100%,0%)!

Hex (#rrggbbaa) to RGBA

The procedure for hex values with alpha should again be similar with the last. We simply detect a 4- or 8-digit value (plus #) then convert the alpha and divide it by 255. To get more precise output but not long decimal numbers for alpha, we can use toFixed(3).

function hexAToRGBA(h) {   let r = 0, g = 0, b = 0, a = 1;    if (h.length == 5) {     r = "0x" + h[1] + h[1];     g = "0x" + h[2] + h[2];     b = "0x" + h[3] + h[3];     a = "0x" + h[4] + h[4];    } else if (h.length == 9) {     r = "0x" + h[1] + h[2];     g = "0x" + h[3] + h[4];     b = "0x" + h[5] + h[6];     a = "0x" + h[7] + h[8];   }   a = +(a / 255).toFixed(3);    return "rgba(" + +r + "," + +g + "," + +b + "," + a + ")"; }

Output RGBA with %s

For a version that outputs percentages, we can do what we did in hexToRGB()—switch r, g, and b to 0–100% when isPct is true.

function hexAToRGBA(h,isPct) {   let r = 0, g = 0, b = 0, a = 1;   isPct = isPct === true;        // Handling of digits   ...    if (isPct) {     r = +(r / 255 * 100).toFixed(1);     g = +(g / 255 * 100).toFixed(1);     b = +(b / 255 * 100).toFixed(1);   }   a = +(a / 255).toFixed(3);    return "rgba(" + (isPct ? r + "%," + g + "%," + b + "%," + a : +r + "," + +g + "," + +b + "," + a) + ")"; }

Here’s a quick fix if the alpha ought to be a percentage, too: move the statement where a is redefined above the last if statement. Then in that statement, modify a to be like r, g, and b. When isPct is true, a must also gain the %.

function hexAToRGBA(h,isPct) {   ...        a = +(a / 255).toFixed(3);   if (isPct) {     r = +(r / 255 * 100).toFixed(1);     g = +(g / 255 * 100).toFixed(1);     b = +(b / 255 * 100).toFixed(1);     a = +(a * 100).toFixed(1);   }    return "rgba(" + (isPct ? r + "%," + g + "%," + b + "%," + a + "%" : +r + "," + +g + "," + +b + "," + a) + ")"; }

When we enter #7f7fff80 now, we should get rgba(127,127,255,0.502) or rgba(49.8%,49.8%,100%,50.2%).

RGB to HSL

Obtaining HSL values from RGB or hex is a bit more challenging because there’s a larger formula involved. First, we must divide the red, green, and blue by 255 to use values between 0 and 1. Then we find the minimum and maximum of those values (cmin and cmax) as well as the difference between them (delta). We need that result as part of calculating the hue and saturation. Right after the delta, let’s initialize the hue (h), saturation (s), and lightness (l).

function RGBToHSL(r,g,b) {   // Make r, g, and b fractions of 1   r /= 255;   g /= 255;   b /= 255;    // Find greatest and smallest channel values   let cmin = Math.min(r,g,b),       cmax = Math.max(r,g,b),       delta = cmax - cmin,       h = 0,       s = 0,       l = 0; }

Next, we need to calculate the hue, which is to be determined by the greatest channel value in cmax (or if all channels are the same). If there is no difference between the channels, the hue will be 0. If cmax is the red, then the formula will be ((g - b) / delta) % 6. If green, then (b - r) / delta + 2. Then, if blue, (r - g) / delta + 4. Finally, multiply the result by 60 (to get the degree value) and round it. Since hues shouldn’t be negative, we add 360 to it, if needed.

function RGBToHSL(r,g,b) {   ...   // Calculate hue   // No difference   if (delta == 0)     h = 0;   // Red is max   else if (cmax == r)     h = ((g - b) / delta) % 6;   // Green is max   else if (cmax == g)     h = (b - r) / delta + 2;   // Blue is max   else     h = (r - g) / delta + 4;    h = Math.round(h * 60);        // Make negative hues positive behind 360°   if (h < 0)       h += 360; }

All that’s left is the saturation and lightness. Let’s calculate the lightness before we do the saturation, as the saturation will depend on it. It’s the sum of the maximum and minimum channel values cut in half ((cmax + cmin) / 2). Then delta will determine what the saturation will be. If it’s 0 (no difference between cmax and cmin), then the saturation is automatically 0. Otherwise, it’ll be 1 minus the absolute value of twice the lightness minus 1 (1 - Math.abs(2 * l - 1)). Once we have these values, we must convert them to values out of 100%, so we multiply them by 100 and round to the nearest tenth. Now we can string together our hsl().

function RGBToHSL(r,g,b) {   ...   // Calculate lightness   l = (cmax + cmin) / 2;    // Calculate saturation   s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));        // Multiply l and s by 100   s = +(s * 100).toFixed(1);   l = +(l * 100).toFixed(1);    return "hsl(" + h + "," + s + "%," + l + "%)"; }

RGB in String

For one string, split the argument by comma or space, strip the %s, and localize r, g, and b like we did before.

function RGBToHSL(rgb) {   let sep = rgb.indexOf(",") > -1 ? "," : " ";   rgb = rgb.substr(4).split(")")[0].split(sep);    for (let R in rgb) {     let r = rgb[R];     if (r.indexOf("%") > -1)       rgb[R] = Math.round(r.substr(0,r.length - 1) / 100 * 255);   }    // Make r, g, and b fractions of 1   let r = rgb[0] / 255,       g = rgb[1] / 255,       b = rgb[2] / 255;    ... }

RGBA to HSLA

Compared to what we just did to convert RGB to HSL, the alpha counterpart will be basically nothing! We just reuse the code for RGB to HSL (the multi-argument version), leave a alone, and pass a to the returned HSLA. Keep in mind it should be between 0 and 1.

function RGBAToHSLA(r,g,b,a) {   // Code for RGBToHSL(r,g,b) before return   ...    return "hsla(" + h + "," + s + "%," +l + "%," + a + ")"; }

RGBA in String

For string values, we apply the splitting and stripping logic again but use the fourth item in rgba for a. Remember the new rgba(r g b / a) syntax? We’re employing the acceptance of it as we did for RGBAToHexA(). Then the rest of the code is the normal RGB-to-HSL conversion.

function RGBAToHSLA(rgba) {   let sep = rgba.indexOf(",") > -1 ? "," : " ";   rgba = rgba.substr(5).split(")")[0].split(sep);    // Strip the slash if using space-separated syntax   if (rgba.indexOf("/") > -1)     rgba.splice(3,1);    for (let R in rgba) {     let r = rgba[R];     if (r.indexOf("%") > -1) {       let p = r.substr(0,r.length - 1) / 100;        if (R < 3) {         rgba[R] = Math.round(p * 255);       } else {         rgba[R] = p;       }     }   }    // Make r, g, and b fractions of 1   let r = rgba[0] / 255,       g = rgba[1] / 255,       b = rgba[2] / 255,       a = rgba[3];    // Rest of RGB-to-HSL logic   ... }

Wish to leave the alpha as is? Remove the else statement from the for loop.

for (let R in rgba) {   let r = rgba[R];   if (r.indexOf("%") > -1) {     let p = r.substr(0,r.length - 1) / 100;      if (R < 3) {       rgba[R] = Math.round(p * 255);     }   } }

HSL to RGB

It takes slightly less logic to convert HSL back to RGB than the opposite way. Since we’ll use a range of 0–100 for the saturation and lightness, the first step is to divide them by 100 to values between 0 and 1. Next, we find chroma (c), which is color intensity, so that’s (1 - Math.abs(2 * l - 1)) * s. Then we use x for the second largest component (first being chroma), the amount to add to each channel to match the lightness (m), and initialize r, g, b.

function HSLToRGB(h,s,l) {   // Must be fractions of 1   s /= 100;   l /= 100;    let c = (1 - Math.abs(2 * l - 1)) * s,       x = c * (1 - Math.abs((h / 60) % 2 - 1)),       m = l - c/2,       r = 0,       g = 0,       b = 0; }

The hue will determine what the red, green, and blue should be depending on which 60° sector of the color wheel it lies.

Color wheel
The color wheel divided into 60° segments

Then c and x shall be assigned as shown below, leaving one channel at 0. To get the final RGB value, we add m to each channel, multiply it by 255, and round it.

function HSLToRGB(h,s,l) {   ...    if (0 <= h && h < 60) {     r = c; g = x; b = 0;   } else if (60 <= h && h < 120) {     r = x; g = c; b = 0;   } else if (120 <= h && h < 180) {     r = 0; g = c; b = x;   } else if (180 <= h && h < 240) {     r = 0; g = x; b = c;   } else if (240 <= h && h < 300) {     r = x; g = 0; b = c;   } else if (300 <= h && h < 360) {     r = c; g = 0; b = x;   }   r = Math.round((r + m) * 255);   g = Math.round((g + m) * 255);   b = Math.round((b + m) * 255);    return "rgb(" + r + "," + g + "," + b + ")"; }

HSL in String

For the single string version, we modify the first few statements basically the same way we did for RGBToHSL(r,g,b). Remove s /= 100; and l /= 100; and we’ll use the new statements to wipe the first 4 characters and the ) for our array of HSL values, then the %s from s and l before dividing them by 100.

function HSLToRGB(hsl) {   let sep = hsl.indexOf(",") > -1 ? "," : " ";   hsl = hsl.substr(4).split(")")[0].split(sep);    let h = hsl[0],       s = hsl[1].substr(0,hsl[1].length - 1) / 100,       l = hsl[2].substr(0,hsl[2].length - 1) / 100;    ... }

The next handful of statements shall handle hues provided with a unit—degrees, radians, or turns. We multiply radians by 180/π and turns by 360. If the result ends up over 360, we compound modulus divide to keep it within the scope. All of this will happen before we deal with c, x, and m.

function HSLToRGB(hsl) {   ...    // Strip label and convert to degrees (if necessary)   if (h.indexOf("deg") > -1)     h = h.substr(0,h.length - 3);   else if (h.indexOf("rad") > -1)     h = Math.round(h.substr(0,h.length - 3) * (180 / Math.PI));   else if (h.indexOf("turn") > -1)     h = Math.round(h.substr(0,h.length - 4) * 360);   // Keep hue fraction of 360 if ending up over   if (h >= 360)     h %= 360;        // Conversion to RGB begins   ... }

After implementing the steps above, now the following can be safely used:

  • hsl(180 100% 50%)
  • hsl(180deg,100%,50%)
  • hsl(180deg 100% 50%)
  • hsl(3.14rad,100%,50%)
  • hsl(3.14rad 100% 50%)
  • hsl(0.5turn,100%,50%)
  • hsl(0.5turn 100% 50%)

Whew, that’s quite the flexibility!

Output RGB with %s

Similarly, we can modify this function to return percent values just like we did in hexToRGB().

function HSLToRGB(hsl,isPct) {   let sep = hsl.indexOf(",") > -1 ? "," : " ";   hsl = hsl.substr(4).split(")")[0].split(sep);   isPct = isPct === true;    ...    if (isPct) {     r = +(r / 255 * 100).toFixed(1);     g = +(g / 255 * 100).toFixed(1);     b = +(b / 255 * 100).toFixed(1);   }    return "rgb("+ (isPct ? r + "%," + g + "%," + b + "%" : +r + "," + +g + "," + +b) + ")"; }

HSLA to RGBA

Once again, handling alphas will be a no-brainer. We can reapply the code for the original HSLToRGB(h,s,l) and add a to the return.

function HSLAToRGBA(h,s,l,a) {   // Code for HSLToRGB(h,s,l) before return   ...    return "rgba(" + r + "," + g + "," + b + "," + a + ")"; }

HSLA in String

Changing it to one argument, the way we’ll handle strings here will be not too much different than what we did earlier. A new HSLA syntax from Colors Level 4 uses (value value value / value) just like RGBA, so having the code to handle it, we’ll be able to plug in something like hsla(210 100% 50% / 0.5) here.

function HSLAToRGBA(hsla) {   let sep = hsla.indexOf(",") > -1 ? "," : " ";   hsla = hsla.substr(5).split(")")[0].split(sep);    if (hsla.indexOf("/") > -1)     hsla.splice(3,1);    let h = hsla[0],       s = hsla[1].substr(0,hsla[1].length - 1) / 100,       l = hsla[2].substr(0,hsla[2].length - 1) / 100,       a = hsla[3];            if (h.indexOf("deg") > -1)     h = h.substr(0,h.length - 3);   else if (h.indexOf("rad") > -1)     h = Math.round(h.substr(0,h.length - 3) * (180 / Math.PI));   else if (h.indexOf("turn") > -1)     h = Math.round(h.substr(0,h.length - 4) * 360);   if (h >= 360)     h %= 360;    ... }

Furthermore, these other combinations have become possible:

  • hsla(180,100%,50%,50%)
  • hsla(180 100% 50% / 50%)
  • hsla(180deg,100%,50%,0.5)
  • hsla(3.14rad,100%,50%,0.5)
  • hsla(0.5turn 100% 50% / 50%)

RGBA with %s

Then we can replicate the same logic for outputting percentages, including alpha. If the alpha should be a percentage (searched in pctFound), here’s how we can handle it:

  1. If r, g, and b are to be converted to percentages, then a should be multiplied by 100, if not already a percentage. Otherwise, drop the %, and it’ll be added back in the return.
  2. If r, g, and b should be left alone, then remove the % from a and divide a by 100.
function HSLAToRGBA(hsla,isPct) {   // Code up to slash stripping   ...        isPct = isPct === true;        // h, s, l, a defined to rounding of r, g, b   ...        let pctFound = a.indexOf("%") > -1;        if (isPct) {     r = +(r / 255 * 100).toFixed(1);     g = +(g / 255 * 100).toFixed(1);     b = +(b / 255 * 100).toFixed(1);     if (!pctFound) {       a *= 100;     } else {       a = a.substr(0,a.length - 1);     }            } else if (pctFound) {     a = a.substr(0,a.length - 1) / 100;   }    return "rgba("+ (isPct ? r + "%," + g + "%," + b + "%," + a + "%" : +r + ","+ +g + "," + +b + "," + +a) + ")"; }

Hex to HSL

You might think this one and the next are crazier processes than the others, but they merely come in two parts with recycled logic. First, we convert the hex to RGB. That gives us the base 10s we need to convert to HSL.

function hexToHSL(H) {   // Convert hex to RGB first   let r = 0, g = 0, b = 0;   if (H.length == 4) {     r = "0x" + H[1] + H[1];     g = "0x" + H[2] + H[2];     b = "0x" + H[3] + H[3];   } else if (H.length == 7) {     r = "0x" + H[1] + H[2];     g = "0x" + H[3] + H[4];     b = "0x" + H[5] + H[6];   }   // Then to HSL   r /= 255;   g /= 255;   b /= 255;   let cmin = Math.min(r,g,b),       cmax = Math.max(r,g,b),       delta = cmax - cmin,       h = 0,       s = 0,       l = 0;    if (delta == 0)     h = 0;   else if (cmax == r)     h = ((g - b) / delta) % 6;   else if (cmax == g)     h = (b - r) / delta + 2;   else     h = (r - g) / delta + 4;    h = Math.round(h * 60);    if (h < 0)     h += 360;    l = (cmax + cmin) / 2;   s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));   s = +(s * 100).toFixed(1);   l = +(l * 100).toFixed(1);    return "hsl(" + h + "," + s + "%," + l + "%)"; }

Hex (#rrggbbaa) to HSLA

There aren’t too many lines that change in this one. We’ll repeat what we recently did to get the alpha by converting the hex, but won’t divide it by 255 right away. First, we must get the hue, saturation, and lightness as we did in the other to-HSL functions. Then, before the ending return, we divide the alpha and set the decimal places.

function hexAToHSLA(H) {   let r = 0, g = 0, b = 0, a = 1;    if (H.length == 5) {     r = "0x" + H[1] + H[1];     g = "0x" + H[2] + H[2];     b = "0x" + H[3] + H[3];     a = "0x" + H[4] + H[4];   } else if (H.length == 9) {     r = "0x" + H[1] + H[2];     g = "0x" + H[3] + H[4];     b = "0x" + H[5] + H[6];     a = "0x" + H[7] + H[8];   }    // Normal conversion to HSL   ...            a = (a / 255).toFixed(3);                    return "hsla("+ h + "," + s + "%," + l + "%," + a + ")"; }

HSL to Hex

This one starts as a conversion to RGB, but there’s an extra step to the Math.round()s of converting the RGB results to hex.

function HSLToHex(h,s,l) {   s /= 100;   l /= 100;    let c = (1 - Math.abs(2 * l - 1)) * s,       x = c * (1 - Math.abs((h / 60) % 2 - 1)),       m = l - c/2,       r = 0,       g = 0,       b = 0;    if (0 <= h && h < 60) {     r = c; g = x; b = 0;   } else if (60 <= h && h < 120) {     r = x; g = c; b = 0;   } else if (120 <= h && h < 180) {     r = 0; g = c; b = x;   } else if (180 <= h && h < 240) {     r = 0; g = x; b = c;   } else if (240 <= h && h < 300) {     r = x; g = 0; b = c;   } else if (300 <= h && h < 360) {     r = c; g = 0; b = x;   }   // Having obtained RGB, convert channels to hex   r = Math.round((r + m) * 255).toString(16);   g = Math.round((g + m) * 255).toString(16);   b = Math.round((b + m) * 255).toString(16);    // Prepend 0s, if necessary   if (r.length == 1)     r = "0" + r;   if (g.length == 1)     g = "0" + g;   if (b.length == 1)     b = "0" + b;    return "#" + r + g + b; }

HSL in String

Even the first few lines of this function will be like those in HSLToRGB() if we changed it to accept a single string. This is how we’ve been obtaining the hue, saturation, and lightness separately in the first place. Let’s not forget the step to remove the hue label and convert to degrees, too. All of this will be in place of s /= 100; and l /= 100;.

function HSLToHex(hsl) {   let sep = hsl.indexOf(",") > -1 ? "," : " ";   hsl = hsl.substr(4).split(")")[0].split(sep);    let h = hsl[0],       s = hsl[1].substr(0,hsl[1].length - 1) / 100,       l = hsl[2].substr(0,hsl[2].length - 1) / 100;            // Strip label and convert to degrees (if necessary)   if (h.indexOf("deg") > -1)     h = h.substr(0,h.length - 3);   else if (h.indexOf("rad") > -1)     h = Math.round(h.substr(0,h.length - 3) * (180 / Math.PI));   else if (h.indexOf("turn") > -1)     h = Math.round(h.substr(0,h.length - 4) * 360);   if (h >= 360)     h %= 360;    ... }

HSLA to Hex (#rrggbbaa)

Adding alpha to the mix, we convert a to hex and add a fourth if to prepend a 0, if necessary. You probably already familiar with this logic because we last used it in RGBAToHexA().

function HSLAToHexA(h,s,l,a) {   // Repeat code from HSLToHex(h,s,l) until 3 `toString(16)`s   ...    a = Math.round(a * 255).toString(16);    if (r.length == 1)     r = "0" + r;   if (g.length == 1)     g = "0" + g;   if (b.length == 1)     b = "0" + b;   if (a.length == 1)     a = "0" + a;    return "#" + r + g + b + a; }

HSLA in String

Finally, the lines of the single argument version up to a = hsla[3] are no different than those of HSLAToRGBA().

function HSLAToHexA(hsla) {   let sep = hsla.indexOf(",") > -1 ? "," : " ";   hsla = hsla.substr(5).split(")")[0].split(sep);        // Strip the slash   if (hsla.indexOf("/") > -1)     hsla.splice(3,1);        let h = hsla[0],       s = hsla[1].substr(0,hsla[1].length - 1) / 100,       l = hsla[2].substr(0,hsla[2].length - 1) / 100,       a = hsla[3];                ... }

Built-in Names

To convert a named color to RGB, hex, or HSL, you might consider turning this table of 140+ names and hex values into a massive object at the start. The truth is that we really don’t need one because here’s what we can do:

  1. Create an element
  2. Give it a text color
  3. Obtain the value of that property
  4. Remove the element
  5. Return the stored color value, which will be in RGB by default

So, our function to get RGB will only be seven statements!

function nameToRGB(name) {   // Create fake div   let fakeDiv = document.createElement("div");   fakeDiv.style.color = name;   document.body.appendChild(fakeDiv);    // Get color of div   let cs = window.getComputedStyle(fakeDiv),       pv = cs.getPropertyValue("color");    // Remove div after obtaining desired color value   document.body.removeChild(fakeDiv);    return pv; }

Let’s go even further. How about we change the output to hex instead?

function nameToHex(name) {   // Get RGB from named color in temporary div   let fakeDiv = document.createElement("div");   fakeDiv.style.color = name;   document.body.appendChild(fakeDiv);    let cs = window.getComputedStyle(fakeDiv),       pv = cs.getPropertyValue("color");    document.body.removeChild(fakeDiv);    // Code ripped from RGBToHex() (except pv is substringed)   let rgb = pv.substr(4).split(")")[0].split(","),       r = (+rgb[0]).toString(16),       g = (+rgb[1]).toString(16),       b = (+rgb[2]).toString(16);    if (r.length == 1)     r = "0" + r;   if (g.length == 1)     g = "0" + g;   if (b.length == 1)     b = "0" + b;    return "#" + r + g + b; }

Or, why not HSL? 😉

function nameToHSL(name) {   let fakeDiv = document.createElement("div");   fakeDiv.style.color = name;   document.body.appendChild(fakeDiv);    let cs = window.getComputedStyle(fakeDiv),       pv = cs.getPropertyValue("color");    document.body.removeChild(fakeDiv);    // Code ripped from RGBToHSL() (except pv is substringed)   let rgb = pv.substr(4).split(")")[0].split(","),       r = rgb[0] / 255,       g = rgb[1] / 255,       b = rgb[2] / 255,       cmin = Math.min(r,g,b),       cmax = Math.max(r,g,b),       delta = cmax - cmin,       h = 0,       s = 0,       l = 0;    if (delta == 0)     h = 0;   else if (cmax == r)     h = ((g - b) / delta) % 6;   else if (cmax == g)     h = (b - r) / delta + 2;   else     h = (r - g) / delta + 4;    h = Math.round(h * 60);    if (h < 0)     h += 360;    l = (cmax + cmin) / 2;   s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));   s = +(s * 100).toFixed(1);   l = +(l * 100).toFixed(1);    return "hsl(" + h + "," + s + "%," + l + "%)"; }

In the long run, every conversion from a name becomes a conversion from RGB after cracking the name.

Validating Colors

In all these functions, there haven’t been any measures to prevent or correct ludicrous input (say hues over 360 or percentages over 100). If we’re only manipulating pixels on a <canvas> fetched using getImageData(), validation of color values isn’t necessary before converting because they’ll be correct no matter what. If we’re creating a color conversion tool where users supply the color, then validation would be much needed.

It’s easy to handle improper input for channels as separate arguments, like this for RGB:

// Correct red if (r > 255)   r = 255; else if (r < 0)   r = 0;

If validating a whole string, then a regular expression is needed. For instance, this is the RGBToHex() function given a validation step with an expression:

function RGBToHex(rgb) {   // Expression for rgb() syntaxes   let ex = /^rgb((((((((1?[1-9]?d)|10d|(2[0-4]d)|25[0-5]),s?)){2}|((((1?[1-9]?d)|10d|(2[0-4]d)|25[0-5])s)){2})((1?[1-9]?d)|10d|(2[0-4]d)|25[0-5]))|((((([1-9]?d(.d+)?)|100|(.d+))%,s?){2}|((([1-9]?d(.d+)?)|100|(.d+))%s){2})(([1-9]?d(.d+)?)|100|(.d+))%)))$ /i;    if (ex.test(rgb)) {     // Logic to convert RGB to hex     ...    } else {     // Something to do if color is invalid   } }

To test other types of values, below is a table of expressions to cover both opaque and alpha-enabled:

Color Value RegEx
RGB /^rgb((((((((1?[1-9]?d)|10d|(2[0-4]d)|25[0-5]),s?)){2}|((((1?[1-9]?d)|10d|(2[0-4]d)|25[0-5])s)){2})((1?[1-9]?d)|10d|(2[0-4]d)|25[0-5]))|((((([1-9]?d(.d+)?)|100|(.d+))%,s?){2}|((([1-9]?d(.d+)?)|100|(.d+))%s){2})(([1-9]?d(.d+)?)|100|(.d+))%)))$ /i
RGBA /^rgba((((((((1?[1-9]?d)|10d|(2[0-4]d)|25[0-5]),s?)){3})|(((([1-9]?d(.d+)?)|100|(.d+))%,s?){3}))|(((((1?[1-9]?d)|10d|(2[0-4]d)|25[0-5])s){3})|(((([1-9]?d(.d+)?)|100|(.d+))%s){3}))/s)((0?.d+)|[01]|(([1-9]?d(.d+)?)|100|(.d+))%))$ /i
Hex /^#([da-f]{3}){1,2}$ /i
Hex (with Alpha) /^#([da-f]{4}){1,2}$ /i
HSL /^hsl(((((([12]?[1-9]?d)|[12]0d|(3[0-5]d))(.d+)?)|(.d+))(deg)?|(0|0?.d+)turn|(([0-6](.d+)?)|(.d+))rad)((,s?(([1-9]?d(.d+)?)|100|(.d+))%){2}|(s(([1-9]?d(.d+)?)|100|(.d+))%){2}))$ /i
HSLA /^hsla(((((([12]?[1-9]?d)|[12]0d|(3[0-5]d))(.d+)?)|(.d+))(deg)?|(0|0?.d+)turn|(([0-6](.d+)?)|(.d+))rad)(((,s?(([1-9]?d(.d+)?)|100|(.d+))%){2},s?)|((s(([1-9]?d(.d+)?)|100|(.d+))%){2}s/s))((0?.d+)|[01]|(([1-9]?d(.d+)?)|100|(.d+))%))$ /i

Looking at the expressions for RGB(A) and HSL(A), you probably have big eyes right now; these were made comprehensive enough to include most of the new syntaxes from CSS Colors Level 4. Hex, on the other hand, doesn’t need expressions as long as the others because of only digit counts. In a moment, we’ll dissect these and decipher the parts. Note that case-insensitive values (/i) pass all these.

RGB

/^rgb((((((((1?[1-9]?d)|10d|(2[0-4]d)|25[0-5]),s?)){2}|((((1?[1-9]?d)|10d|(2[0-4]d)|25[0-5])s)){2})((1?[1-9]?d)|10d|(2[0-4]d)|25[0-5]))|((((([1-9]?d(.d+)?)|100|(.d+))%,s?){2}|((([1-9]?d(.d+)?)|100|(.d+))%s){2})(([1-9]?d(.d+)?)|100|(.d+))%)))$ /i

Because rgb() accepts either all integers or all percentages, both cases are covered. In the outmost group, between the ^rgb( and )$ , there are inner groups for both integers and percentages, all comma-spaces or spaces only as separators:

  1. (((((1?[1-9]?d)|10d|(2[0-4]d)|25[0-5]),s?){2}|(((1?[1-9]?d)|10d|(2[0-4]d)|25[0-5])s){2})((1?[1-9]?d)|10d|(2[0-4]d)|25[0-5]))
  2. ((((([1-9]?d(.d+)?)|100|(.d+))%,s?){2}|((([1-9]?d(.d+)?)|100|(.d+))%s){2})(([1-9]?d(.d+)?)|100|(.d+))%)

In the first half, we accept two instances of integers for red and green from 0–99 or 111-199 ((1?[1-9]?d)), 100–109 (10d), 200-249 ((2[0-4]d)), or 250–255 (25[0-5]). We couldn’t simply do d{1,3} because values like 03 or 017 and those greater than 255 shouldn’t be allowed. After that goes the comma and optional space (,s?). On the other side of the |, after the first {2} (which indicates two instances of integers), we check for the same thing with space separators if the left side is false. Then for blue, the same should be accepted, but without a separator.

In the other half, acceptable values for percentages, including floats, should either be 0–99, explicitly 100 and not a float, or floats under 1 with the 0 dropped. Therefore, the segment here is (([1-9]?d(.d+)?)|100|(.d+)), and it appears three times; twice with separator (,s?){2}, %s){2}), once without.

It is legal to use percentages without space separators (rgb(100%50%10%) for instance) in CSS, but the functions we wrote don’t support that. The same goes for rgba(100%50%10%/50%), hsl(40 100%50%), and hsla(40 100%50%/0.5). This could very well be a plus for code golfing and minification!

RGBA

/^rgba((((((((1?[1-9]?d)|10d|(2[0-4]d)|25[0-5]),s?)){3})|(((([1-9]?d(.d+)?)|100|(.d+))%,s?){3}))|(((((1?[1-9]?d)|10d|(2[0-4]d)|25[0-5])s){3})|(((([1-9]?d(.d+)?)|100|(.d+))%s){3}))/s)((0?.d+)|[01]|(([1-9]?d(.d+)?)|100|(.d+))%))$ /i

The next expression is very similar to the pervious, but three instances of integers (((((1?[1-9]?d)|10d|(2[0-4]d)|25[0-5]),s?){3})) or percentages ((((([1-9]?d(.d+)?)|100|(.d+))%,s?){3})), plus comma optional space are checked. Otherwise, it looks for the same thing but with space separators, plus a slash and space (/s) after the blue. Next to that is ((0?.d+)|[01]|(([1-9]?d(.d+)?)|100|(.d+))%) where we accept floats with or without the first 0 ((0?.d+)), 0 or 1 ([01]) on the dot, or 0–100% ((([1-9]?d(.d+)?)|100|(.d+))%).

Hex with Alpha

// #rgb/#rrggbb /^#([da-f]{3}){1,2}$ /i // #rgba/#rrggbbaa /^#([da-f]{4}){1,2}$ /i

For both hex—with and without alpha—instances of numbers or letters a–f ([da-f]) are accepted. Then one or two instances of this are counted for either short or longhand values supplied (#rgb or #rrggbb). As an illustration, we have this same short pattern: /^#([da-f]{n}){1,2}$ /i. Simply change n to 3 or 4.

HSL and HSLA

// HSL /^hsl(((((([12]?[1-9]?d)|[12]0d|(3[0-5]d))(.d+)?)|(.d+))(deg)?|(0|0?.d+)turn|(([0-6.d+)?)|(.d+))rad)((,s?(([1-9]?d(.d+)?)|100|(.d+))%){2}|(s(([1-9]?d(.d+)?)|100|(.d+))%){2}))$ /i // HSLA /^hsla(((((([12]?[1-9]?d)|[12]0d|(3[0-5]d))(.d+)?)|(.d+))(deg)?|(0|0?.d+)turn|(([0-6.d+)?)|(.d+))rad)(((,s?(([1-9]?d(.d+)?)|100|(.d+))%){2},s?)|((s(([1-9]?d(.d+)?)|100|(.d+))%){2}s/s))((0?.d+)|[01]|(([1-9]?d(.d+)?)|100|(.d+))%))$ /i

After the ( in both expressions for HSL and HSLA, this large chunk is for the hue:

((((([12]?[1-9]?d)|[12]0d|(3[0-5]d))(.d+)?)|(.d+))(deg)?|(0|0?.d+)turn|(([0-6.d+)?)|(.d+))rad)

([12]?[1-9]?d) covers 0–99, 110–199, and 210–299. [12]0d covers 110–109 and 200–209. Then (3[0-5]d) takes care of 300–359. The reason for this division of ranges is similar to that of integers in the rgb() syntax: ruling out zeros coming first and values greater than the maximum. Since hues can be floating point numbers, the first (.d+)? is for that.

Next to the | after the aforementioned segment of code, the second (.d+) is for floats without a leading zero.

Now let’s move up a level and decipher the next small chunk:

(deg)?|(0|0?.d+)turn|(([0-6.d+)?)|(.d+))rad

This contains the labels we can use for the hue—degrees, turns, or radians. We can include all or none of deg. Values in turn must be under 1. For radians, we can accept any float between 0–7. We do know, however, that one 360° turn is 2π, and it stops approximately at 6.28. You may think 6.3 and over shouldn’t be accepted. Because 2π is an irrational number, it would be too messy for this example to try to satisfy every decimal place provided by the JavaScript console. Besides, we have this snippet in our HSLTo_() functions as a second layer of security if hues 360° or over were to happen:

// Keep hue fraction of 360 if ending up over if (h >= 360)   h %= 360;

Now let’s move up a level and decipher the second chunk:

(,s?(([1-9]?d(.d+)?)|100|(.d+))%){2}

We’re counting two instances of comma-space-percentages for the saturation and lightness (space optional). In the group after the ,s?, we test for values 0–99 with or without decimal points (([1-9]?d(.d+)?)), exactly 100, or floats under 1 without the leading 0 ((.d+)).

The last part the HSL expression, before the ending ()$ /i), is a similar expression if spaces are the only separator:

(s(([1-9]?d(.d+)?)|100|(.d+))%){2}

s is in the beginning instead of ,s?. Then in the HSLA expression, this same chunk is inside another group with ,s? after its {2}.

((,s?(([1-9]?d(.d+)?)|100|(.d+))%){2},s?)

That counts the comma-space between the lightness and alpha. Then if we have spaces as separators, we need to check for a space-slash-space (s/s) after counting two instances of space and a percentage.

((s(([1-9]?d(.d+)?)|100|(.d+))%){2}s/s))

After that, we have this left to check the alpha value:

(((0?.d+)|[01])|(([1-9]?d(.d+)?)|100|(.d+))%)

Matches for (0?.d+) include floats under 1 with or without the leading 0, 0 or 1 for [01], and 0–100%.

Conclusion

If your current challenge is to convert one color space to another, you now have some ideas on how to approach it. Because it would be tiresome to walk through converting every color space ever invented in one post, we discussed the most practical and browser-supported ones. If you’d like to go beyond supported color spaces (say CMYK, XYZ, or CIE L*a*b*), EasyRGB) provides an amazing set of code-ready formulas.

To see all the conversions demonstrated here, I’ve set up a CodePen demo that shows inputs and outputs in a table. You can try different colors in lines 2–10 and see the complete functions in the JavaScript panel.

See the Pen Color Conversion by Jon Kantner (@jkantner) on CodePen.

The post Converting Color Spaces in JavaScript appeared first on CSS-Tricks.

CSS-Tricks

, , ,
[Top]