The PNG Guide is an eBook based on Greg Roelofs' book, originally published by O'Reilly.

Gamma and Color Correction

Since this routine is also where any gamma and color correction (recall Chapter 10, "Gamma Correction and Precision Color") would take place, we should step back a moment and look at how the main program deals with that. First I have a confession: I did not attempt any color correction. (Truly, I am scum.) But this does not excuse you, the reader, from supporting it, at least in higher-end applications! The X Window System's base library, Xlib, has included the X Color Management System since X11R5; it is accessed via the Xcms functions, an extensive API supporting everything from color-space conversion to gamut compression. Apple supports the ColorSync system on the Macintosh and will be releasing a version for Windows. And Microsoft, if not already supporting the sRGB color space natively in recent releases of Windows, certainly can be assumed to do so in coming releases; they and Hewlett-Packard collaborated on the original sRGB proposal.

But where color correction can be a little tricky, gamma correction is quite straightforward. All one needs is the ``gamma'' value (exponent) of the user's display system and that of the PNG file itself. If the PNG file does not include a gAMA or sRGB chunk, there is little to be done except perhaps ask the user for a best-guess value; a PNG decoder is likely to do more harm than good if it attempts to guess on its own. We will simply forego any attempt at gamma correction, in that case. But on the assumption that most PNG files will be well behaved and include gamma information, we included the following code at the beginning of the main program:

    double LUT_exponent;
    double CRT_exponent = 2.2;
    double default_display_exponent;

#if defined(NeXT)
    LUT_exponent = 1.0 / 2.2;
    if (some_next_function_that_returns_gamma(&next_gamma))
        LUT_exponent = 1.0 / next_gamma;
#elif defined(sgi)
    LUT_exponent = 1.0 / 1.7;
    /* there doesn't seem to be any documented function to
     * get the "gamma" value, so we do it the hard way */
    infile = fopen("/etc/config/system.glGammaVal", "r");
    if (infile) {
        double sgi_gamma;

        fgets(fooline, 80, infile);
        sgi_gamma = atof(fooline);
        if (sgi_gamma > 0.0)
            LUT_exponent = 1.0 / sgi_gamma;
#elif defined(Macintosh)
    LUT_exponent = 1.8 / 2.61;
    if (some_mac_function_that_returns_gamma(&mac_gamma))
        LUT_exponent = mac_gamma / 2.61;
    LUT_exponent = 1.0;   /* assume no LUT:  most PCs */

    default_display_exponent = LUT_exponent * CRT_exponent;

The goal here is to make a reasonably well informed guess as to the overall display system's exponent (``gamma''), which, as you'll recall from Chapter 10, "Gamma Correction and Precision Color", is the product of the lookup table's exponent and that of the monitor. Essentially all monitors have an exponent of 2.2, so I've assumed that throughout. And almost all PCs and many workstations forego the lookup table (LUT), effectively giving them a LUT exponent of 1.0; the result is that their overall display-system exponent is 2.2. This is reflected by the last line in the ifdef block.

A few well-known systems have LUT exponents quite different from 1.0. The most extreme of these is the NeXT cube (and subsequent noncubic models), which has a lookup table with a 1/2.2 exponent, resulting in an overall exponent of 1.0 (i.e., it has a ``linear transfer function''). Although some third-party utilities can modify the lookup table (with a ``gamma'' value whose inverse is the LUT exponent, as on SGI systems), there appears to be no system facility to do so and no portable method of determining what value a third-party panel might have loaded. So we assume 1.0 in all cases when the NeXT-specific macro NeXT is defined.

Silicon Graphics workstations and Macintoshes also have nonidentity lookup tables, but in both cases the LUT exponent can be varied by system utilities. Unfortunately, in both cases the value is varied via a parameter called ``gamma'' that matches neither the LUT exponent nor the other system's usage. On SGI machines, the ``gamma'' value is the inverse of the LUT exponent (as on the NeXT) and can be obtained either via a command (gamma) or from a system configuration file (/etc/config/system.glGammaVal); there is no documented method to retrieve the value directly via a system function call. Here we have used the file-based method. If we read it successfully, the overall system exponent is calculated accordingly; if not, we assume the default value used on factory-shipped SGI systems: ``gamma'' of 1.7, which implies a display-system exponent of 2.2/1.7, or 1.3. Note, however, that what is being determined is the exponent of the console attached to the system running the program, not necessarily that of the actual display. That is, X programs can display on remote systems, and the exponent of the remote display system might be anything. One could attempt to determine whether the display is local by checking the DISPLAY environment variable, but to do so correctly could involve several system calls (uname(), gethostbyname(), etc.) and is beyond the scope of this demo program. A user-level work-around is to set the SCREEN_GAMMA variable appropriately; I'll describe that in just a moment.

The Macintosh ``gamma'' value is proportional to the LUT exponent, but it is multiplied by an additional constant factor of 2.61. The default gamma is 1.8, leading to an overall exponent of (1.8/2.61) × 2.2, or 1.5. Since neither of the two front ends (X or Windows) is designed to work on a Mac, the code inside the Macintosh if-def (and the Macintosh macro itself) is intended for illustration only, not as a serious example of ready-to-compile code. Indeed, a standard component of Mac OS 8.5 is Apple's ColorSync color management system (also available as an add-on for earlier systems), which is the recommended way to handle both gamma and color correction on Macs.

It is entirely possible that the user has calibrated the display system more precisely than is reflected in the preceding code, or perhaps has a system unlike any of the ones we have described. The main program also gives the user the option of specifying the display system's exponent directly, either with an environment variable (SCREEN_GAMMA is suggested by the libpng documentation) or by direct input. For the latter, we have once again resorted to the simple expedient of a command-line option, but a more elegant program might pop up a dialog box of some sort, or even provide a calibration screen. In any case, our main program first checks for the environment variable:

    if ((p = getenv("SCREEN_GAMMA")) != NULL)
        display_exponent = atof(p);
        display_exponent = default_display_exponent;

If the variable is found, it is used; otherwise, the previously calculated default exponent is used. Then the program processes the command-line options and, if the -gamma option is found, its argument replaces all previously obtained values.

That turned out to be a moderately lengthy explanation of the demo program's approach to gamma correction (or, more specifically, to finding the correct value for the display system's exponent), mostly because of all the different ways the value can be found: system-specific educated guesses at the time of compilation, system-specific files or API calls at runtime, an environment variable, or direct user input. The actual code is only about 20 lines long.

Last Update: 2010-Nov-26