Thursday, 13 October 2011

Transparent graphics with pure GDI (Part 1)

Transparent graphics are used everywhere these days.  Half the UIs you will have used today will have made some use of composited graphics, overlays, or something similar.  You've probably seen:
  • Selections with transparent rectangles or other shapes
  • Text with a transparent shape behind it, so you can see the through to the background and still clearly read the text
  • Custom drawing on glass, such as text with the blurred white background and custom controls
How do you achieve all of these with Delphi or C++Builder?

This is part 1 of a short series that examines alpha-aware graphics using native GDI only - not GDI+, not DirectX, and not with any other custom non-GDI graphics implementation.  By the end of the series I will have introduced a small alpha-aware canvas class that you can use to draw standard shapes (rectangles, ellipses, etc) just as you can with the normal TCanvas (and using standard TFont, TPen and TBrush objects), to compose several layers of alpha blended graphics together, and to draw on glass on Vista and Windows 7.

This is a screenshot of the demo program which I used to test this class.  It's not the prettiest demo program ever, but you can see all of the above in action in this screenshot:






A lot of this is quite hard with standard VCL classes.

What Part 1 covers

This post will cover:

  • Introduction to transparency and the alpha channel
  • Using constant alpha (transparency) across an image, and blending it over another image
  • Using this to create an Explorer-style transparent selection
  • Per-pixel alpha, premultiplied alpha, and advanced blending

A quick introduction to partial transparency

Windows uses GDI for most drawing, and GDI was never intended for something as complex as partial transparency or composition.  There aren't many options for implementing transparency.  One common one is "colour keying", where a single colour is completely transparent.  Windows can be color-keyed to create holes or transparent segments, or bitmaps on toolbar buttons use this to draw and have a transparent background.  But we want something more complex: partial transparency, where a pixel is neither fully on nor fully off.

This article and its sequel assumes you're familiar with the VCL TBitmap class and with Scanline, and are moderately familiar (or at least aware of) the internal Windows objects used for drawing - things like what a device context is, a DIB, the use of SelectObject, etc.  However, I'll try to explain the key bits as I go along.

Partial (per-pixel) transparency

A Windows device-independent bitmap can have varying degrees of bit-ness (1, 2, 4, 8... this can include a palette, something out of scope for now).  Two of these interest us: 24 and 32.  A 24-bit DIB has three channels of eight bits each: red, green and blue.  A 32-bit DIB has a fourth channel, which is either unused or can be used for alpha.  Alpha is the degree of transparency: 0 is fully transparent (invisible), 255 is fully opaque.

Windows provides one function for drawing 32-bit bitmaps using alpha for the fourth channel, the alpha-aware equivalent of BitBlt: AlphaBlend.

The problem is that apart from this one method, GDI knows nothing of alpha.  The use of this fourth channel is effectively user-defined.  To GDI, when drawing, you are effectively using a three-channel bitmap with a fourth present that it doesn't know how to interpret.  If you draw to a 32-bit bitmap with GDI functions, anything present in this fourth channel will be clobbered - its value will be reset to 0.  0, unfortunately, means 'fully transparent'.

AlphaBlend with per-pixel transparency is therefore generally used when you create a 32-bit bitmap some other way, such as by loading one from file.  A good example of this is this great article on an alpha-blended splash screen, where the splash screen is loaded from a transparent PNG file.  It also explains some terminology in far more detail (and accuracy) than I will here.

Constant transparency

AlphaBlend is also useful for non-32-bit bitmaps, i.e. bitmaps without a fourth alpha channel per pixel.  It can be used this way with 'constant alpha', that is a set alpha value across the whole image.

This is the simplest way to sue AlphaBlend, and is where we'll start.

The simplest AlphaBlend example possible

A simple but useful example is to blend a bitmap (without alpha, just a standard opaque bitmap) over another bitmap with a constant transparency.  An example of this is a selection rectangle: it's one solid object, blended over the other.  Transparency does not change from pixel to pixel over the bitmap.

AlphaBlend has a BLENDFUNCTION parameter.  This structure is how you control how the function's blending behaves - the other parameters are identical to BitBlt (which opaquely draws one image over another.)  The first two of the four parameters only have one valid value.  That leaves two useful ones:
  • AlphaFormat.  This should be AC_SRC_ALPHA when you have per-pixel alpha, and 0 otherwise.  For this simple example, using constant alpha across the image, it should be set to 0.
  • SourceConstantAlpha.  If you use AC_SRC_ALPHA, set it to 255.  Otherwise, and this includes this simple non-per-pixel example, set it to a non-255 value.  This will be interpreted as a constant alpha value that will be applied across the entire bitmap - that is, you don't have to have an alpha channel with valid values, it is this number that will be the alpha for the entire bitmap.
The procedure to draw a selection rectangle is simple, then.  Create a bitmap for your selection rectangle.  Fill it with a solid colour, such as clHighlight.  Blend it with a non-255 value for SourceConstantAlpha - I find that a value of 64 or 96 mimics Explorer's selection rectangle well.

Here is a simple code snippet painting a TPaintBox, with two predefined TBitmaps for the background and blended-on-top foreground images.  First, setting up the bitmaps (note there's no worrying about the bitness, we'll get to specific bits / pixel formats later):

BackgroundBMP := TBitmap.Create;
BackgroundBMP.LoadFromFile('oranges3-small.bmp');
FrontBMP := TBitmap.Create;
FrontBMP.Width := 300;
FrontBMP.Height := 150;
FrontBMP.Canvas.Brush.Color := clHighlight;
FrontBMP.Canvas.FillRect(Rect(0, 0, 300, 150));

Second, painting in the paintbox's OnPaint event handler:

// Background image
PaintBox1.Canvas.Draw(0, 0, BackgroundBMP);

// Blend a foreground image over the top - constant alpha, not per-pixel
BlendFunc.BlendOp := AC_SRC_OVER;
BlendFunc.BlendFlags := 0;
BlendFunc.SourceConstantAlpha := 96;
BlendFunc.AlphaFormat := 0;
Windows.AlphaBlend(PaintBox1.Canvas.Handle, 50, 50, FrontBMP.Width, FrontBMP.Height,
FrontBMP.Canvas.Handle, 0, 0, FrontBMP.Width, FrontBMP.Height, BlendFunc);

// Solid edge to the rectangle, because Explorer draws selections like that:
PaintBox1.Canvas.Pen.Color := clHighlight;
PaintBox1.Canvas.Brush.Style := bsClear;
PaintBox1.Canvas.Rectangle(Rect(50, 50, 50 + FrontBMP.Width, 50 + FrontBMP.Height));

This gives the following:


Excellent!  That looks pretty close to an Explorer-style transparent selection to me.

(Note that AlphaBlend will stretch the source image, so for something like a selection you don't need to create a bitmap the exact size of the user's selection - create a single small one and draw it to the larger coordinates, and it will stretch as you paint it.  This will also be a lot faster than creating a bitmap of the 'right size' every time the selection rectangle changes.)

So far so good!  You now have a grasp of:
  • Basic AlphaBlend usage
  • Constant image-wide alpha
  • And can use it to draw Explorer-style selections
What next?  Per-pixel alpha, that is, a different degree of transparency for each pixel.

Per-pixel alpha values

Initially this seems simple: use a 32-bit bitmap, set the alpha channel for each pixel, use AC_SRC_ALPHA to specify that's what you've done, and call AlphaBlend.  However, it's slightly more complicated than that, and the reason is this sentence from MSDN: "[AlphaBlend uses] premultiplied alpha, which means that the red, green and blue channel values in the bitmap must be premultiplied with the alpha channel value. For example, if the alpha channel value is x, the red, green and blue channels must be multiplied by x and divided by 0xff prior to the call."

It's easy to miss this.  Luckily it's fairly simple to implement.  Basically you iterate over all the pixels in your bitmap, set the alpha value, and modify the red, green and blue values.

Why does Windows require a pre-multiplied bitmap?  Probably it is to improve performance, but this comes at the cost of requiring your code to implement the premultiplication.  Chances are, this code will be slower than the equivalent code that could have been implemented in the graphics driver.

You can use the inbuilt PRGBQuad type to point to a specific pixel if you wish, but I'm going to use my own type, for two reasons: first, TRGBQuad's alpha channel is named 'rgbReserved'; second, this custom type will be extended in Part 2.  The definition here is missing some key bits.

Type definition of TQuadColor:

TQuadColor = packed record
  case Boolean of
    True : (Blue,
            Green,
            Red,
            Alpha : Byte);
    False : (Quad : Cardinal);
end;
PQuadColor = ^TQuadColor;
PPQuadColor = ^PQuadColor;

This structure lets you access each channel individually, or the whole value as a 32-bit value.

Next, the above code that blended the front bitmap, modified for premultiplied per-pixel alpha (remember the MSDN quote describing the algorithm, above) using ScanLine to get direct access to the pixel data.  When you use ScanLine, you need to be aware of the pixel format, that is, exactly what a pixel is and what your pointer points to.  Here, the bitmap's PixelFormat is set to pf32Bit, to ensure first that it has the four channels / 32 bits that AlphaBlend requires for per-pixel use, and second so that addressing a pixel as a PQuadColor is correct.

As a demo, this varies the alpha across the image horizontally:

var
  RowY,
  ColumnX : Integer;
  Alpha : Single;
  Pixel : PQuadColor;
  AlphaByte : Byte;
begin
  FrontBMP := TBitmap.Create;
  FrontBMP.PixelFormat := pf32Bit; // Important for AlphaBlend and for four channels we access through Scanline
  FrontBMP.Width := 300;
  FrontBMP.Height := 150;
  FrontBMP.Canvas.Brush.Color := clHighlight;
  FrontBMP.Canvas.FillRect(Rect(0, 0, 300, 150));

  for RowY := 0 to FrontBMP.Height-1 do
  begin
    Pixel := FrontBMP.Scanline[RowY];
    for ColumnX := 0 to FrontBMP.Width - 1 do
    begin
      Alpha := ColumnX / FrontBMP.Width; // 0-1 of how far along the row this is
      AlphaByte := Round(Alpha * 255.0); // Convert 0-1 to 0-255
      // Set the alpha channel
      Pixel.Alpha := AlphaByte;
      // Premultiply R, G and B
      Pixel.Red := Round(Pixel.Red * Alpha);
      Pixel.Green := Round(Pixel.Green * Alpha);
      Pixel.Blue := Round(Pixel.Blue * Alpha);
      Inc(Pixel);
    end;
  end;
end;

Finally, the call to AlphaBlend is changed:

BlendFunc.BlendOp := AC_SRC_OVER;
BlendFunc.BlendFlags := 0;
BlendFunc.SourceConstantAlpha := 255;
BlendFunc.AlphaFormat := AC_SRC_ALPHA;
Windows.AlphaBlend(PaintBox1.Canvas.Handle, 50, 50, FrontBMP.Width, FrontBMP.Height, FrontBMP.Canvas.Handle,
  0, 0, FrontBMP.Width, FrontBMP.Height, BlendFunc);

When run, this code gives you the following:


Awesome!  That is per-pixel alpha transparency, blended over another image.

Part 2 - the complicated stuff

So far, this article has covered:
  • What alpha transparency is
  • Basic AlphaBlend usage, using constant alpha, and using that to draw an Explorer-style selection
  • Per-pixel alpha, pre-multiplied alpha, using ScanLine to modify the pixels of an image, and advanced use of AlphaBlend to blend this per-pixel-transparent image
That's actually a far bit of material.  Part 2 of this series will continue:
  • Non-rectangular drawing
  • Handling GDI's clobbering of the alpha channel
  • Better use of per-pixel alpha (based on what's drawn, which is more useful than a horizontal gradient)
  • Introduction of the TTransparentCanvas class: a TCanvas-like class allowing you to draw rectangles, ellipses, text etc as you normally would, including using TPen, TBrush and TFont.  It composes the layers of drawing as you go, building a per-pixel alpha-aware image.  It also allows you to draw on glass - something VCL applications traditionally have trouble with.  It's this class that is demonstrated in the image right at the start of this article.

5 comments:

  1. Thanks! This is very useful. Looking forward to part two.

    ReplyDelete
  2. Great article .. congrats !

    For the ones looking into GDI+ you may consider the FREE Mitov's IGDIPlus library (from 5.0 with XE2 support) that simplify a lot the GDI+ functionality into a very Delphi-friendly way (a great usecase of interfaces also): http://www.mitov.com/products/igdi+

    ReplyDelete
  3. Thanks guys! Glad you like it.

    GDI+ is great, and I've used it quite a bit. That library looks like an excellent translation too. I've found GDI+ can be very slow though - even on XP, it was only hardware-accelerated under some circumstances, and I think on Vista it never was at all.

    My theory with GDI is that, although what you can achieve is slightly more limited (no antialiased edges for example) most operations will be hardware-accelerated through the driver and so your graphics should be much faster. It also means you don't have to add extra libraries to your code, and you can easily extend your current GDI/TCanvas-based drawing code. The trick is just figuring out how to achieve some things like transparency... thus this article :)

    ReplyDelete
  4. Thanks! I recently implemented a non-rectangular selection similar to your code but it's rather slow. Hopefully I can make use of this TTransparentCanvas class to make it faster. :-)

    ReplyDelete
  5. After re-reading your article I replaced my "FrontBMP" for rectangular selections with a 1x1 bitmap and the drawing has sped up a gazillion times. (Obvious in hindsight :-)) I still have to revisit the non-rectangular situation. Thanks again!

    ReplyDelete