Language

Pliant graphical stack color models

The mainstream graphic libraries way of coding colors

Once again, mainstream graphic stack design is largely derived from historical time where they started as hardware abstraction layers.

Standard encodings are:

   •   

1 bit per pixel: black and white

   •   

4 bits per pixel: 16 colors RGB palette

   •   

8 bits per pixel: 256 colors RGB palette

   •   

16 bits per pixel: pseudo truecolor RGB (exact encoding varies between 5-5-5, and 6-6-4)

   •   

24 or 32 bits per pixel: truecolor RGB

RGB stands for Red Green Blue, and it encodes the color as a level or red light, a level of green light, and a level of blue light since it was the way to produce color on old computer screens.

Some libraries provide extra more advanced encodings:

   •   

32 bits per pixel: RGBA (RGB with an extra alpha channel encoding transparency level)

   •   

32 bits per pixel: CMYK

   •   

48 bits per pixel: RGB with 16 bits per component instead of 8

CMYK stands for Cyan Magenta Yellow Black, and it encodes the color as an amount of each of the four inks since it's the way to print colors on most printing systems.

Pliant graphic stack way of coding colors

Here are the rules followed by various Pliant color models used in the Pliant graphical stack.

   •   

The Pliant graphical library encodes color with always 8 bits per component (see FAQ for extra comments).

   •   

The number of components can vary from 1 to 16.

   •   

There can be either no transparency channel, or one that applies to all components, or one per component.

The color model used for specifying a color is provided as a string parameter passed to 'color_gamut' function that returns a data structure with type ColorGamut providing all informations (number of components, of bytes per pixel, etc) about pixel encoding details to various graphical stack functions.

Here is a sample:

var Link:ColorGamut g :> color_gamut "rgb"

The standard names for additive color models are:

   •   

grey : single grey component (1 byte per pixel)

   •   

rgb : Three RGB components (3 bytes per pixel)

   •   

rgb32 : Three RGB components plus 1 padding byte (4 bytes per pixel)

   •   

rgba : Three RGB components plus a single transparency channel (4 bytes per pixel)

   •   

bgr : Same as rgb, but components are stored the other way round

   •   

bgr32 : Same as rgb32, but components are stored the other way round (this seems to be the preferred encoding by modern graphic cards)

   •   

and there can any number of subtractive color models described bellow.

First, a color device is mostly composed of a set of inks with color spectrum values describing the color of inks or inks mixtures at various levels.

Then, the name a subtractive color model is the name of the device, followed by a colon sign, then the name of all inks (components) separated by plus signs, and an optional '+transparency' or '+transparencies' if one or one per component transparency channel is to be added.
Here are some valid samples:

   •   

my_printer:cyan+magenta+yellow+black

   •   

offset:cyan+magenta+yellow+black+p_485_c+transparencies

The first color model has 4 bytes per pixel, the second has 10.
The fact that we force to provide the inks (components) list after the color device name instead of only the color device is because some color devices are color guides in facts, as opposed to being printer color profiles, so they contain a large number of inks.

Each color device is encoded as a Pliant PML encoded database file stored in data:/pliant/color/device/ directory. The data structure of the database is defined in module /pliant/graphic/color/database.pli
The advantage of using one database file for each color device is that since a color device is generally providing color calibration informations for a well defined printer using well defined driver parameters and paper, they might (should) be a lot of them available, it makes it easy to download or load only the few ones really used.

The URL for accessing the various color devices currently defined in your system is:
loopback:/pliant/graphic/color/editor

Here is an example loading the data:/pliant/graphic/color/epson/R800/semigloss/log PML encoded Pliant database file defining the 6 inks used by an Epson R800 inkjet printer:

var Link:ColorGamut g :> color_gamut "epson/R800/semigloss:cyan+magenta+yellow+black+red+blue"

Also, before using a substractive gamut, you should test if it's a valid one since constructing it might have failed as a result of missing underlying database:

if g=failure
  console "there is no " g:name " gamut (" g:message ")" eol

Gamut properties

The two basic properties of a gamut is decoding and encoding a pixel:

var ColorGamut g :> color_gamut "rgb"
var ColorRGB888 p := color rgb 100 150 200
var (Array Float32 gamut_maximum_dimension) c
decode addressof:p c
encode c addressof:p

Decoding means convert each component (encoded in the pixel as an 8 bits unsigned integer ranging from 0 to 255) to a 32 bits floating point value ranging from 0 to 1.
In very few words, ColorRGB888 is the Pliant data type encoding a pixel in RGB color model. See 'Single color coding' paragraph below for extra details.

Then, various properties of the color model can be queried through:

var Int i
i := g pixel_size
i := g dimension
i := g transparency
i := g padding

'dimension' is the dimension of the color model from a mathematical point of view. For RGB or RGBA, it's 3. For CMYK, it's 4.
'transparency' specifies how many alpha channels come next in the pixel. It can be either 0 or 1 or the same as 'dimension'.
Please notice that since at the moment we always encode each color model component on 8 bits, we always have:
pixel_size = dimension + transparency + padding

var Str s
s := g name
i := g model

The 'model' value can either be 'color_gamut_additive' or 'color_gamut_substractive'.
Please notice that the spelling mistake in 'color_gamut_substractive' is in the code, so you have to do the same until I correct it everywhere.

Some extra properties of the color model can be obtained through 'query' method:

var Str s
s := g query "component_name 0"

'component_name' is often used to enumerate inks in a subtractive gamut.
For extra informations that can be obtained through 'query' method, see various implementations of 'query' in /pliant/graphic/color/gamut.pli module.

Colors conversion

The great feature of Pliant graphic stack gamuts is that they provide not too bad (I could have said world best) color conversion.

The basic usage is:

var ColorGamut g :> color_gamut "rgb"
var ColorRGB888 p := color rgb 100 150 200
var ColorXYZ c := g simulate addressof:p
formulate c addressof:p

'simulate' finds the CIE XYZ color that will result from the various selected components values in the specified color model.
'formulate' finds some values for the various components of the selected color model that produce a color matching (as far as possible) the provided CIE XYZ encoded target.

Common sense would be to convert from one gamut to the other (let's say from RGB to printer CMYK) through calling RGB 'simulate' method, then calling CMYK 'formulate' method. Anyway, it does not always produce the optimum result, and 'formulate' is quite slow, so that direct conversion using several cache levels is generally preferred. Here is how to do it:

var ColorGamut sg :> color_gamut "rgb"
var ColorGamut dg :> color_gamut "epson/R800/semigloss:cyan+magenta+yellow+black"
var Arrow s := dg speedup sg ""
var ColorRGB888 p := color rgb 100 150 200
dg convert sg addressof:p addressof:(var ColorBuffer q) 1 p
console "Cyan value is " (cast (addressof:q map uInt8 0) Int) eol
console "Magenta value is " (cast (addressof:q map uInt8 1) Int) eol

'speedup' builds and returns an object that contains or maps various optimization caches.
Then 'convert' is used to convert a set of pixels at once. Here the 1 parameter means that we want to convert a single pixel.

- to be added: how the conversion machinery works - yuck -

Handling color spectrum

Starting from here, I'm referring, probably often with wrong wording, to a lot of very technical notions about color. Please search for external documents about various CIE color models if you need step by step introduction to CIE color models.

Color spectrum handling in defined in module /pliant/graphic/color/spectrum.pli through two data types:

   •   

ColorSpectrum is the slow more general one where the sampling step can be freely selected.

   •   

ColorSpectrum32 is the more efficient one using fixed sampling range and step (from 400 to 700 nm with 10 nm stepping)

Here are some sample usage of ColorSpectrum:

var ColorSpectrum s
s set_step 10
s set_measure 400 0.1
var Float f := s get_measure 400
var Float f := s 403

'get_measure' will return the sample with the wavelength matching best the specified one. On the other hand, the empty method used at the last line of this sample will return a value computed through interpolating the two measures with wavelengths surrounding the specified one.

And, now some usage of ColorSpectrum32:

var ColorSpectrum32 a := cast s ColorSpectrum32
var ColorSpectrum32 b c
c := a + b
c := a - b
c := 0.5 * a
c := a * b
c := a / b
c := a ^ 0.5
var Float f
f := a integral
f := a modulus
c := min a b
c := max a b
c := log a
c := exp a
c := exposure a 0.1
c := unexposure a 0.1

For most operations such as plus, the implementation is to apply the specified operation on each measure.
Then 'integral' returns the sum of all measures multiplied by the measure stepping.
'modulus' returns the sum of all absolute measures (the measure without the sign) multiplied by the measure stepping.

'exposure' is an improved gamma function. It is the interpolation of the exponential curve between 0 and some point depending on the parameters.
The properties are:

   •   

(exposure 0 p) = 0 whatever p value is.

   •   

if p is small, then (exposure 0.5 p) is close to 0.5+p

   •   

(exposure 1 p) = 1 whatever p value is.

'unexposure' is the exact opposite of 'exposure', so we have

   •   

(unexposure (exposure x p) p) = x whatever x and p are.

   •   

(exposure (exposure x p) -p) is close to x whatever x and p are.

Single color coding

This document is assuming you understand what sRGB, CIE XYZ, Lab and LCh color models are, if not, please ask Google to point you out Wikipedia articles.

Module /pliant/graphic/color/color.pli provides extra data types and functions to ease some conversions.

Available data types are:

   •   

ColorRGB (fields are r, g and b, and range from 0 to 1)

   •   

ColorXYZ (fields are X, Y and Z, and range from 0 to roughly 1)

   •   

ColorXYZn (fields are X, Y and Z, and range from 0 to 1)

   •   

ColorYxy (fields are X, Y and Z, and range from 0 to 1)

   •   

ColorHSV (fields are h ranging from 0 to 360, and s and v ranging from 0 to 1)

   •   

ColorLab (fields are L ranging from 0 to 100, and a and b ranging roughly from -100 to 100)

   •   

ColorLCh (fields are L ranging from 0 to 100, C ranging from 0 to roughly 100, and h ranging from 0 to 360)

For RGB, the red, green and blue coordinates are assumed to be the ones defined in sRGB standard.
Moreover, when casting from XYZ to Lab or LCh, the X, Y and Z filters are the 2° defined by the CIE, and the reference illuminant is D50.
XYZn stands for normalized XYZ, so X Y and Z coordinates have been divided by X Y and Z coordinates of the D50 reference illuminant.
HSV is just another way to encode RGB, when h stands for hue and ranges from 0 to 360, s stands for saturation and ranges from 0 to 1, and v stands for value and also ranges from 0 to 1. s=1 means that there is no white in the color, that is (min R G B)=0.  v=1 means that the color cannot be made more powerful without adding some white, that is (max R G B)=1.
LCH is CIE LCHab.

Here is a sample usage of these data types and conversions:

var ColorXYZ c1 ; c1 X := 0.4 ; c1 Y := 0.6 ; c1 Z := 0.7
var ColorLCh c2 := cast c1 ColorLCh
console "L = " c2:L " , C = " c2:C " , h = " c2:h eol

One very useful provided function is the color distance. The recommended one is implementing standard CMC 86 color distance:

var ColorXYZ c1 c2
var Float d := cmc_distance c1 c2

'lab_distance' is also provided, but it matches human perceived color distances less accurately.

Then an extra ColorRGB888 data type and easy construction functions are provided in /pliant/graphic/color/rgb888

var ColorRGB888 c := color rgb 100 150 200

The color can also be specified in a cylindric representation of the sRGB color space:

var ColorRGB888 c := color hsl 60 30 80

The first parameter, hue, ranges from 0 to 360 where 0 means red, 60 means yellow, 120 means green, 240 means blue. The second, saturation ranges from 0 to 100 where 0 means grey, 100 means maximum saturation in the sRGB color space, and the third parameter, light, ranges from 0 to 100. What is special about Pliant hsl is that the four main colors, red, yellow, green and blue have light 50 and saturation 100, so the light is not much correlated to CIE Y light.

'color' instuction accepts two more ways to specify an sRGB color:

var ColorRGB888 c := color hexa "FF0000" # red
var ColorRGB888 c := color html "#FF0000" # red again

'ColorRGB888' data type also provide to and from string casting, so you can do:

if ("[dq]FF0000[dq]" parse (var ColorRGB888 c))
  console c eol # produces FF0000

or

if ("[dq]#FF0000[dq]" parse (var ColorRGB888 c))
  console (string c "html") # produces #FF0000

or

if ("[dq]color rgb 255 0 0[dq]" parse (var ColorRGB888 c))
  console c eol

or

if ("[dq]color hsl 0 100 50[dq]" parse (var ColorRGB888 c))
  console c eol

Please notice that to and from string casting is unconsistent. If you do:

var ColorRGB888 c
if (string:c parse (var ColorRGB888 c2))
  console "this is consistent" eol

you get nothing. To get it working, you would have to do:

var ColorRGB888 c
if ("[dq]"+string:c+"[dq]" parse (var ColorRGB888 c2))
  console "this is working" eol

or

var ColorRGB888 c
if (string:string:c parse (var ColorRGB888 c2))
  console "this is working" eol

Transparent casting between ColorRGB888 and ColorRGB is also provided, so that you can write:

var ColorRGB c := color rgb 100 150 200

Please notice that ColorRGB is using linear encoding with components ranging from 0 to 1, whereas ColorRGB888 is using gamma 2.4 encoding as assumed in sRGB standard and integer components ranging from 0 to 255.

Module /pliant/graphic/color/adjust.pli provides a nice 'color_adjust' function that enables to easily apply changes to a color:

var ColorXYZ c
...
color_adjust c "exposure 0.25 temperature 3000 orthogonal 5100 saturation -0.2 contrast 0.1"

'exposure' does a bit the same as putting more or less light before taking the photo. Neutral value is 0, -0.25 means roughly half the amount of light, 0.25 twice more light, and 1 or 2 are fairly extreme values.
'temperature' adjusts color temperature. Neutral value is 5000 meaning 5000 Kelvin degrees. 4000 will make the color more yellowish, 6000 more blueish.
'orthogonal' also adjusts color, but on the other axis. Neutral value is still 5000, 4900 will make the color more reddish, 5100 will make it more greenish. The effect of 'orthogonal' seems much stronger to the eye than the effect of 'temperature' because it does not happen in nature, so adjust gently only, or you will get ugly results.
'saturation' adjusts ... saturation. Neutral value is 0, -1 or 1 are fairly extreme values.
'contrast' adjusts the contrast. Neutral value is 0, -1 or 1 are fairly extreme values.
There are plenty of less frequently used parameters, see the code.

FAQ

Assuming that a color component is always encoded on 8 bits in a bad idea.

Yes indeed.
Also, for efficiency reasons, some dirty parts of the code assume that components can be accessed directly (one component per byte) so don't call the proper decoding functions.
We can get rid of it through two different approaches:

   •   

Check the 'bits_per_component' flag of the gamut and automatically switch to less efficient but properly working version when the assertion is not satisfied.

   •   

Just document the functions that do the assertion. Aiee. I'm bitten by the consequences of writing the documentation long after the code :-(

Handling 1 bit per pixel black and white images as 8 bits images consumes too much memory for my application.

Providing color models that would not fit the byte boundary (such as 1 bit per pixel black and white color model) is just not possible because it would make the overall code too complicated.
On the other hand, two solutions are possible:

   •   

If the black and white image is not the result of some dithering algorithm, handling it as an ImagePaked data type instead of an ImagePixmap might solve the problem (at the expense of speed) because ImagePacked as run length compression capabilities.

   •   

We could implement 'ImageBitmap' data type that stores only the top most bit of each component. So, an 'ImageBitmap' with color model 'grey' would truly consume one bit per pixel.

As a summary, the solution here is not to extend the color models, but extend the in memory image encodings if necessary.

Some scanners and camera have 10 bits or 12 bits sensors. The 24 bits RGB encoding provided in Pliant graphic stack is not enough since it's only 8 bits per component. Why don't you provide 48 bits RGB (16 bits per component) ?

More than 8 bits per component is mostly of no use. I mean, if you take a 12 bits per component RGB image, then reduce to 8 bits per component using an algorithm with basic dithering capabilities, nobody will see the difference in the end. Even 6 bits per component is probably enough in most situations.
So, I can see only one situation where 12 bits has some real value: if you plan to later apply a very strong gamma correction to the image. As an example, if the initial image is much too dark. So my two cents advise is: apply the gamma correction in the input device driver, then cut to 8 bits through basic dithering.
Anyway, various RGB gamuts are currently implemented as ColorGamutRGB real data type and the code handling it is rather short, so if 48 bits RGB proves really useful at some point, adding ColorGamutRGB16 would not be a huge amount of work.

Looking at the code, I see an 'XYZ' gamut with 3 32 bits floating point components. What is it used for ?

I bet it's not used any more. Will probably be removed when I'm sure it's really not used.