Conversions

Converting between the tc-lib-color models: the conversion matrix, the formulas, round-trip accuracy, inversion and gamut

Every model implements all five to*Array() methods, so any colour reaches any model in one call. The array that comes back is the component array the target constructor takes.

<?php

require_once __DIR__ . '/vendor/autoload.php';

use Com\Tecnick\Color\Model;

$rgb = (new \Com\Tecnick\Color\Web())->getColorObj('#336699');

echo Model::create('GRAY', $rgb->toGrayArray())->getCssColor(), "\n";
echo Model::create('HSL', $rgb->toHslArray())->getCssColor(), "\n";
echo Model::create('CMYK', $rgb->toCmykArray())->getCssColor(), "\n";
echo Model::create('LAB', $rgb->toLabArray())->getCssColor(), "\n";
g(95)
hsl(210,50%,40%)
cmyk(66.6667%,33.3333%,0%,40%)
lab(42.0081% -0.1517 -32.846)

The Matrix

#336699 expressed in each model, converted to each of the others:

From \ ToGRAYRGBHSLCMYKLAB
GRAYg(95)rgb(95,95,95)hsl(0,0%,37.192%)cmyk(0%,0%,0%,62.808%)lab(40.2514% 0 0)
RGBg(95)rgb(51,102,153)hsl(210,50%,40%)cmyk(66.6667%,33.3333%,0%,40%)lab(42.0081% -0.1517 -32.846)
HSLg(95)rgb(51,102,153)hsl(210,50%,40%)cmyk(66.6667%,33.3333%,0%,40%)lab(42.0081% -0.1517 -32.846)
CMYKg(95)rgb(51,102,153)hsl(210,50%,40%)cmyk(66.6667%,33.3333%,0%,40%)lab(42.0081% -0.1517 -32.846)
LABg(95)rgb(51,102,153)hsl(210,50%,40%)cmyk(66.6667%,33.3333%,0%,40%)lab(42.0081% -0.1517 -32.846)

Every row but GRAY returns the colour it started from. A grey carries no hue, so the whole GRAY row is grey and the original colour is not recoverable from it.

What Each Conversion Computes

RGB is the hub. A conversion between two non-RGB models goes through it.

ToMethod
GrayscaleRec. 709 luma: 0.2126 red, 0.7152 green, 0.0722 blue
HSLthe standard hexcone transform, hue picked by the channel holding the maximum
CMYKcomplement the three channels, extract the common minimum as the key
CIE Lablinearize sRGB, apply the D65 XYZ matrix, apply the CIE pivot function

None of them consults a colour profile. The Lab conversion is the only one derived from a colour appearance model, and it uses a fixed white point.

Round Trip

Converting out and back is lossless for three of the four, at 8-bit precision, over the whole web colour table:

ViaCallRestored exactlyWorst channel error
GRAY$rgb->toGrayArray()15 of 150237
HSL$rgb->toHslArray()150 of 1500
CMYK$rgb->toCmykArray()150 of 1500
LAB$rgb->toLabArray()150 of 1500

Grayscale is lossy by definition rather than by rounding: three components in, one out.

The script that produced the three lossless rows:

<?php

require_once __DIR__ . '/vendor/autoload.php';

use Com\Tecnick\Color\Model;
use Com\Tecnick\Color\Model\Rgb;
use Com\Tecnick\Color\Web;

$web = new Web();
$worst = ['HSL' => 0.0, 'CMYK' => 0.0, 'LAB' => 0.0];

foreach (Web::WEBHEX as $hex) {
    $rgb = $web->getRgbObjFromHex($hex);
    $before = $rgb->getNormalizedArray(255);

    foreach (['HSL' => 'toHslArray', 'CMYK' => 'toCmykArray', 'LAB' => 'toLabArray'] as $type => $method) {
        $back = Model::create($type, $rgb->{$method}())->toRgbArray();
        $after = (new Rgb($back))->getNormalizedArray(255);
        foreach (['R', 'G', 'B'] as $channel) {
            $worst[$type] = max($worst[$type], abs($before[$channel] - $after[$channel]));
        }
    }
}

print_r($worst);
Array
(
    [HSL] => 0
    [CMYK] => 0
    [LAB] => 0
)

Lossless at 8 bits is not lossless. The float components drift in the last places, and a chain of conversions accumulates that drift even where each step round-trips cleanly at 8 bits. Convert once, from the model the colour is held in, rather than at every stage of a pipeline.

Inversion

invertColor() mutates the receiver and returns it. withInvertedColor() returns a copy and leaves the receiver alone.

// returns a copy
echo $rgb->withInvertedColor()->getRgbHexColor(), "\n";
echo $rgb->getRgbHexColor(), "\n";

// mutates and returns $this
echo $rgb->invertColor()->getRgbHexColor(), "\n";
echo $rgb->getRgbHexColor(), "\n";
#cc9966
#336699
#cc9966
#cc9966

Inversion is defined on the RGB channels for every model, so inverting an Hsl object converts to RGB, complements, and converts back. The result is the complement of the colour, not of the components: inverting a hue of 210 degrees does not give 30.

Gamut

RGB and CMYK cover different sets of colours. The conversion maps a saturated screen colour to the closest value the formula produces, so the two models agree across most of the range and differ at the saturated end.

Pick the authoritative side for a document and hold the colours there. Where the press is authoritative, hold them as CMYK or as spot colours, which carry a named ink through to the plate. Where the screen is, hold them as RGB and convert on output.

Pin the values with tests. Drift in a conversion chain does not show up in a diff.

See Also