[cairo] RFC: More accurate color conversion

Søren Sandmann sandmann at cs.au.dk
Fri Oct 4 18:02:53 PDT 2013


Hi,

cairo-color.c contains the following function:

    /* Convert a double in [0.0, 1.0] to an integer in [0, 65535]
     * The conversion is designed to divide the input range into 65536
     * equally-sized regions. This is achieved by multiplying by 65536 and
     * then special-casing the result of an input value of 1.0 so that it
     * maps to 65535 instead of 65536.
     */
    uint16_t
    _cairo_color_double_to_short (double d)
    {
        uint32_t i;
        i = (uint32_t) (d * 65536);
        i -= (i >> 16);
        return i;
    }

that serves as the canonical way to convert a double-precision color to
a uint16_t normalized color. As the comment says, the function divides
the input range into 65536 equal sized regions, and then it maps each
region to an integer.

The function was written following this mail from Carl:

    http://lists.freedesktop.org/archives/cairo/2006-August/007709.html

in which among other things he argues that an alternative algorithm:

    i = floor (f * 65535 + 0.5)

has the drawback that "the regions that map to values of 0 and N are
half the size of the regions that map to all other values". It is true
that the alternative has this property, but I want to argue that this is
a feature, not a bug.

If the resulting integers were arbitrary sample points, it would make
sense to space them evenly across the input range, ie., to divide the
input range into 65536 regions and then position the sample points in
the middle of each range.

But the output integers have inherent values, where 0x0000 corresponds
to 0.0, 0xffff corresponds to 1.0 and 0x0001 corresponds to
1/65535.0. That is not an arbitrary convention -- when these integers
are stored in framebuffers or .PNG files they are (at least in
principle) the assumption is that they correspond to these values. The
reason for choosing the sample points in this way, is that it is
important to be able to represent the particular values 0.0 and 1.0
exactly.

The correct way to convert from floating point to integers is then to
pick the integer i such that its corresponding value is as close as
possible to the given floating point value. In other words, the
conversion should be done by rounding, and not by dividing the input
range into equal-sized sections.

Therefore I'd like to propose this patch:

    diff --git a/src/cairo-color.c b/src/cairo-color.c
    index 9c85255..c2a11a1 100644
    --- a/src/cairo-color.c
    +++ b/src/cairo-color.c
    @@ -78,18 +78,13 @@ _cairo_stock_color (cairo_stock_t stock)
     }
     
     /* Convert a double in [0.0, 1.0] to an integer in [0, 65535]
    - * The conversion is designed to divide the input range into 65536
    - * equally-sized regions. This is achieved by multiplying by 65536 and
    - * then special-casing the result of an input value of 1.0 so that it
    - * maps to 65535 instead of 65536.
    + * The conversion is designed to choose the integer i such that
    + * i / 65535.0 is as close as possible to the input value.
      */
     uint16_t
     _cairo_color_double_to_short (double d)
     {
    -    uint32_t i;
    -    i = (uint32_t) (d * 65536);
    -    i -= (i >> 16);
    -    return i;
    +    return d * 65535.0 + 0.5;
     }
     
     static void

The same thing should be done in pixman eventually, but the patch there
is going to be a bit more involved for various reasons.

(Note: I'm not (at the moment) proposing to do rounding instead of bit
replication for conversions between integer pixels).


Søren


More information about the cairo mailing list