GdkRGB

The next step up in the drawing hierarchy is the GdkRGB drawing API. Although technically this is still part of GDK, we consider it separately here because unlike most of GDK, which is a thin wrapper, GdkRGB is pure added value and doesn't wrap any specific part of Xlib.

The name "GdkRGB" sounds like it might refer to some sort of object or structure. This is not the case. GdkRGB is a unified set of functions for drawing on GdkDrawable surfaces-that is, GdkWindow or GdkPixmap. These functions help manage colors for you, behind the scenes. By drawing with GdkRGB, you won't have to deal with visuals and color maps.

The RGB Buffer

GdkRGB stores all of its pixel data in 24-bit color arrays, and then converts the colors and images to the target visual and color map when it renders them to the drawable. Thus it shields you from the hassle of dealing with color lookups and buffer management (remember that each color depth uses a different amount of memory to hold a single on-screen pixel). With GdkRGB, you always work with the same type of 24-bit color array, so you won't have to create special rendering routines for each type of visual.

As the name "GdkRGB" suggests, each pixel contains a red, a green, and a blue element, each one 8 bits in size, implying that each color can have an intensity value ranging from 0 to 255. The values of these three elements are packed consecutively into three guchar elements of the RGB buffer array. You might guess from this arrangement that the size of the RGB buffer is the number of pixels in the image, multiplied by 3. Thus a 25 40 image would correspond to a guchar array with 3,000 (25 40 3) elements. However, this is not the case.

The dimensions of the buffer's image affect the exact size allocated for the buffer. To help optimize access to the later parts of the image buffer, each full row of pixels in the buffer is aligned on a 4-byte (or 4-guchar) boundary. This design typically leads to slightly faster memory lookups at the hardware level, and since graphics rendering consists largely of memory copies in long, repeti- tive loops, any optimization within these innermost loops can make quite a difference. To keep things simple and customizable, each buffer is assigned a row stride, the exact length that each row takes up in the buffer array. The row stride is the sum of all the data bytes in a single row of pixels, plus any extra bytes needed to pad the row to 4-byte alignment.

Let's take our 25 40 image as an example. Each 25-pixel row uses 75 bytes of data. To make sure the next row starts on a 4-byte boundary, at 76 bytes, we have to add a single unused byte of padding to the end of each row. The result is that we can calculate the offset to any row by multiplying by 4 rather than by 3,1 so we have a buffer size of 3,040 (76 40) instead of 3,000. Figure 10.4 illustrates the concept of row strides.

Figure 10-4. RGB Row Strides

It's not easy to aesthetically convert a 24-bit color buffer into a 16-bit color buffer, and it is even harder to convert it into an 8-bit (or lower) color buffer. The quality of your results will vary wildly, depending on the intricacies of the algorithms you use. For each pixel in your 24-bit image, you will have to search the target visual's color map for an appropriate color. The simplest, and probably fastest, approach is to find the nearest match. When going from 24-bit to 16-bit, you won't lose much quality because you still have 64K colors to choose from.

The real problems come when you try to degrade your 16-million-color image down to 256 colors. Simple closest-match algorithms can't do justice to the image and will lead to severe banding. This sharp line of transition between color hues shows up most often in conversions of a smooth color gradient to a lower color depth.

A more sophisticated color conversion algorithm can smooth out this abrupt transition by dithering colors. Dithering introduces an element of fuzziness to color conversions. Rather than taking into account only one pixel at a time, a dithering algorithm looks at surrounding pixels and tries to come up with a combination of adjacent pixel colors that the human eye will mistake for an intermediate color. A common application of dithering (especially in newspapers) is the fine-grained black-and-white checkerboard, which appears from a distance to be a flat gray. Sophisticated dithering techniques can do wonders to maintain image quality in conversions to a lower color depth.

This discussion about color conversions is intended to illustrate the amount of work that goes on behind the scenes in GdkRGB. If it weren't for GdkRGB, you would have to implement all these conversion routines yourself to produce consistent-looking images across the full range of color depths and visuals. By analogy, if you've never bicycled from Detroit to Albuquerque, you may not appreciate how much more convenient it is to fly there. GdkRGB makes the arduous journey from one color depth to another quick and easy.

If you wish to use GdkRGB in your application, you will need to initialize it, as shown here, before you call any of its functions:

void gdk_rgb_init(  );
        

GdkRGB makes use of global variables that need to be loaded in order for it to function properly. Failure to initialize GdkRGB can result in application crashes, so don't forget this step! If you are using GdkRGB indirectly, through a wrapper such as GNOME or gdk-pixbuf (see Section 10.5), you shouldn't have to initialize it, because the wrapper will take care of that for you.

If you get into trouble and need help figuring out why your calls to GdkRGB aren't doing what you think they should be doing, you can turn on the verbose debugging mode by calling the following function with a value of TRUE:

void gdk_rgb_set_verbose(gboolean verbose);
        

You can turn it back off by calling the function again with a FALSE. It may not tell you exactly what's going on, but if you're lucky, it will give you a hint or two.

Drawing Functions

The most impressive feature of the GdkRGB API is its set of drawing routines, each of which targets a GdkDrawable instance. The drawing routines are quite similar, differing primarily in the particular algorithms they use to transfer and downgrade the 24-bit colors into the target drawable.

In most cases you will be able to get by with using only the first drawing function, gdk_draw_rgb_image( ). This function, like the others, copies a rectangular block of pixels from anywhere inside the RGB buffer into the target drawable, automatically converting it to the proper visual. You must also supply the dithering style, a pointer to the RGB buffer, and your buffer's row stride so that GdkRGB knows exactly where to wrap the pixel rows:

Void gdk_draw_rgb_image(GdkDrawable *drawable,
    GdkGC *gc, gint x, gint y, gint width, gint height,
    GdkRgbDither dith, guchar *rgb_buf, gint rowstride);
        

The dith parameter can be GDK_RGB_DITHER_NONE for no dithering (remember our discussion about banding), GDK_RGB_DITHER_NORMAL to tell GdkRGB to dither only when rendering to 8-bit color visuals, or GDK_RGB_DITHER_MAX to dither on both 8-bit and 16-bit visuals. Usually the reduction in performance that accompanies dithering on a 16-bit color drawable isn't worth the slight improvement in appearance, so your best bet is GDK_RGB_DITHER_NORMAL. The gc parameter points to a GdkGC structure, which should hold any contextual information for the drawing operation (see Section 2.2.4).

GdkRGB has four additional specializations of the basic gdk_draw_rgb_image( ) function. The first two constrain the rendering to a specific style of visual. gdk_draw_gray_image( ) uses only shades of gray to render to the drawable, regardless of the target visual. You can use gdk_draw_indexed_image( ) to force GdkRGB to render according to a color map you specify. Rather than using the native GDK API for creating and populating your color map, GdkRGB provides an easy-to-use convenience function, gdk_rgb_cmap_new( ). This color map is allocated dynamically, so you must free it when you're done, using gdk_rgb_cmap_free( ):

void gdk_draw_gray_image(GdkDrawable *drawable,
    GdkGC *gc, gint x, gint y, gint width, gint height,
    GdkRgbDither dith, guchar *buf, gint rowstride);
void gdk_draw_indexed_image (GdkDrawable *drawable,
    GdkGC *gc, gint x, gint y, gint width, gint height,
    GdkRgbDither dith, guchar *buf, gint rowstride,
    GdkRgbCmap *cmap);
GdkRgbCmap* gdk_rgb_cmap_new (guint32 *colors, gint n_colors);
void gdk_rgb_cmap_free (GdkRgbCmap *cmap);
        

The other two drawing functions have a less visible effect on the image. The gdk_draw_rgb_image_dithalign( ) function is good for rendering parts of a dithered image, particularly to a scrollable drawing surface. In order for the dithering algorithms to match up seamlessly, without leaving noticeable transition lines, you must supply two extra parameters-xdith and ydith-the offset of the scrolled image from its base, unscrolled position. Supplying consistent values of xdith and ydith ensures that multiple passes of the dithering algorithm will generate the same pattern, even when the rendering region shifts.

The final drawing function, gdk_draw_rgb_32_image( ), is more of a friendly curiosity than a critical supplement. It attempts to align every 3-byte color value in the RGB buffer along a 4-byte boundary to improve memory access time. Rather than just aligning the final pixel of a row, it aligns every pixel in the image. In practice, however, the added speed is not enough to justify the significant increase in memory consumption.

void gdk_draw_rgb_image_dithalign(GdkDrawable *drawable,
    GdkGC *gc, gint x, gint y, gint width, gint height,
    GdkRgbDither dith, guchar *rgb_buf, gint rowstride,
    gint xdith, gint ydith);
void gdk_draw_rgb_32_image(GdkDrawable *drawable,
    GdkGC *gc, gint x, gint y, gint width, gint height,
    GdkRgbDither dith, guchar *buf, gint rowstride);
        

Color Management

As we learned in Section 10.1, color management can be a tricky process at the lower levels. One of the most tedious tasks is setting up the visual and the color map for a given display. A simple mistake inside the complex color man- agement code can cause your application to crash and bail out when it becomes impossible to render to a visual in a sane way, leading to X's nebulous "Bad Match" errors.

Another helpful abstraction service that GdkRGB provides is the automatic generation of a rational visual and color map for whichever display the application runs on. If one user runs the application on an 8-bit pseudocolor display, GdkRGB detects that and creates an appropriate indexed color map. If another user runs the same application on a 24-bit color display, GdkRGB creates a TrueColor color map and its corresponding visual instead. You can access this customized color map and visual at any time after gdk_rgb_init( ) has run, by calling these two functions:

GdkColormap* gdk_rgb_get_cmap(  );
GdkVisual* gdk_rgb_get_visual(  );
        

You can force a widget to use GdkRGB's color map and visual by pushing it onto a pair of global stacks (really a pair of singly linked lists used to simulate the stacks) in GTK+ and then popping it back off when you're done. You need to push it only for the time it takes to create the widget. Once created, the widget will continue to use that color map and visual for the rest of its life span; all of the widget's children will inherit them too. The common practice is to push them, create the top-level widget, and then pop them immediately afterward:

gtk_widget_push_visual(gdk_rgb_get_visual(  ));
gtk_widget_push_colormap(gdk_rgb_get_cmap(  ));
widget = gtk_widget_new(...);
gtk_widget_pop_visual(  );
gtk_widget_pop_colormap(  );
        

GdkRGB also supplies a convenience function for looking up values in this GdkColormap. If you feed it a 24-bit RGB color value, it will return that color's corresponding value in the color map. You can then use the returned color in the various raw GDK functions. Two other functions allow you to set the default foreground and background colors of the graphics context with RGB colors, rather than having to perform the lookup first, as you would normally have to do with the gdk_gc_set_fore/background( ) functions.

gulong gdk_rgb_xpixel_from_rgb(guint32 rgb);
void gdk_rgb_gc_set_foreground(GdkGC *gc, guint32 rgb);
void gdk_rgb_gc_set_background(GdkGC *gc, guint32 rgb);
        

Finally, you can use gdk_rgb_ditherable( ) to find out whether or not GdkRGB's visual is ditherable, and act accordingly. You might use this function to decide whether to call gdk_draw_rgb_image( ) or gdk_draw_rgb_image_ditherable( ):

gboolean gdk_rgb_ditherable(  );