premultiplied alpha without color loss in transparent pixels

tl;dr possible by sacrificing some floating point precision near 0.0

Pre-multiplied alpha is a pixel representation where the red, green and blue are scaled by the alpha component, a common component of making compositing, scaling, rotating and more behave correctly with regards to transparency.

The traditional wisdom is that a disadvantages of pre-multiplied alpha is loss of color information for transparent pixels:

In other words, color information of transparent pixels is lost in premultiplied alpha, as the conversion from premultiplied alpha to straight alpha is undefined for alpha equal to zero. Premultiplied alpha has some practical advantages over normal alpha blending because interpolation and filtering give correct results[citation needed]." 2018-08-26

GEGL uses the premultiplied babl-format "RaGaBaA float" for the resamplers used in rotate/scale/deform ops as well as in other color filtering or mixing operations like the layer modes of GIMP. Using pre-multiplied leads to simpler more efficient code, hower it also means that if we rotate or scale an image with transparency in GIMP-2.10 the color information of the transparent pixels is lost, and tools like anti-erase and using curves on the alpha channel no longer have recoverable color information this used to work in GIMP-2.8 before moving to GEGL.

The traditional wisdom of color loss stems from an era of imaging with integer components,  like u8 and u16. With floating point we have to opportunity to adapt our pre-multiplied representation slightly and trade off some precision for maintained color information. The precision of color data as alpha decreases and approaches 0.0 is still of high quality for very low opacity values and we're already using an opacity epsilon-cut off for floating point values in babl and GEGL to check if we are nearly enough transparent for some code paths. We can make use of such an epsiilon as a minimum alpha-value to consider and thus achieve color storage for our small, practically 0.0 alpha value.

This is the C code for pre-multiplying in this way:

/* make 16bit alpha being unaffected the trade off between alpha and color fidelity */
#define BABL_ALPHA_FLOOR   (1.0f/(1<<16))

static inline rgba_premultiply (const float *rgba, float *rgba_premultiplied)
 float alpha = rgba[3];
 if (alpha <= BABL_ALPHA_FLOOR)
     if (alpha >= 0.0f)
       alpha = BABL_ALPHA_FLOOR;
     else if (alpha >= -BABL_ALPHA_FLOOR)
       alpha = -BABL_ALPHA_FLOOR;

 rgba_premultiplied[0] = rgba[0] * alpha;
 rgba_premultiplied[1] = rgba[1] * alpha;
 rgba_premultiplied[2] = rgba[2] * alpha;
 rgba_premultiplied[3] = alpha;

With this change alone we preserve color. In babl, GEGL, and GIMP we also want alpha in of 0.0 to come out exactly as 0.0, and for our inverse conversion we map exactly BABL_ALPHA_FLOOR back to 0.0, while treating other values as we did previously:

static inline rgba_unpremultiply (const float *rgba_premultiplied, float *rgba)
 float alpha = rgba_premultiplied[3];
 if (alpha == 0.0f)
   rgba[0] = rgba[1] = rgba[2] = rgba[3] = 0.0f;
   float reciprocal = 1.0f / alpha;

   rgba[0] = rgba_premultiplied[0] * reciprocal;
   rgba[1] = rgba_premultiplied[1] * reciprocal;
   rgba[2] = rgba_premultiplied[2] * reciprocal;

   if (alpha == BABL_ALPHA_FLOOR || alpha == -BABL_ALPHA_FLOOR)
     alpha = 0.0f;

   rgba[3] = alpha;

The above also takes negative alpha into consideration, for a simpler implementation the parts considering negative alpha values could be dropped - but interpolation with negative values can potentially be useful so we strive to keep them. The choice of 1.0/(1<<16) makes the existing integer fast paths in babl for 16bit integer format premultiplication and unpremultiplication continue to be valid, since all the collapsed values fall in the 0 value of the format.

For buffers internal to GEGL/GIMP that end up converted back to non-premultiplied there is no issue; and the operations can be aware of the special handling of alpha 0 if neccesary; the adapted representation behaves well with code that is not written with it in mind making this a change that when done in babl, it trickles up as a feautre all the way to GIMP with pre-multiplied alpha now keeping color (also in 8bit and 16bit modes, since intermediate buffers are floating point).

If export operations writing out pre-multiplied data it will not contain exact 0.0 but it will be correctly interpreted by other software, we could potentially add support in babl for both types of premultiplied formats - at the cost of decreasing lookup performance also for other conversions, but see last part of next paragraph.

For import filters that do not follow we could potentially detect alpha values below 0.0 and treat such buffers slightly differently - the precision loss in alpha does not make it seem worth the extra complexity, since 16bit precision is far better than human visual perceptual fidelity.

This type of premultiplied alpha is already in babl master for testing by those who run babl/GEGL/GIMP from git, next time a babl release occurs (triggered by either GEGL or GIMP making a release) these changes and other improvements will be part of it.

Update June-2019: This topic has now been revisited, leaving the alpha channel unchanged, and doing good things for both separate and associated alpha see this commit for further details: