Implementing Bi-Level Transparency in MS-Windows

[Index]

Introduction

Bitmaps which have transparent pixels, otherwise known as clipmasked bitmaps, or bitmaps with bi-level transparency information, are useful constructs. They provide a simple interface to collections of pixels which are effectively non-rectangular.

A previous document describes an implementation of bi-level transparency in X-Windows. This document addresses the same concerns within the MS-Windows programming environment.

There are some complications when using MS-Windows. X-Windows has a simple method of using a 1 bit per pixel clipmask to restrict drawing operations, but in MS-Windows there is no corresponding method. Instead, multiple calls to the BitBlt function are needed, using a variety of raster operation codes, to achieve the desired effects.

Implementation

MS-Windows provides no direct scheme for restricting drawing to a shape defined by a bitmap. In X-Windows, such a scheme allows the filling of arbitrary-shaped regions with colour, or copying selected pixels from a bitmap.

Despite this omission, MS-Windows has a compensating feature which X-Windows does not: ternary raster operation code, the so-called ROP codes.

A ROP code is an integer used by the MS-Windows graphics drawing interface library (called GDI) to determine how source and destination pixels are combined with the currently selected drawing pattern. These three elements are usually denoted by their first initials, S, D or P.

The way GDI uses these ROP codes is quite interesting. There are 256 ROP codes, which is 2 to the power of (2 to the power of 3). The ROP code contains not only the 8-bit number which corresponds to the appropriate combination of S, D and P, but also includes another 16 bits of information which are used by GDI in an internal compiler to write a small piece of machine code which implements the appropriate graphical operation.

There are 256 ROP codes because of the combinations of S, D and P, as can be seen in this table:

	P Pattern      1 1 1 1 0 0 0 0
	S Source       1 1 0 0 1 1 0 0
	D Destination  1 0 1 0 1 0 1 0
	ROP Code       ? ? ? ? ? ? ? ?

The code is uniquely identified by the 0's and 1's that replace the question marks in the above table.

Interestingly, X-Windows does not have as many codes. It lists only 16 raster operation codes, because the currently selected pixel value (the equivalent of the Pattern field above) is not treated as a separate entity. Therefore, some of the techniques described by this document will only work on MS-Windows, not X-Windows.

The way to achieve transparent pixels in a Bitmap in MS-Windows is to perceive that with 256 ROP codes, it is possible to use a 1 bit per pixel bitmap to make pixels on a destination bitmap have a colour defined by a pattern. Following such an operation with a carefully chosen BitBlt operation can simulate the effect of a clipmask.

A pattern in MS-Windows parlance is a name for a small bitmap which is tiled across a window to produce a brush effect. For the purposes of this discussion we shall assume the pattern is filled with just one colour, so that the words 'pattern' and the letter 'P' will be synonymous with the current drawing colour.

MS-Windows clipmasks

Before defining a Bitmap structure, we must decide how bits should be organised inside a clipmask, which is an MS-Windsow BITMAP having a depth of 1 bit per pixel.

There are two ways we could define these bits: either 0 indicates opaque pixels, and 1 indicates transparent; or vice versa.

This discussion will assume we are using the former definition, that 0 indicates opaque, and 1 transparent. This is the opposite of X-Windows, but that really doesn't matter, since we are not exposing this definition to the outside world. Rather, it is encapsulated behind the interface of the Bitmap object itself and the drawing operations. We could, of course, choose instead to use the same definition as X-Windows, but as it turns out this is less convenient because it uses fewer of the ROP codes which have names.

In MS-Windows there are 256 ROP codes, but only a few of them are given names in the literature. These few are possibly implemented and tested better than the others since they are mentioned repeatedly in the programming texts for that platform. Although it would be perfectly possible to use the X-Windows definition of clipmask contents, doing so uses less of the named ROP codes, and therefore is less preferred than the above scheme. In practice, there is probably no difference in the quality of any of the ROP code, but all other things being equal, and given there is no urgent need to use the same clipmasking scheme as X-Windows, we prefer the approach given here. Were we to expose the clipmasking features of the drawing system to programmers, there may be a need to rethink this policy, but until then, using 0's for opaque and 1's for transparent suffices.

Implementing fill_rect in MS-Windows

The first function to implement is also the simplest, fill_rect, which fills a rectangle with the current drawing colour.

There are two cases to handle. Either the destination has no clipmask (and is thus fully opaque), or else it has a clipmask. The first case is simple, and reduces to a single PatBlt function call, while the second case is two PatBlt's to draw to the image first, then to the clipmask.

We wish the clipmask to record an accurate picture of which pixels in the Bitmap's image are transparent, so drawing to the Bitmap must cause the clipmask to become opaque wherever we've drawn. Since we are using 0's to indicate opaqueness in the clipmask, this amounts to a PatBlt with a ROP code of BLACKNESS. (BLACKNESS is a poor name, because it really indicates that pixels become zero wherever drawing occurs. Most MS-Windows applications use zero to indicate the colour black, hence the name.)

Here is the MS-Windows implementation of fill_rect, without client-side clipping:

void fill_rect(Graphics *dst, Rect r)
{
	unsigned long mode;
	HDC dst_dc, dst_mask_dc;
	HBITMAP dst_mask;

	if (dst->colour.alpha > 127)
		return; /* nothing to draw if colour is transparent */

	dst_dc = dst->dc;
	dst_mask = dst->bmap->clipmask;

	/* handle XOR mode */
	if (dst->xor_mode)
		mode = PATINVERT;
	else
		mode = PATCOPY;

	if (dst_mask == 0) {
		/* destination is fully opaque: simple case */

		/* fill pixels with colour */
		PatBlt(dst_dc, r.x, r.y, r.width, r.height, mode);
	}
	else {
		/* destination has a clipmask */
		dst_mask_dc = CreateCompatibleDC(dst_dc);
		if (! dst_mask_dc)
			return 0;
		SelectObject(dst_mask_dc, dst_mask);

		/* fill pixels with colour */
		PatBlt(dst_dc, r.x, r.y, r.width, r.height, mode);

		/* update transparency information */
		PatBlt(dst_mask_dc, r.x, r.y, r.width, r.height,
			BLACKNESS);

		DeleteDC(dst_mask_dc);

		/* remove redundant destination clipmask, if any */
		if ((r.x == 0) && (r.y == 0) &&
		    (r.width == dst->area.width) &&
		    (r.height == dst->area.height)) /* all opaque */
		{
			DeleteObject(dst_mask);
			dst->bmap->clipmask = 0;
		}
	}
}

It is interesting to compare this implementation with the X-Windows version. The HDC replaces the X GC, and the HBITMAP replaces the X Pixmap, but otherwise the two are very similar.

One major difference is that the above function handles two different drawing modes, the normal painting mode, in which the drawing colour replaces destination pixels, and the XOR mode in which the drawing colour and the destination pixel values are combined using a bitwise exclusive-or operation. The XOR mode is useful for creating a "rubber-banding effect" where an object is drawn by dragging the mouse outwards. This mode is often used to illustrate to students of computer graphics that pixels are merely numbers, and can be controlled using mathematical principles.

Throughout this discussion of the MS-Windows implementation of drawing operations, we will continue to examine the XOR mode and its implications. The X-Windows implementation had no need for integrating support for the XOR mode into the drawing operation, because X provides a method to set the drawing mode outside those functions (XSetFunction), thus we could assume that XOR mode had already been activated or deactivated. Within MS-Windows it is not that simple, because the drawing mode must be specified as a parameter to each drawing call.

The PatBlt function does the hard work of block-transferring the pattern (or current drawing colour) to the destination. If we are using the normal paint mode, this means we use a mode of PATCOPY, which simply copies the pattern over the target pixels. If the destination has a clipmask we must then call PatBlt again, setting the new opaque pixels to zero, using a mode of BLACKNESS.

If using XOR mode, we use the mode PATINVERT, which is a misnomer, since the pattern is not being inverted at all. It is an exclusive-or operation between the pattern and destination pixels, otherwise written as P^D using C language notation. The mode of BLACKNESS used for the clipmask is still valid, since a colour will turn the clipmask opaque whether or not it is being XORed with a transparent or opaque destination pixel; we are only XORing the colours, not the transparency information.

The test at the start of the function avoids drawing anything if the current drawing colour is transparent, and the final if-statement in the function handles the special case that we've turned the entire clipmask opaque. Like X-Windows, this removal of the destination clipmask is really placed in the wrong place; it should occur before the second PatBlt so as to avoid the redundant call. This is a minor point, but worth fixing in any real implementation.

Implementing copy_rect in MS-Windows

The copy_rect operation is similar to fill_rect except that instead of drawing the colour pattern using PatBlt, it draws from another bitmap, using BitBlt. There are four cases to handle, depending on whether the source or destination do or do not have clipmasks.

Before proceeding we must consider the question of how transparency is represented in a bitmap's image, as opposed to the clipmask. You will recall that the clipmask is a BITMAP of depth 1 bit per pixel, and that 0's represent opaque, and 1's represent transparent.

In order to simulate the effects of clipmask drawing, we must perform two BitBlt operations. The first sets the opaque pixels in the destination to a known configuration, and the second copies only the opaque pixels from the bitmap's image into the area which was earlier prepared.

A valid scheme is as described in this table:

	Clipmask          C     1 1 1 1  0 0 0 0    (1=trans, 0=opaque)
	Source            S     X X 0 0  1 1 0 0
	Destination       D     1 0 1 0  1 0 1 0    Mode Name
	---------------   ---   ----------------    ---------
	MASK stage 1      C&D   1 0 1 0  0 0 0 0    SRCAND
	MASK stage 2      S|D   1 0 1 0  1 1 0 0    SRCPAINT

	COPY (no C)       S     X X X X  1 1 0 0    SRCCOPY

	XOR (C or no C)   S^D   1 0 1 0  0 1 1 0    SRCINVERT

In the table, C stands for the source clipmask, S for the source image, and D for the destination image. In the clipmask, 1's indicate transparency, so the left-hand side of the table should produce no changes. Where the clipmask C is 0, the destination should be changed.

The first two result lines of the table show the two-stage BitBlt operation in action. The clipmask C is ANDed with the destination D to make pixels in the destination zero where the opaque pixel will go. In the second stage, the source image S is ORed with the destination pixels. This requires a little explanation.

In the table above, X stands for "does not occur". The left-most two entries in the source line are marked X, but they should be marked with 1's, to illustrate all bit combinations. However, we wish to make the second stage described above produce the correct pixels, so we want an OR operation to place only the opaque pixels in the destination. For this reason, we want the transparent pixels in the image to be set to 0's, so that ORing them with the destination will produce no change. Thus, we can ignore those first two columns in the table, since wherever the clipmask is 1 (transparent) the source image must be 0 (transparent). It is this combination of 1's in the clipmask, and 0's in the source image which allows two BitBlt operation to copy only the opaque pixels to the destination.

The third line of the table illustrates what happens when there is no source clipmask. In that case, a single BitBlt operation with a mode of SRCCOPY suffices, since the source must be completely opaque. The left-hand side of the table is marked with X's, since this refers to the case that there is transparency information in the clipmask, but since there is no clipmask that never occurs.

The final line in the table shows the desired result when drawing using XOR mode. The exclusive-or operation causes no changes where one of the values is zero, so where the source is transparent (pixels values are 0) the destination remains unchanged, as it should. Where the source image is opaque, the pixel values will be a combination of zeros and ones, and so they will interact with the destination. And interesting point to note is that the clipmask is not needed in this operation, since the zeros within the source image are sufficient to describe the output. Hence, one BitBlt with a mode of SRCINVERT is enough to implement XOR mode.

The table only addresses the question of how to produce the correct pixels in the destination image. We need to end this discussion by examining what happens to the destination's clipmask, if one exists.

The destination's clipmask can only ever proceed towards a totally opaque state, at which point it should be deleted from the bitmap. As a consequence, we need only ever form the union of the source's transparency information with the destination's. If the source image has no associated clipmask, we simply fill the appropriate area in the destination clipmask with 0's to indicate transparency. This corresponds to a PatBlt with a mode of BLACKNESS.

If the source and destination both have clipmasks, we form the union by using a BitBlt operation to draw the source clipmask into the destination clipmask using a mode of SRCAND, which causes 0's in the source (opaque) to become 0's in the destination.

The worst case requires three calls to BitBlt, the best case one. Texts on the subject use these techniques, so this approach is not inefficient, and is really the only technique available on the MS-Windows platform.

We will not delve into code in this section; the code is a straight-forward translation of the discription already given.

Implementing draw_text in MS-Windows

There is not much more to say about the MS-Windows implementation of the text drawing function, except to say that Subfonts, like clipmasks, use 0's for opaque parts of glyphs, and 1's for transparency.

The code is similar to the code for fill_rect, because it involves copying the current drawing colour (the pattern) where each glyph is opaque.

The main difference is in the following code snippet, which sets the appropriate drawing mode:

	/* handle XOR mode */
	if (dst->xor_mode)
		mode = 0x009A0709L;  /* D -> D^S where font opaque */
	else
		mode = 0x00B8074AL;  /* D -> P where font opaque */

These numbers are mysterious, and only documented on the MS web site. They are two of the 256 ROP codes which happen to have no convenient name. The first implements an XOR mode only where the font's clipmask is opaque; the second copies the colour pattern only where the font is opaque. They are both ternary ROP codes in that they involve all three component pixel values, S, D and P. The precise details of each are not important, because the 256 codes implement all possibilities; had we chosen different values to signify transparency within clipmasks or source images, another ROP code would have existed to do the job.

Because fonts are generated from the same function as is used within X-Windows, we have the possibility to produce text which uses the same fonts and looks exactly the same on both MS-Windows and X-Windows, down to each individual pixel. The manner in which pixel copying and colouring is different on the two platforms, but it is clearly possible to conceal the manner and produce implementations which present the same programming interface. This paves the way for complete portability between platforms.

Efficiency

How efficient are the ROP codes used above? Are some ROP codes faster than others? Is there are faster approach to transparency than performing a second BitBlt operation?

The answer to these questions would rely on careful measurement, but we can observe a few general truths.

MS-Windows programs generally run on the same machine as the graphics hardware that GDI interfaces with, and as a consequence the cost of sending messages to the graphics system is lower than in X-Windows, where traffic of this nature can be a larger source of inefficiency. The cost of sending a BitBlt message is essentially a function call, which can be made quite efficient by modern compilers for the Intel architecture.

In simple experiments is can be shown that on the same machine, drawing a page full of text is about twice as fast on a MS-Windows implementation, than on an X-Windows implementation. This shows that the cost of merely communicating with X-Windows may be causing significant delay. We're assuming here that the underlying implementation of X-Windows is roughly as fast as the underlying GDI implementation, but GDI may be using hardware acceleration, since MS-Windows is the platform used for most games, so much effort is spent in making graphics operations efficient. In is not surprising that X-Windows lags MS-Windows.

The fact that the MS-Windows drawing operations use the common, named ROP code for two out of the three operations discussed is a good sign. If there were speed differences between different ROP codes, it is likely that the ones with names (which most programmers use) would be the most highly optimised.

Any further discussion of efficiency in this area would require careful experimentation, but simple experiments have shown that the above operations are sufficiently fast to render a full page of text on all entry-level machines being sold today in under one second.

Conclusion

Bitmaps which support transparent pixels have been implemented for the MS-Windows platform successfully, involving little inefficiency. They behave identically to those implemented on X-Windows, through the use of some clever bit-manipulation and creative use of ROP codes.

Supporting more than this simple binary transparency to achieve better alpha blending would be possible, but may not be easily portable across other platforms. This simple bi-level transparency suffices for many common applications, and has be used to implement a portable bitmapped font scheme.

More investigation needs to be made into whether other platforms such as the Apple Macintosh can support this elegant bitmap construct.

(C) 2001 GraphApp Documentation Team