Gamma-corrected LCD-filtered text rendering, or
alpha blending two sRGB colors in linear color space for displaying on a sRGB device

Update! Alpha Correction can be used to simulate sRGB blending without sRGB blending primitive.

To view this demo, you need TFT display with RGB subpixel order. It works in Firefox and Chrome, and maybe in some other compatible browser. Here's Linux and Windows 7 side-by-side, if you can't see the demo live. OS X font rendering results in all of the AppleFontSmoothing levels are discussed here. The problem I'm talking about is with the text in the bottom box that shows significant color fringing when Linux renders it. The rest of the page deals with why this happens and what can be done about it.

Executive summary, with pictures

The technical problem is missing colorspace conversion during alpha blending. Libraries such as freetype generate coverage data of the glyphs quantized to 8 bits, so a value such as 0x80 from freetype means "50 % of this pixel is foreground and 50 % is background". That is to say: the visible light emitted by the pixel on screen should be a 50 % blend between the intensities of the colors as they would appear on screen surface unblended. However, RGB component values do not have a linear relationship with light intensities on the screen. In simplest possible terms, the average of white and black color (in physical light intensity terms) is not r=g=b=128 but r=g=b=188. To account for this nonlinearity, we must do gamma correction.

The simplest way to do the gamma correction is to mathematically approximate the component-to-light-intensity formula, which is approximately a power law, and then perform the blending, and then perform the power law in reverse, so that the display will then apply the power law again when it displays the color. However, multiple conversions like this are time-consuming, inconvenient and result in loss of accuracy. (Result is still more accurate than you would get from ignoring the gamma entirely, though!) From a programmer point of view, a sane thing to do would be to begin using linear color representations everywhere, and to spend more bits at the dark intensities. As it turns out, there's actually a specification that does just this, called scRGB(16).

In this demo, I am simply employing gamma correction between single-color foreground and background, where the foreground is the text color and the background is whatever the text is on. You can control the third example, where I have chosen the alphablending case of opposite colors that personally annoys me the most. Just set gamma = 1.0 in the control form to emulate typical Linux result, and see that missing gamma correction causes color fringing, excessive darkening and/or smudginess at curves.

Javascript (sRGB blend)

Javascript (Alpha Correction blend)

Browser

1
2
3

Control

Phrase:
Font:
Gamma:
Background:
Foreground:

Walkthrough of the JavaScript code

  1. The text is rendered 3x wider than normal. This gives me the real pixels to use for each individual R, G and B component. I use the browser to print the text on canvas, then read the pixels back for processing.
  2. To eliminate color fringing, a simple 3-pixel moving average is calculated across the image. This ensures that narrow details, or lines whose widths differ from multiples of 3 do not come out colored.
  3. The text is rendered against white and black background simply by calculating the appropriate gamma correction. A general screenspace blending goes according to the following formula, repeated for all color components (input1, input2, and alpha all change for every subpixel! This is called "component alpha"):
   #define GAMMA 2.2

   /* input1 = source color 1, 0 .. 1
    * input2 = source color 2, 0 .. 1
    * alpha = alpha blend factor, 0 .. 1
    */

   // uncorrected formula (same as GAMMA=1.0):
   // double output = input1 * alpha + input2 * (1.0 - alpha);

   // corrected formula:
   double tmp = pow(input1, GAMMA) * alpha + pow(input2, GAMMA) * (1.0 - alpha);
   double output = pow(tmp, 1.0/GAMMA);

Further discussion

White colors tend to bleed into dark colors in the eye, resulting in correctly calculated inverse images appearing perceptively inequal. For instance, you might think that the white-on-black text is much stronger than the black-on-white text. If you are very suspectible to this effect, it unfortunately means that you can not fully trust what your eyes are telling you, but need to mentally compensate for this effect. (The code is still correct, even if the results might not subjectively please you.)

You can test your system's gamma here. Technically there is no single exponent value that converts between the sRGB and linear light color spaces, but the error caused with the 2.2 approximation is fairly small for this use case. Feel free to use official sRGB conversion formulas in production applications.

Bonus: an excellent demo about related problem that occurs with most image scaling software.

Checklist of linux font rendering issues

  1. hinting: turn off via gnome-tweak-tool or equivalent. No hinting at all is required when gamma correcting works properly in sense that color fringing doesn't occur. Too much hinting in fact looks bad because in general the hints may have been designed for the incorrect beldning in mind.
  2. default LCD filter in cairo: IMHO only the FIR3 aka FreeType's "light" filter should ever be used. This can be changed in fontconfig.
  3. xrender: need to use software rendering for now, then use modified pixman with sRGB surface support and make clients use these surface types when blending as the source and destination bitmap types. The font surface itself acts as a mask for the blending operator, and should be in linear color space, as that is how freetype defines it. Component alpha mode should be used with the mask, and the alpha of the Cairo bitmap ignored.