Graphics
Brush Bitmaps  Lab Report
Example of using brush bitmaps to create patterned masks,
which are combined using transparency into a composite bitmap

Purpose
This lab report shows how to use brush bitmaps to paint selected areas with specified patterns.  Also, this project shows how to merge several masks with various patterns with a background image to form a single composite bitmap.

Background
A common way to fill solid shapes, such as rectangles, polygons, or even ellipses, is with a TBrush object.  The color of the brush is specified using the Color property, while the pattern is specified with the Style property.  The most common brush style is bsSolid, which fills an area with a solid color, but other TBrushStyle styles include bsClear, bsHorizontal, bsVertical, bsFDiagonal, bsBDiagonal, bsCross and bsDiagCross.  All of these brush styles are shown below (from a graphic created using Printer Demo #2):

bsClear is usually used to write text with a transparent background as shown in the second row from the top above.  [See more comments about a bsClear engima.]

All of the above TBrushStyles are ignored when the Bitmap property of the TBrush is defined.  The TBrush.Bitmap property enables a brush to paint with a user-defined bitmap, which can result in a variety of patterns. 

The original Delphi 1 documentation said the TBrush.Bitmap "must be 8 pixels high and 8 pixels wide."  The online Delphi 5 documentation says "If the image is larger than eight pixels by eight pixels, only the top left eight-by-eight region is used."  These statements were true for Windows 95, but these limitations were removed in Windows 98 and Windows 2000.  Larger bitmaps can be used in Windows 98/2000 with a brush to tile an area.

Bitmap.Brush colors have been traditionally limited to 16 of the 20 Windows system colors:

Decimal Hex Name
0 0 clBlack
1 1 clMaroon 
2 2 clGreen
3 3 clOlive
4 4 clNavy
5 5 clPurple
6 6 clTeal
7 7 clGray
8 8 clSilver
9 9 clRed
10 A clLime
11 B clYellow
12 C clBlue
13 D clFuchsia
14 E clAqua
15 F clWhite

The 16-color restriction with pf4bit bitmaps is no longer present in Windows 98/2000 where a 24-bit color bitmap can be used as the brush.

Materials and Equipment

Software Requirements
Windows 95/98/2000
Delphi 3/4/5 (to recompile) 
BitmapBrushes.EXE

Hardware Requirements
High/True Color Display

Procedure

  1. Double click on the BitmapBrushes  icon to start the program.  The default background is automatically displayed when the program starts.
  2. Press the Masks button to see separate red, green and blue masks created from the original image.
  3. Press the Composite button to see the background image be combined with the three masks to form a composite image.  Here are the images in this transformation:
+ =
  1. Experiment with various backgrounds by changing the spinboxes in the background column.  If you have a Windows 98 or 2000 machine, press the Win  98/2000 button to see a 32-by-32 true color bitmap be used to tile the background.  Here are the images in the Win 98/Win 2000 transformation:
+ =

Discussion

I developed the first version of this project under Windows 2000 with Delphi 3 (and 5) and had no problems.  Several changes were needed to get everything to work under Windows 95/98.  These differences are displayed with a bold font in the code below.

Background Image.  The default brush bitmap is defined to be this 8-by-8 matrix:

The spinboxes on the screen define the "+" color to be 11 (i.e., clYellow) and the background color to be 8 (i.e., clSilver).  Since each pixel in a pf4bit Scanline is a nibble, i.e., a single hex digit, each row in the bitmap can be represented by four bytes.  This bitmap can be specified in hex (decimal 11 = hex B) as follows:

BB BB BB BB
BB B8 8B BB
BB B8 8B BB
B8 88 88 8B
B8 88 88 8B
BB B8 8B BB
BB B8 8B BB
BB BB BB BB

Specifying the constant array above would be relatively simple, but allowing changes to the colors via SpinEditBoxes makes the process a bit more complicated.

The TFormBitmapBrushes.SpinEditChange method defines this bitmap using a Pattern matrix and a Lookup table:

CONST
  Pattern:  TBrushPattern =
    ( (0,0,0,0),   // Indirect pattern:
      (0,1,2,0),   // these values are subscripts into
      (0,1,2,0),   // Lookup table used to define
      (1,3,3,2),   // BrushPattern
      (1,3,3,2),
      (0,1,2,0),
      (0,1,2,0),
      (0,0,0,0));
VAR
  Lookup:  ARRAY[0..3] OF BYTE;

The Lookup table is defined as follows using the SpinEditBoxes:

Lookup[0] := 16*SpinEditBackground.Value + SpinEditBackground.Value;
Lookup[1] := 16*SpinEditBackground.Value + SpinEditPlus.Value;
Lookup[2] := 16*SpinEditPlus.Value       + SpinEditBackground.Value;
Lookup[3] := 16*SpinEditPlus.Value       + SpinEditPlus.Value;

The bytes in the scanlines of the bitmap are defined in the BrushPattern matrix:

CONST
    PatternRows    = 8;
    PatternColumns = 8;
TYPE
  TBrushPattern   = ARRAY[0..PatternRows-1,0..PatternColumns DIV 2 - 1] OF BYTE;
VAR
   BrushPattern:  TBrushPattern;
   i           :  INTEGER;
   j           :  INTEGER;
// Use Pattern and Lookup table to define BrushPattern
FOR j := 0 TO PatternRows-1 DO
  FOR i := 0 TO PatternColumns DIV 2 - 1 DO
    BrushPattern[j,i] := Lookup[ Pattern[j,i] ];

The DefineBrushPattern function converts the BrushPattern to a TBitmap:

FUNCTION DefineBrushPattern(CONST  BrushPattern:  TBrushPattern):  TBitmap;
  VAR
    i     :  INTEGER;
    j     :  INTEGER;
    Row   :  pByteArray;
BEGIN
  RESULT := TBitmap.Create;
  RESULT.Width  := PatternColumns;
  RESULT.Height := PatternRows;
  RESULT.PixelFormat := pf4bit;  // 4 bits/pixel
  // Fill the bitmap from the constant array
  // Each byte of scanline contains two pixels.
  // Each nibble of byte is a pixel.
  FOR j := 0 TO PatternRows-1 DO
  BEGIN
    Row := RESULT.Scanline[j];
    FOR i := 0 TO PatternColumns DIV 2 - 1 DO
      Row[i] :=  BrushPattern[j,i]
  END
END {DefineBrushPattern};

The BitmapBackground is created and displayed in the ImageBackground:

BitmapBackground := TBitmap.Create;
BitmapBackground.Width  := ImageOriginal.Width;
BitmapBackground.Height := ImageOriginal.Height;
//  This can be done in Windows 2000, but in other versions the
//  BitmapBackground needs to be pfDevice (the default) so that
//  FillRect works correctly below. 
//  BitmapBackground.PixelFormat := pf24bit;
BitmapBackground.Canvas.Brush.Bitmap := DefineBrushPattern(BrushPattern);
BitmapBackground.Canvas.FillRect(BitmapBackground.Canvas.ClipRect);
ImageBackground.Picture.Graphic := BitmapBackground;

The default BitmapBackground appears like this on the screen:

Changing the SpinEditBoxes will change the colors in the above image.  Alternately, if you are running Windows 98 or Windows 2000, you can press the Win 98/2000 button (you'll get a strange image in Windows 95).  This background bitmap is formed from a 32-by-32 pixel, true-color bitmap in the TFormBitmapBrushes.ButtonWin982000Click method:

This code

TYPE
  TRGBTripleArray = ARRAY[WORD] OF TRGBTriple;
  pRGBTripleArray = ^TRGBTripleArray;
...
VAR
  BitmapBrush:  TBitmap;
  i          :  INTEGER;
  j          :  INTEGER;
  row        :  pRGBTripleArray;
begin
  ...
  BitmapBackground := TBitmap.Create;
  BitmapBackground.Width  := ImageOriginal.Width;
  BitmapBackground.Height := ImageOriginal.Height;
  BitmapBackground.PixelFormat := pf24bit;
  BitmapBrush := TBitmap.Create;
  BitmapBrush.Width  := 32;
  BitmapBrush.Height := 32;
  BitmapBrush.PixelFormat := pf24bit;
  FOR j := 0 TO BitmapBrush.Height-1 DO
  BEGIN
    row := BitmapBrush.Scanline[j];
    FOR i := 0 TO BitmapBrush.Width-1 DO
    BEGIN
      WITH row[i] DO
      BEGIN
        rgbtRed   := MulDiv(255, i, BitmapBrush.Width-1);
        rgbtGreen := MulDiv(255, j, BitmapBrush.Height-1);
        rgbtBlue  := 0
      END
    END
  END;

  BitmapBackground.Canvas.Brush.Bitmap := BitmapBrush;
  BitmapBackground.Canvas.FillRect(BitmapBackground.Canvas.ClipRect);
  ImageBackground.Picture.Graphic := BitmapBackground;
...

creates the following background image:

Since this bitmap has a height and width of 96 pixels, the 32-by-32 brush bitmap is painted in a 3-by-3 matrix, as shown above.

Masks.  Pressing the Masks button calls  TFormBitmapBrushes.ButtonMasksClick method.  This method consists of separate calls to a CreateAndShowMask procedure for each of the colored areas.  This is not a very general routine, since it assumes three colored blobs of particular colors are present in the original bitmap.  

There are two (or more) possible implementation options.  One option would be to find the "blob" and fill it in place with the desired bitmap brush.  I chose a second way that involves a separate mask for each of the blobs.  This second approach allowed showing how to use transparency to form a composite image of a base bitmap and several "mask" bitmaps.

The following code defines the BluePattern, which is the brush bitmap for the blue area, RGB = (0,0,128), in the original bitmap.

CONST
  BluePattern:  TBrushPattern =    // C=blue, F=white
      ( ($FF,$FF,$FF,$FF),
        ($FF,$CC,$CC,$FF),
        ($FC,$FF,$FF,$CF),
        ($FC,$FC,$CF,$CF),
        ($FC,$FC,$CF,$CF),
        ($FC,$FF,$FF,$CF),
        ($FF,$CC,$CC,$FF),
        ($FF,$FF,$FF,$FF));

This TBrushPattern constant directly defines the scanlines for the desired Brush.Bitmap, which is to be used to fill the original blue area.  The enlarged BluePattern appears as follows in graphical form:

Given the blue color in the original image was RGB = (0, 0, 128), and given the BluePattern above, the call to CreateAndShowMask creates the output bitmap BitmapMaskBlue and also displays it in ImageMaskBlue.  

CreateAndShowMask(  0,  0,128, BitmapMaskBlue,  ImageMaskBlue,  BluePattern);

Similar calls to CreateAndShowMask are used to handle the red and green "blobs" in the original image with separate patterns used to fill each of the areas.  See the source code for complete details.

CreateAndShowMask scans the ImageOriginal.Picture.Bitmap and does two things.  The routine creates a blank/white mask where the white area is the set of pixels from the original bitmap with the specified (R,G,B) values.  All other pixels are colored black.  While creating this black/white BitmapMask, the centroid coordinates, (iCentroid, jCentroid), are computed as the "average" x and y coordinates.  (See the Polygon Area Lab Report for another way to compute the centroid of a polygon.)  Such an "average" coordinate find the centroid for any convex shape -- a shape in which a line between any two points is contained within the shape.

PROCEDURE CreateAndShowMask(CONST R, G, B:  BYTE;
                            VAR   BitmapMask:  TBitmap;
                            CONST Image     :  TImage;
                            CONST Pattern   :  TBrushPattern);
  VAR
    Count      :  INTEGER;
    i          :  INTEGER;
    j          :  INTEGER;
    iCentroid  :  INTEGER;
    jCentroid  :  INTEGER;
    rowIn      :  pRGBTripleArray;
    rowOut     :  pRGBTripleArray;
BEGIN
  // Create Bitmap here, but don't free until Form.Destroy.  Keep
  // these bitmaps in-memory for other manipulation
  BitmapMask := TBitmap.Create;
  BitmapMask.Width  := ImageOriginal.Width;
  BitmapMask.Height := ImageOriginal.Height;
  BitmapMask.PixelFormat := pf24bit;
  iCentroid := 0;
  jCentroid := 0;
  Count := 0;
  FOR j := 0 TO BitmapMask.Height-1 DO
  BEGIN
    rowOut := BitmapMask.Scanline[j];
    rowIn  := ImageOriginal.Picture.Bitmap.Scanline[j];
    // Access TRGBTriple in way that works in D3-D5 (but there's
    // an easier way in D5)
    FOR i := 0 TO BitmapMask.Width-1 DO
    BEGIN
      IF   (rowIn[i].rgbtRed   = R) AND
           (rowIn[i].rgbtGreen = G) AND
           (rowIn[i].rgbtBlue  = B)
      THEN BEGIN
        rowOut[i].rgbtRed   := 255;    // white
        rowOut[i].rgbtGreen := 255;
        rowOut[i].rgbtBlue  := 255;
        iCentroid := iCentroid + i;
        jCentroid := jCentroid + j;
        Count := Count + 1;
      END
      ELSE BEGIN
        rowOut[i].rgbtRed   := 0;      // black
        rowOut[i].rgbtGreen := 0;
        rowOut[i].rgbtBlue  := 0
      END
    END
  END;
  // This MUST be done in Windows 95/98 to get the proper mask by
  // FloodFill below.  However, this can be deleted in Windows 2000.
  BitmapMask.PixelFormat := pfDevice;
  IF   Count > 0
  THEN BEGIN
    iCentroid := iCentroid DIV Count;
    jCentroid := jCentroid DIV Count;
    BitmapMask.Canvas.Brush.Bitmap := DefineBrushPattern(Pattern);
    BitmapMask.Canvas.FloodFill(iCentroid, jCentroid, clWhite, fsSurface);
  END;
  Image.Picture.Graphic := BitmapMask
END {CreateAndShowMask};

The last IF statement above computes the (x,y)  centroid when one or more pixels of the specified color are found.  Then the desired Brush.Bitmap is created, and used to fill the white area of the BitmapMask with the desired bitmap pattern.  The remaining area of the mask remains black.

The three masks appear as follows for the three colored areas:

Composite Image.  Given a background image, and the three masks above, a composite image is fairly easy to create in the TFormBitmapBrushes.ButtonCompositeClick method.   The BitmapBackground image is drawn directly on the BitmapComposite.Canvas.  The Transparent property of each of the masks is set to TRUE, and then each is drawn in turn on the BitmapComposite.Canvas.   Note that the Transparent property only makes a difference when drawing the bitmap on another canvas.

// Draw the background on a new bitmap and use transparency to draw the
// three masks on top of the same bitmap to form a single composite bitmap.
procedure TFormBitmapBrushes.ButtonCompositeClick(Sender: TObject);
  VAR
    BitmapComposite:  TBitmap;
begin
  BitmapComposite := TBitmap.Create;
  TRY
    BitmapComposite.Width  := ImageOriginal.Width;
    BitmapComposite.Height := ImageOriginal.Height;
    BitmapComposite.PixelFormat := pf24bit;
    BitmapComposite.Canvas.Draw(0,0, BitmapBackground);
    BitmapMaskRed.Transparent := TRUE;
    BitmapComposite.Canvas.Draw(0,0, BitmapMaskRed);
    BitmapMaskGreen.Transparent := TRUE;
    BitmapComposite.Canvas.Draw(0,0, BitmapMaskGreen);
    BitmapMaskBlue.Transparent := TRUE;
    BitmapComposite.Canvas.Draw(0,0, BitmapMaskBlue);
    ImageComposite.Picture.Graphic := BitmapComposite
  FINALLY
    BitmapComposite.Free
  END;
end;

With the default Bitmap.TransparentMode of tmAuto the TransparentColor property returns the color of the bottom-leftmost pixel of the bitmap image, which was black for all the the masks.  (When TransparentMode is set to tmFixed, the TransparentColor property can be use to specify the transparency color.)

The resulting composite images for the two backgrounds and the masks discussed above are as follows:

Conclusions
"Tiling" a bitmap in Windows 95/98 is very easy by setting the Canvas.Brush.Bitmap to be the "tile," and then using FillRect to fill the entire area.  A Brush.Bitmap is an easy way to fill an area with a unique pattern.


Keywords
TBrush, TBitmap, Brush.Bitmap, Windows system colors, pf4bit, pf24bit, Scanline, FillRect, FloodFill, centroid, mask, Draw, Transparent, Windows 95/98/2000, Delphi 3/4/5

Download
Delphi 3/4/5 Source and EXE (135 KB):  BrushBitmaps.ZIP


Acknowledgement:  Thanks to Armando Marques Sobrinho from Brazil for asking how to create patterns from solid areas in an image and providing an example bitmap, which was modified slightly to be the "original" image in this Lab Report.


Related
Steve Schafer's UseNet Post about how to apply the Windows Shutdown Screen Effect to a Bitmap


Updated 26 Feb 2005


since 20 Aug 2000