The X Color Context: Color Management For (Almost) Any Occasion, Part II

by John Cwikla

Copyright © 1995,1996,1997,1998,1999,2000,2001,2002 John Cwikla, All Rights Reserved.

In my last column I began my look at using color in X. This month I will implement the ideas presented there into a client side color management library, the X Color Context. The X Color Context, or XCC, was designed to be a general color solution. My goal was to abstract color allocation by allowing a client to always receive a valid pixel for an RGB triplet given any depth, visual and colormap.

There is, however, a trade to be made between gains and restrictions in using the library. The XCC is a read-only color model, even for visuals with read-write colormaps. This is due to the extensive use of standard colormaps and "virtual" standard colormaps. ("Virtual" standard colormaps are RGB cubes allocated in a read-write colormap using the features of a standard colormap, but instead of using a standard colormap defined on a property on the root window, the client allocates the colors on the fly.) Some operations, such as dynamically changing cells in the colormap, are not supported. The first advantage of using the XCC is the ability to get a "closest" pixel for any color specification. "Closest" is still relative to the number of cells the XCC can use -- as the bit depth of the visual increases, the difference between the requested color and the returned color becomes smaller. The second advantage is that the XCC is visual transparent. The same calls are made to use a 24 bit DirectColor visual as are to use a 1 bit StaticColor visual. Next, once initialized the XCC makes no server trips to return pixel values. Everything requiring server intervention is done when the XCC is initialized. Finally, all color entries are allocated with XAllocColor() and thus are shareable. If more than one program is using the XCC they will be able to coexist peacefully.

So what is missing? Color quantizing and dithering has been left out of this discussion and the XCC library. However, once the ideas of the XCC are in place, you should be able to choose your favorite algorithms and use the pixels returned from the XCC in conjunction with tiles or stipples to create the dither pattern of choice. The XCC uses the device dependent color functions rather than the device independent (Xcms) equivalents. There is no reason why the ideas of the XCC could not be extended to encapsulate the Xcms as well.

Programmer Interface

The XCC has three major public routines.

extern XCC XCCCreate(Display *_dpy, Visual *_visual, int _usePrivateColormap,
	int _useStdCmaps, Atom _stdColormapAtom, Colormap *_colormap);

_dpy: Display * returned from XOpenDisplay(), XtOpenDisplay(), etc.
_visual: Visual on which to base the current XCC.
_usePrivateColormap: if non-zero forces creation of a private colormap
_useStdCmaps: if non-zero searches root window for standard colormap
	properties to use
_stdColormapAtom: preferred standard colormap
*_colormap: returned colormap

XCCCreate() initializes an X Color Context. The two parameters of note are the _visual and the *_colormap. The XCC is intialized according to the visual's attributes. This visual is used to determine visual class, bit depth, and an ID to match against any standard colormaps that would be used. The returned *_colormap can be the default colormap, a private colormap, or a previously allocated colormap being used by a standard colormap. XCCCreate() does all the needed server calls, once initialized all calculations are done on the client side.

extern unsigned long XCCGetPixel(XCC _xcc, unsigned int _red, 
	unsigned int _green, unsigned int _blue);

This the main XCC call. Given an RGB triplet with values in the range (0-65535, 0-65535,0-65535), XCCGetPixel() returns the "closest" pixel available to this XCC.

extern void  XCCFree(XCC _xcc);

The XCC deallocation routine frees the resources associated with this XCC.

XCC Internals

The XCC is initialized in three steps.

First if _useStdCmaps is non-zero, check to see if a standard colormap exists. If _stdColormapAtom is non-zero check for that property first, otherwise interate through the list XA_RGB_DEFAULT_MAP, XA_RGB_BEST_MAP, XA_RGB_GRAY_MAP, for color visuals and XA_RGB_GRAY_MAP, XA_RGB_DEFAULT_MAP, for grayscale visuals. If a standard colormap is found it can only be used if the the stdCmap.visualid is the same as the ID of the visual passed to XCCCreate().

If finding a standard colormap was unsuccesful (or untried) the XCC is initialized according to the visual class. If _usePrivateColomap is true or the visual passed to XCCCreate() is not the default visual a colormap is created, otherwise the default colormap is used.

Since this color management system uses read-write colormaps in a read-only fashion, it is possible to separate the six visual classes into three pairs: StaticGray and GrayScale, StaticColor and PseudoColor, and TrueColor and DirectColor. StaticGray, GrayScale, StaticColor and PseudoColor will use a "virtual" standard colormap within the XCC. The XStandardColormap structure defines a convenient way of representing an RGB cube, so we will allocate our colors according to the standard colormap definition. This has the added bonus that no matter whether the standard colormap is real or virtual we can use the same code to do pixel calculations. TrueColor and DirectColor calculate pixels directly using bits and masks defined by the visual and will not require a virtual standard colormap.

Finally, we watch for allocation failure. Although this will not happen with read-only colormaps, this problem still looms with read-write colormaps. For any of the read-write colormaps, the entries will be allocated greedily -- as many entries as possible will be allocated at initialization if a standard colormap does not exist. If only two entries can be allocated and the visual's depth is greater than 1, a private colormap is created and the allocations are done with the new colormap.

The rest of this column will touch on the highlights of the XCC, to see a full implementation, download the AppPlusShell package at ftp://ftp.x.org/contrib/widgets/AppPlusS.1.2.18.tar.Z.

1. Standard Colormaps

Finding a standard colormap is as easy as calling XGetRGBColormaps(). Given the atom representing the standard colormap name, XGetRGBColormaps() returns a list of XStandardColormaps structures and a count. If the count is greater than zero, traverse through the list and find the XStandardColormap with the visualid that matches the visual id of our visual. If this is not done, it is possible that our client will received a BadMatch error. To get a pixel with a standard colormap we use the formula:

	return _xcc->stdCmap.base_pixel 
		+ ((_red * _xcc->stdCmap.red_max)/0xFFFF) * _xcc->stdCmap.red_mult
		+ ((_green * _xcc->stdCmap.green_max)/0xFFFF) * _xcc->stdCmap.green_mult
		+ ((_blue * _xcc->stdCmap.blue_max)/0xFFFF) * _xcc->stdCmap.blue_mult;

The XCC does not create a standard colormap if it does not exist. I found through reactions from our users that those that did not understand what standard colormaps were, and from interaction with other applications that do not use standard colormaps, creating a standard colormap usually does more harm than good if only one application uses it. On the other hand, experienced users who see the benefits of a standard colormap can preallocate a portion of their default colormap as large or small as they want, in the hopes X programs will use it.

2. StaticColor and PseudoColor

StaticColor visuals have no guarantee what color will exist in what entry in a colormap, only that they cannot be changed. For PseudoColor, colors are allocated but the returned entry in the colormap is pretty random -- usually the first free entry in the colormap, but the first free entry shifts with each color allocation by your, or any other, client. So how can we get them into some order that we can use easily and be able to calculate a pixel? The answer has two parts: a lookup table and a "virtual" standard colormap.

Our standard colormap in this case is "virtual" in the sense that it does not actually exist, instead we are just using the XStandardColormap structure as a convenient way to represent our color allocation. A standard colormap is based on the RGB colorspace representing a cube with red, green and blue each along an axis. Since it is a cube, our allocation will consist of n*n*n colors, with n*n*n <= visual->map_entries. For example, an 8 bit StaticColor visual's largest cube would be 6*6*6, or 216 colors. When we allocate our colors greedily trying to get as many as possible. Since they can be stored anywhere in the colormap by the server, we will put the pixel value in a lookup table, in a predefined order, the order that a real standard colormap would have.

/* First find out how big our cube can be: */

	cubeval = 1;
	while((cubeval*cubeval*cubeval) < _xcc->visual->map_entries)
		cubeval++;
	cubeval--;
	_xcc->numColors = cubeval * cubeval * cubeval;

	_xcc->CLUT = (unsigned long *)Xmalloc(sizeof(unsigned long) * _xcc->numColors);
	cstart = (XColor *)Xmalloc(sizeof(XColor) * _xcc->numColors);

/* Next initialize our "virtual" standard colormap */

retryColor:

	_xcc->stdCmap.red_max = cubeval - 1;
	_xcc->stdCmap.green_max = cubeval - 1;
	_xcc->stdCmap.blue_max = cubeval - 1;
	_xcc->stdCmap.red_mult = cubeval * cubeval;
	_xcc->stdCmap.green_mult = cubeval;
	_xcc->stdCmap.blue_mult = 1;
	_xcc->stdCmap.base_pixel = 0;

/* Allocate our colors and store the value in the CLUT. */

	clrs = cstart;
	count = 0;
	for (red = 0; red <= _xcc->stdCmap.red_max; red++)
	{
		for (green = 0; green <= _xcc->stdCmap.green_max; green++)
		{
			for(blue = 0; blue <= _xcc->stdCmap.blue_max; blue++)
			{
				if (_xcc->stdCmap.red_max)
					clrs->red = (0xFFFF * red) / _xcc->stdCmap.red_max;
				else
					clrs->red = 0;
				if (_xcc->stdCmap.green_max)
					clrs->green = (0xFFFF * green) / _xcc->stdCmap.green_max;
				else
					clrs->green = 0;
				if (_xcc->stdCmap.blue_max)
					clrs->blue = (0xFFFF * blue) / _xcc->stdCmap.blue_max;
				else
					clrs->blue = 0;

/* On failure, we shrink the size of our cube side by 1 and retry */

				if (!XAllocColor(_xcc->dpy, _xcc->colormap, clrs))
				{
					XFreeColors(_xcc->dpy, _xcc->colormap, _xcc->CLUT, count, 0);

					cubeval--;

					if (cubeval > 1)
						goto retryColor;
					else
					{
						XFree((char *)_xcc->CLUT);
						_xcc->CLUT = NULL;
						_initBW(_xcc);
						XFree((char *)cstart);
						return;
					}

				}
				_xcc->CLUT[count++] = clrs++->pixel;
			}
		}
	}

Now when we want a pixel, we use:

	return _xcc->CLUT[_xcc->stdCmap.base_pixel 
		+ ((_red * _xcc->stdCmap.red_max)/0xFFFF) * _xcc->stdCmap.red_mult
		+ ((_green * _xcc->stdCmap.green_max)/0xFFFF) * _xcc->stdCmap.green_mult
		+ ((_blue * _xcc->stdCmap.blue_max)/0xFFFF) * _xcc->stdCmap.blue_mult];

Which is exactly what was done for the real standard colormap case, with the addition of a lookup table.

3. StaticGray and GrayScale

StaticGray and GrayScale are handled the same way except, instead of a cube we will ramp our colors from black to white, with intermediate gray values.

/* This time our ramp can use as many entries as possible. */

	_xcc->numColors = _xcc->visual->map_entries;

	_xcc->CLUT = (unsigned long *)Xmalloc(sizeof(unsigned long) * _xcc->numColors);
	cstart = (XColor *)Xmalloc(sizeof(XColor) * _xcc->numColors);

retryGray:

/* Our increment will create a calculatable interval of grays. */

	dinc = 65535.0/(_xcc->numColors-1);

	clrs = cstart;
	for(i=0;i<_xcc->numColors;i++)
	{
		clrs->red = clrs->blue = clrs->green = dinc * i;
		if (!XAllocColor(_xcc->dpy, _xcc->colormap, clrs))
		{
			XFreeColors(_xcc->dpy, _xcc->colormap, _xcc->CLUT, i, 0);

			i--;
			_xcc->numColors = (i < _xcc->numColors/2) ? i : _xcc->numColors/2;

			if (_xcc->numColors > 1)
				goto retryGray;
			else
			{
				XFree((char *)_xcc->CLUT);
				_xcc->CLUT = NULL;
				_initBW(_xcc);
				XFree((char *)cstart);
				return;
			}
		}
		_xcc->CLUT[i] = clrs++->pixel;
	}

	XFree((char *)cstart);

/* Fill in our "virtual" standard colormap, only using the red fields. */

	_xcc->stdCmap.colormap = _xcc->colormap;
	_xcc->stdCmap.base_pixel = 0;
	_xcc->stdCmap.red_max = _xcc->numColors-1;
	_xcc->stdCmap.green_max = 0;
	_xcc->stdCmap.blue_max = 0;
	_xcc->stdCmap.red_mult = 1;
	_xcc->stdCmap.green_mult = _xcc->stdCmap.blue_mult = 0;

To get a pixel value:

	_red = _red * 0.3 + _green * 0.59 + _blue * 0.11;
	_green = 0;
	_blue = 0;

	return _xcc->CLUT[_xcc->stdCmap.base_pixel
		+ ((_red * _xcc->stdCmap.red_max)/0xFFFF) * _xcc->stdCmap.red_mult
		+ ((_green * _xcc->stdCmap.green_max)/0xFFFF) * _xcc->stdCmap.green_mult
		+ ((_blue * _xcc->stdCmap.blue_max)/0xFFFF) * _xcc->stdCmap.blue_mult];

The same calculation as the StaticColor/PseudoColor except for the modified _red, _green and _blue values into one gray value using NTSC conversion. Note that the green and blue are still used, as this will allow us to have code that falls through to the StaticColor/PseudoColor case in a C switch statement.

One quick note for 1 bit StaticColor (mono): If you look at the above pixel calculation closely you will see that due to round-off any non-white RGB combination will be converted to the black pixel entry, which probably is not desirable. Instead the XCC sets up a special case for monocolor, again using NTSC conversion:

	value = (double)_red/65535.0 * 0.3 + (double)_green/65535.0 * 0.59 + (double)_blue/65535.0 * 0.11;
	if (value > 0.5)
		return _xcc->whitePixel; /* previously allocated */
	else
		return _xcc->blackPixel; /* previously allocated */

In the mono case no "virtual" colormap is needed.

4. TrueColor and DirectColor

Many programs handle TrueColor since allocating a color is really just shifting bits of the red, green and blue components appropriately into a pixel and requires no trips to the server. DirectColor, on the other hand, strikes fear even into the hearts of experienced programmers. The secret to using DirectColor is to set up its colormap so that it looks like a TrueColor colormap. In both cases, no "virtual" standard colormap is needed since all the information we need must be calculated.

First, to set up TrueColor for the XCC:

	rmask = _xcc->masks.red = _xcc->visualInfo->red_mask;
	_xcc->shifts.red = 0;
	_xcc->bits.red = 0;
	while (!(rmask & 1))
	{
		rmask >>= 1;
		_xcc->shifts.red++;
	}
	while((rmask & 1))
	{
		rmask >>= 1;
		_xcc->bits.red++;
	}

This code calculates what bit positions red will occupy in a pixel, and how many bits describe red in that pixel. This is repeated for green and blue. To calculate a pixel:

	_red >>= 16 - _xcc->bits.red;
	_green >>= 16 - _xcc->bits.green;
	_blue >>= 16 - _xcc->bits.blue;

	return ((_red << _xcc->shifts.red) & _xcc->masks.red) 
		| ((_green << _xcc->shifts.green) & _xcc->masks.green)
		| ((_blue << _xcc->shifts.blue) & _xcc->masks.blue);

Now, for DirectColor we start by the same initialization we did for TrueColor to get our bits and masks. Next we need to allocate our colors and store them into a lookup table since which entries we will be given is unknown.

	rval = _xcc->visualInfo->red_mask;
	gval = _xcc->visualInfo->green_mask;
	bval = _xcc->visualInfo->blue_mask;

	while(!(rval & 1))
		rval >>= 1;
	while(!(gval & 1))
		gval >>= 1;
	while(!(bval & 1))
		bval >>= 1;

	rtable = (unsigned long *)Xmalloc(sizeof(unsigned long) * (rval+1));
	gtable = (unsigned long *)Xmalloc(sizeof(unsigned long) * (gval+1));
	btable = (unsigned long *)Xmalloc(sizeof(unsigned long) * (bval+1));

/* 
** Max entry is the red, green or blue component that can has the most
** bits.  These are not always the same, for instance in 8 bit DirectColor
** where the bits are usually 3/3/2 
*/

	_xcc->maxEntry = (rval > gval) ? rval : gval;
	_xcc->maxEntry = (_xcc->maxEntry > bval) ? _xcc->maxEntry : bval;

	cstart = (XColor *)Xmalloc(sizeof(XColor) * (_xcc->maxEntry+1));
	_xcc->CLUT = (unsigned long *)Xmalloc(sizeof(unsigned long) * (_xcc->maxEntry+1));

retrydirect:

/*
** Ramp each of the components from black to maximum intensity of
** that component.
*/

	for(n=0;n<=rval;n++)
		rtable[n] = rval ? 65535.0/rval * n : 0;
	for(n=0;n<=gval;n++)
		gtable[n] = gval ? 65535.0/gval * n : 0;
	for(n=0;n<=bval;n++)
		btable[n] = bval ? 65535.0/bval * n : 0;

	_xcc->maxEntry = (rval > gval) ? rval : gval;
	_xcc->maxEntry = (_xcc->maxEntry > bval) ? _xcc->maxEntry : bval;

	count = 0;
	clrs = cstart;
	_xcc->numColors = (bval + 1) * (gval + 1) * (rval + 1);

/*
** This is the part that throws most people.  We are really
** allocating the bits in the entry, not the color in the entry,
** which is done indirectly.  For instance allocating (0, 1, 4)
** and (8, 16, 32) lets us use the colors (0, 1, 4), (0, 1, 32),
** (8, 1, 4), etc.  Thus for 8 bit DirectColor with a 3/3/2 we
** only need to allocate 8 colors.  For 24 bit TrueColor we only
** need to allocate 256 colors.
*/

	for(n=0;n<=_xcc->maxEntry;n++)
	{
		dinc = (double)n/(double)_xcc->maxEntry;
		clrs->red = rtable[(int)(dinc * rval)];
		clrs->green = gtable[(int)(dinc * gval)];
		clrs->blue = btable[(int)(dinc * bval)];
/*
** Allocate the color and store the pixel in the CLUT
*/
		if (XAllocColor(_xcc->dpy, _xcc->colormap, clrs))
		{
			_xcc->CLUT[count++] = clrs->pixel;
			clrs++;
		}
		else
		{
/*
** On allocation failure, we will remove 1 bit of information from
** each component and try again.  This was a judgement call on my
** part, and you may want to do something different, like remove 
** one bit from blue first, then green, then red, and cycle that way.
*/
			XFreeColors(_xcc->dpy, _xcc->colormap, _xcc->CLUT, count, 0);

			bval >>= 1;
			gval >>= 1;
			rval >>= 1;

			_xcc->masks.red = (_xcc->masks.red >> 1) & _xcc->visualInfo->red_mask;
			_xcc->masks.green = (_xcc->masks.green >> 1) & _xcc->visualInfo->green_mask;
			_xcc->masks.blue = (_xcc->masks.green >> 1) & _xcc->visualInfo->blue_mask;

			_xcc->shifts.red++;
			_xcc->shifts.green++;
			_xcc->shifts.blue++;

			_xcc->bits.red--;
			_xcc->bits.green--;
			_xcc->bits.blue--;

			 _xcc->numColors = (bval + 1) * (gval + 1) * (rval + 1);

			if (_xcc->numColors > 1)
				goto retrydirect;
			else
			{
				XFree((char *)_xcc->CLUT);
				_xcc->CLUT = NULL;
				_initBW(_xcc);
				break;
			}
		}
	}

A DirectColor pixel is calculated by:

	return (_xcc->CLUT[(int)((_red * _xcc->maxEntry)/65535)] & _xcc->masks.red)
		| (_xcc->CLUT[(int)((_green * _xcc->maxEntry)/65535)] & _xcc->masks.green)
		| (_xcc->CLUT[(int)((_blue * _xcc->maxEntry)/65535)] & _xcc->masks.blue);

A quick note on DirectColor allocation. If any color has already been allocated in the colormap you immediately lose one bit that the XCC can allocate, unless you are lucky enough that that value falls within the XCC's mathematically calculated ramp. This may not seem bad at first, since we can always create a private colormap and use all the entries. But there is still a problem! Both the Xt color allocation and Motif color allocation routines just call XAllocColor() on the current colormap. While the Xt mechanism is a resource converter and can be overridden, unfortunately the Motif (as of 1.2.4) cannot. With Motif 3D borders if any widget allocates colors before the XCC is initialized, those bits are probably unusable. If a widget allocates colors after the XCC is initialized, unless they match exactly what the XCC has allocated, the widget will probably be black and white. In the PseudoColor case there were some entries left over, so this wasn't really a problem, and we could always start our GrayScale case with a less than max allocation number. For DirectColor we lose bits, which for 24 bit this is not too bad, but for anything less it becomes much worse. It is up to the programmer to decide which is more desirable, or to modify the DirectColor algorithm to take this into account.


24 bit DirectColor 8 bit greedy PseudoColor, 125 colors 8 bit XA_RGB_GRAY_MAP
static char *ColorList[] =
{
    "White", "Red", "Green", "Blue", "Yellow", "Violet",
    "Cyan", "Orange", "Brown", "LightSkyBlue", "DarkSeaGreen", "gold",
    "salmon", "coral", "orchid", "seashell", "YellowGreen", "RosyBrown",
    "#RC63F9", "#339A36", "#5F8D09", "Black",
};

#define NUMBER(a) (sizeof(a)/sizeof(a[0]))

static void DrawIt(_w, _nil, _event)
Widget _w;
XtPointer _nil;
XEvent *_event;
{
    Pixel pix, backPix;
    XColor color;
    int x, y;
    int i;
    double ginc, gr;

    ginc = 65535.0/(NUMBER(ColorList) * TheTextHeight);

    for(i=0,gr=0;i < NUMBER(ColorList)*TheTextHeight;i++,gr+=ginc)
    {
        pix = XCCGetPixel(TheXCC, (int)gr, (int)gr, (int)gr);
        XSetForeground(XtDisplay(_w), TheGC, pix);
        XDrawLine(XtDisplay(_w), XtWindow(_w), TheGC,
            0, i, 256, i);
    }

    y = TheFont -> ascent;
    x = 0;
    for(i=0,gr=0;i < NUMBER(ColorList);i++,gr+=ginc)
    {
        color.red = color.blue = color.green = 0;
        XParseColor(XtDisplay(_w), XCCGetColormap(TheXCC),
            ColorList[i], &color);

        pix = XCCGetPixel(TheXCC, color.red, color.green, color.blue);
        XSetForeground(XtDisplay(_w), TheGC, pix);
        XDrawString(XtDisplay(_w), XtWindow(_w), TheGC,
            x, y, ColorList[i], strlen(ColorList[i]));
        y += TheTextHeight;
    }

}


Final Word

There will always be programs that need to manipulate colormaps, or to allocate a large portion (or the entire) colormap for their own use. But many programs do not have that requirement, and are only interested in getting a pixel in return for an RGB triplet. The X Color Context and the ideas behind it should offer this to any X programmer, and getting your application to run on any X display no longer has to be a daunting task.