@Morgan_Hardwood @houz @paperdigits @patdavid
WARNING: long message ahead…
Yesterday evening I have finally managed to bypass the OSX UI color management and to send pixel values directly to screen… below I will try to describe how I got there.
Foreword 1: I am shocked by the Apple API documentation… I have looked into many Open Source libraries, made by volunteers during their limited spare time, and each single one I have checked had an API documentation that was 100 times better than Apple’s… it seems that in Cupertino they are making fun of their users!
One example for all: the documentation of the CGColorSpaceCreateDeviceRGB() function. This is the first time I see a function which is described by means of what this function is NOT doing:
Colors in a device-dependent color space are not transformed or otherwise modified when displayed on an output device…
… colors in a device color space often appear different when displayed on different output devices.
And, “cerise sur le gateau”, device color spaces are not recommended when color preservation is important.
Nowhere it is written what this function is good for… and I still don’t have a guess.
In the GTK+/CAIRO sources it is assumed that this function returns the current display profile, but I am pretty much convinced that it doens’t, for three main reasons:
- it is nowhere written in the documentation
- I cannot see how this could work in a dual-monitor set-up, as there is no way to tell the function the ID of the monitor for which we are asking the associated profile
- the CGColorSpace object has no associated ICC profile data, i.e. CGColorSpaceCopyICCProfile() returns NULL
Foreword 2: so far I only checked OSX 10.10 and a GTK2 application… things might be different in other OSX versions and/or with GTK3
Foreword 3: this is my understanding of how the underlying screen drawing works in the GTK+/CAIRO Quartz backend:
- the pixel data gets associated with a CGImage* object, which also stores the color profile for interpreting the pixel values
- the CGImage is drawn into a CGContext, which in turn contains the color profile associated with the corresponding output device
In such a scheme, the ICC conversion to the display profile is delegated to the underlying OSX graphics system, unless one creates a CGImage that is already associated with the display profile, so that no further color conversions are applied by the system.
Reading the GTK+/CAIRO sources, it seems that the developers assume that the display profile can be retrieved with the CGColorSpaceCreateDeviceRGB() function, but as far as I could check this doesn’t seem to be the case…
The first thing I did was to modify the GTK+/CAIRO sources by replacing all calls to CGColorSpaceCreateDeviceRGB() with a custom function CGColorSpaceCreateDeviceRGB_test() defined like this:
static CGColorSpaceRef CGColorSpaceCreateDeviceRGB_test()
{
return CGColorSpaceCreateDeviceRGB();
//return CGColorSpaceCreateWithName(kCGColorSpaceGenericRGBLinear);
}
In other words, with this function I can re-define the color profile that CAIRO assumes for the display. This profile is then used to create the CGImage objects that are used to draw pixels on screen.
The first interesting thing happens when I set the previous function to return CGColorSpaceCreateWithName(kCGColorSpaceGenericRGBLinear), thus telling the Quartz rendering engine that the image data is in linear gamma space (while in reality it is in sRGB colorspace). The displayed image is brighter, meaning that indeed the linear colorspace has been taken into account when converting the pixels to the display profile.
At this point, all we need is a correct way to pass the display profile to the CGImage objects created by GTK+/CAIRO. Retrieving the ICC profile of a given monitor is actually very simple:
int monitor = 0;
CGColorSpaceRef space = NULL;
space = CGDisplayCopyColorSpace (monitor);
This will return the ICC profile of the main monitor. In a multi-head setup, one has to first detect the monitor to which the application window is associated, for example using the code taken from gimp_widget_get_monitor().
So here is my new CGColorSpaceCreateDeviceRGB_test() function:
static CGColorSpaceRef CGColorSpaceCreateDeviceRGB_test()
{
int monitor = 0;
CGColorSpaceRef space = NULL;
space = CGDisplayCopyColorSpace (monitor);
return space;
//return CGColorSpaceCreateDeviceRGB();
//return CGColorSpaceCreateWithName(kCGColorSpaceGenericRGBLinear);
}
To test the new code, I have used a coloured gradient and displayed it in PhotoFlow, either using the correct monitor profile or associating a wide-gamut profile to my monitor (resulting in washed-out colors if color management is applied).
Here is the gradient in the standard configuration:
Note that both the gradient in PhotoFlow and the desktop image in the background show vivid colors.
Next I have selected a wide-gamut display profile, and opened PhotoFlow with the default GTK+/CAIRO libraries. Both the gradient and the desktop image have washed-out colors, because they are both color-managed in the same way:
Finally, I have opened PhotoFlow using the patched GTK+/CAIRO libraries. This time, the gradient in PhotoFlow has vivid colors, because the pixels values have been dent to screen directly without any further color conversion:
To conclude: the good news is that there is a relatively simple solution for bypassing the OSX UI color management and take control over the conversion to the display profile. However, this requires some relatively important patching of the GTK+/CAIRO sources, as well as a way to tell GTK+/CAIRO the ID of the monitor for which to retrieve the ICC profile.
How long it might take to have such patches accepted and properly implemented, I don’t know…