3

I'm trying to write a function that will take an RGB(A) color and save its values in an array, with an option to exclude the alpha value. I'm having trouble figuring out how to write the regex that will match the input, but I'm thinking that something like this is what I'm looking for:

function rgb_array( $color, $include_alpha = true ) {

    $pattern = 'what goes here?';

    $color = preg_match( $pattern, $color, $matches);

    if ( $include_alpha == true && ! empty( $matches[4] ) ) {
        $color = array( $matches[1], $matches[2], $matches[3], $matches[4] );
    } else {
        $color = array( $matches[1], $matches[2], $matches[3] );
    }

    return $color;
}

I would like to be able to feed it any valid form of rgb/rgba:

rgb(0,0,0)
rgb(00,00,00)
rgb(0, 0, 0)
rgb(00, 00, 00)
rgba(0,0,0,0.5)
rgba(255, 255, 255, 1)
etc...

And have it produce an array:

[0] => '00', // red
[1] => '00', // green
[2] => '00', // blue
[3] => '1' // alpha only included if available and desired.

At the moment I'm able to accomplish this through str_replace:

$color  = 'rgb(12, 14, 85)';
$remove = array( 'rgb', 'a', '(', ')', ' ' );
$color  = str_replace( $remove, '', $color );
$color  = explode( ',', $color );

But it feels hacky and I can't find a good way to optionally include/exclude alpha.

Thanks for your help, and if there's a completely different approach than preg_match that would be better, I'm all ears.

Shoelaced
  • 846
  • 5
  • 27
  • Why not simply split on comma, then you will have an array containing each. – Poul Bak Nov 04 '18 at 00:05
  • I'm currently doing that with `explode()` as I showed at the end, but if I keep that then I need a way to exclude the alpha value from the array if it's not wanted or the passed color doesn't include it. – Shoelaced Nov 04 '18 at 00:08
  • possible duplicate of https://stackoverflow.com/questions/7543818/regex-javascript-to-match-both-rgb-and-rgba – Jon Marnock Nov 04 '18 at 02:53

4 Answers4

4

Not only will my answer extract the substring values that you desire, it will additionally perform a reasonably high level of validation (not 100% perfect validation). I modified the pattern from https://stackoverflow.com/a/31245990/2943403 to more precisely serve this question. I trust you will find this answer to be accurate, elegant, and direct.

If you want to strip back / simplify the pattern, this will do: (Regex101 Demo)
~^rgba?\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([.\d]+))?\)$~

Code: (Demo w/ $include_alpha = true) (Demo w/ $include_alpha = false) (Regex101 Demo)

function rgb_array($color, $include_alpha = true) {
    $pattern = '~^rgba?\((25[0-5]|2[0-4]\d|1\d{2}|\d\d?)\s*,\s*(25[0-5]|2[0-4]\d|1\d{2}|\d\d?)\s*,\s*(25[0-5]|2[0-4]\d|1\d{2}|\d\d?)\s*(?:,\s*([01]\.?\d*?))?\)$~';

    if (!preg_match($pattern, $color, $matches)) {
        return [];  // disqualified / no match
    }

    return array_slice($matches, 1, $include_alpha ? 4 : 3);
}

$strings = [
    'rgb(0,0,0)',
    'rgb(00,00,00)',
    'rgb(0, 0, 0)',
    'donkey eggs',
    'rgb(00, 00, 00)',
    'rgba(0,0,0,0.5)',
    'rgba(255, 255, 255, 1)'
];

foreach ($strings as $string) {
    echo "Process: $string\n";
    var_export(rgb_array($string));
    echo "\n";
}

Output:

Process: rgb(0,0,0)
array (
  0 => '0',
  1 => '0',
  2 => '0',
)
Process: rgb(00,00,00)
array (
  0 => '00',
  1 => '00',
  2 => '00',
)
Process: rgb(0, 0, 0)
array (
  0 => '0',
  1 => '0',
  2 => '0',
)
Process: donkey eggs
array (
)
Process: rgb(00, 00, 00)
array (
  0 => '00',
  1 => '00',
  2 => '00',
)
Process: rgba(0,0,0,0.5)
array (
  0 => '0',
  1 => '0',
  2 => '0',
  3 => '0.5',
)
Process: rgba(255, 255, 255, 1)
array (
  0 => '255',
  1 => '255',
  2 => '255',
  3 => '1',
)

p.s. If you want to use preg_split() you can be far less specific about your pattern. Here's a one-liner for you. (Demo)

function rgb_array($color, $include_alpha = true) {
    return array_slice(preg_split('~[^\d.]+~', $color, -1, PREG_SPLIT_NO_EMPTY), 0, $include_alpha + 3);
}
mickmackusa
  • 43,625
  • 12
  • 83
  • 136
  • Nice! This is clearly the most complete answer (Sorry, Nick, yours worked too!) I didn't really need validation but this function runs much faster than the other way I was validating, so that's still helpful. – Shoelaced Nov 04 '18 at 16:36
  • 1
    @Shoelaced no problem at all - you should always accept the **best** answer even if that does come along after you've accepted another one. – Nick Nov 04 '18 at 23:26
2

If you want a regex solution, here it is:

rgba?\((\s?\d+),(\s?\d+),(\s?\d+)(?:,\s?(\d+(?:\.\d+)?))?\)

It matches 'rgb' at start, then an optional 'a' and a left parenthes, then it creates 3 similar groupes, matching an optinal White Space followed by one or more digits. The fourth Group is optional and will match one or more digits, optionally followed by a dot, and one or more digits.

The colors will be at index 1, 2, 3 and 4 (if avaiable).

Poul Bak
  • 10,450
  • 5
  • 32
  • 57
  • this matches invalid rgb things such as rbga(0, 0, 0) or rgb(0, 0, 0, 0). It also only allows for a single whitespace around the digits. A slightly broader approach is something like this (though this puts things in different positional parameters depending on the type, and additionally misses a lot of other valid rgb/rgba things and also accepts a bunch of invalid stuff): /(?:(rgb)\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)|(rgba)\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+(?:\.\d+)?)\s*\))/i Again, this isn't a good solution, so don't use it, but it shows what I'm getting at. – Jon Marnock Nov 04 '18 at 02:57
  • 1
    Thanks! This worked but I ended up using the `preg_split` solution. Also for the record I'm validating the input separately... – Shoelaced Nov 04 '18 at 03:45
2

You could use preg_split to split the string into its component parts. The regex splits the string at one of the leading rgba(, a , or the trailing ). The PREG_SPLIT_NO_EMPTY flag is used to avoid blank values in the output array.

$strings = array('rgb(0,0,0)',
'rgb(00,00,00)',
'rgb(0, 0, 0)',
'rgb(00, 00, 00)',
'rgba(0,0,0,0.5)',
'rgba(255, 255, 255, 1)');
foreach ($strings as $string) {
    print_r(preg_split('/rgba?\(\s*|\s*,\s*|\s*\)/', $string, -1, PREG_SPLIT_NO_EMPTY));
}

Output:

Array
(
    [0] => 0
    [1] => 0
    [2] => 0
)
Array
(
    [0] => 00
    [1] => 00
    [2] => 00
)
Array
(
    [0] => 0
    [1] => 0
    [2] => 0
)
Array
(
    [0] => 00
    [1] => 00
    [2] => 00
)
Array
(
    [0] => 0
    [1] => 0
    [2] => 0
    [3] => 0.5
)
Array
(
    [0] => 255
    [1] => 255
    [2] => 255
    [3] => 1
)
Nick
  • 138,499
  • 22
  • 57
  • 95
  • Thanks! This seems to be the simplest and I ended up using `array_pop()` to conditionally remove the alpha value. – Shoelaced Nov 04 '18 at 03:38
0

A bit improved explode solution:

function rgb_array($color, $include_alpha = true)
{
    $color = trim($color, 'rgba()');       # "rgba(255, 255, 255, 1)" => "255, 255, 255, 1"
    $color = str_replace(' ', '', $color); # "255, 255, 255, 1"       => "255,255,255,1"
    $color = explode(',', $color);         # "255,255,255,1"          => ["255", "255", "255", "1"]
    $color[] = 1;                          # add extra value to be sure that all parsed colors have transparency

    return array_slice($color, 0, 3 + $include_alpha); # length is 3 or 4, depends on $include_alpha value
}

You can also extract numbers from the string based on the fact that built-in PHP type cast to float and int removes extra chars from the string:

function rgb_array($color, $include_alpha = true)
{
    $array = array_map('floatval', explode(',', $color));
    $array[] = 1;

    return array_slice($array, 0, 3 + $include_alpha);
}
user1597430
  • 1,138
  • 1
  • 7
  • 14