RGB 565 Color space

Introduction

We're all used to the RGB24 (888) colorspace, where each color component will take a value from 0-255 (8 bits), giving a total amount of 16 million different colors. This is good enough to represent most colors human eyes can see and is the standard in most desktops, is used by HTML, most images and editing software and that's why it's referred as True Color. Internally, for memory cacheline efficiency this is stored as 4 bytes instead of 3 and thus one byte is lost in exchange of extra access performance.

But some systems, specially embedded systems, might have limitations on video memory, memory bandwidth or others and saving 2 bytes is worth. For such systems, RGB16 (565) format is used, effectively reducing memory usage by half. This system cannot represent as many colors, being limited to 65 thousand colors and is referred as High Color. As one can see from its name, the green component has one extra bit to store more information as human eye can notice more variations of green than red or blue. This extra bit comes at extra cost as well, as some values are possible to represent in green but not in red and blue and thus gray might become green.

Evas is smart enough to render everything in 32bits, taking maximum precision and then downscaling colors to required color space as required, applying dithering techniques to make the result look better (no blocking/banded gradients, for example). However this all adds overhead and some systems might not be fast enough to handle it and then Evas provides the 16bpp engine.

Going 16bpp engine might be faster, but it is not as complete as the 32bpp and you're giving up color precision too soon, not being able to apply techniques such as dithering and results will look slightly worse.

Reducing 16bpp engine artifacts

The rendering artifacts can be reduced by various means. Some are automatic, but some require human/designer intervention. For instance Evas will automatically dither loaded images, avoiding it to contain bands/blocky gradients and shades. But that dithering might mean that colors will differ slightly on images than in solid fills.

Take as example a scene with top half being an image painted with color #cecece (206,206,206) and the bottom half being a rectangle with the same color. It would make sense to it look the same, but it does not! As this color cannot be represented in 16bpp, it will be reduced to #c8ccc8 (200, 204, 200), being green and not actually gray as required. As Evas will do dithering on image loading, the top part will be slightly better, being composed of lots of points with color up or down that value, while the bottom will be fully (200,204,200) color! Ugh oh!

You might imagine this situation is hard to happen, but one real example is to have the top part to provide a shadow as JPEG image, being from the top color to the gray color (both rectangles since it's faster to paint), ending as a solid gray to match. But if designer choose (206,206,206) it will be visible where the image finishes and the rectangle starts.

Solution is quite easy: ask designer to use #c8c8c8 (200, 200, 200) or #d0d0d0 (208, 208, 208) as the gray color.

To help you finding out the correct color, the following math and code can be used:

r_correct = (r_orig >> 3) << 3;
g_correct = (g_orig >> 2) << 2;
b_correct = (b_orig >> 3) << 3;
#!/usr/bin/python                                          

import sys

if len(sys.argv) != 2 and len(sys.argv) != 4:
    raise SystemExit("Usage: %s <color>" % sys.argv[0])

def parse_color(color):
    try:
        return int(color, 0)
    except ValueError:
        raise ValueError(("unsupported color component: %s, "
                          "use a valid integer value/representation.") % color)

def parse_from_tuple(components):
    if len(components) != 3:
        raise ValueError("components should be red, green and blue")
    r, g, b = components
    r = parse_color(r)
    g = parse_color(g)
    b = parse_color(b)
    return (r << 16) | (g << 8) | b

if len(sys.argv) == 2:
    color = sys.argv[1]
    if color.startswith("0x"):
        color = int(color, 16)
    elif color.startswith("#"):
        color = color.replace("#", "0x")
        color = int(color, 16)
    elif ',' in color:
        color = parse_from_tuple(color.split(','))
    else:
        try:
            color = int(color)
        except ValueError:
            raise ValueError(("unsupported color: %s, use either #rrggbb, "
                              "0xrrggbb or rr,gg,bb") % color)
else:
    color = parse_from_tuple(sys.argv[1:])

r = (color >> 16) & 0xff
g = (color >> 8) & 0xff
b = color & 0xff

print "Original color: %#06x (#%06x) (%02d, %02d, %02d)" % \
    (color, color, r, g, b)

dr = (r >> 3) << 3
dg = (g >> 2) << 2
db = (b >> 3) << 3

downcolor = (dr << 16) | (dg << 8) | db
print "16bpp color...: %#06x (#%06x) (%02d, %02d, %02d)" % \
    (downcolor, downcolor, dr, dg, db)