Articles   Members Online:
-Article/Tip Search
-News Group Search over 21 Million news group articles.
-Delphi/Pascal
-CBuilder/C++
-C#Builder/C#
-JBuilder/Java
-Kylix
Member Area
-Home
-Account Center
-Top 10 NEW!!
-Submit Article/Tip
-Forums Upgraded!!
-My Articles
-Edit Information
-Login/Logout
-Become a Member
-Why sign up!
-Newsletter
-Chat Online!
-Indexes NEW!!
Employment
-Build your resume
-Find a job
-Post a job
-Resume Search
Contacts
-Contacts
-Feedbacks
-Link to us
-Privacy/Disclaimer
Embarcadero
Visit Embarcadero
Embarcadero Community
JEDI
Links
How to convert Windows Bitmaps to Windows Regions very fast Turn on/off line numbers in source code. Switch to Orginial background IDE or DSP color Comment or reply to this aritlce/tip for discussion. Bookmark this article to my favorite article(s). Print this article
BitmapToRegion (Delphi-like version - very fast) 07-Jul-03
Category
Win API
Language
Delphi 4.x
Views
177
User Rating
No Votes
# Votes
0
Replies
0
Publisher:
DSP, Administrator
Reference URL:
DKB
			 Author: Felipe Machado

How to convert Windows Bitmaps to Windows Regions very fast. This is the 
Delphi-like replacement for the BitmapToRegion function. This version is much 
cleaner, smaller and educational mantaining a high performance. You will also find 
many interesting comments and techniques inside.

Answer:

Overview 

The function BitmapToRegion creates a Windows Region (HRGN) from a Windows Bitmap 
(HBITMAP) which is used as a mask. You choose one color to be made "transparent", 
meaning that areas of the bitmap with this color will be left out of the resulting 
region. This region will take the shape of the "non-transparent" pixels from the 
original bitmap, which may or may not have a regular shape. You can later apply 
this region to any window (all windowed controls including the form) using the 
SetWindowRgn API call. Using this method you can create non-rectangular forms or 
controls easily. The function also accepts a color tolerance for red, green, and 
blue values which means that a color range could be specified rather than only one 
color. 

How it works? 

The function iterates over all the bitmap scanlines searching for contiguous 
non-transparent pixels on a row-by-row basis. 

It keeps record of the last visible pixel position on the row and loops until a 
transparent pixel is found or the end of the row reached. Variable "x0" holds the 
last visible pixel position and "x" holds the current pixel position. If x0 = x it 
means that the current pixel is transparent and must be ignored, if x > x0 then we 
have at least one visible pixel. 

We then add a rect containing the pixels (x0,y) to (x,y+1) to a windows structure 
RGNDATA which is passed to the function ExtCreateRegion, later used to create the 
desired region (the RGNDATA is explained later on this article). The variable y 
holds the current row being scanned. If we aren't yet at the end of the row we will 
make x0 = x and will restart looping until another transparent pixel is found or 
the end of the row reached (doing the same procedures again). If the end of the row 
is finally reached we will jump to the next bitmap row, starting with x0 = x = 0. 

By doing this to the entire bitmap we will end up with the desired visible region. 

Problems found 

** Windows 98 Limitations 
Using this function on Windows 98 could fail with very complex masks (bitmaps). 
That is due to a limitation on ExtCreateRegion under this OS: the function fails if 
the number of rects is too large. To workaround this, every time the number of 
rects reachs 2000, we call ExtCreateRegion and store it in Result (if it is the 
first region created) or we combine this region with the one already created. 

** Accessing the RGNDATA rects by index 
The region data is made up of a RGNDATAHEADER which specifies the type and size of 
the region plus a buffer of arbitrary size called Buffer (brilliant!). The problem 
is that we need to access Buffer as if it was an Array of Rects, but it is defined 
as: 

Buffer: array[0..0] of CHAR;

This is a very commom C construct actually denoting a char pointer (char *), but 
Pascal is a far more strong typed language preventing us from accessing this array 
directly. The easiest way to access the rects on this buffer is by typecasting it 
to a more convenient structure like: 

TRectArray = array[0..(MaxInt div SizeOf(TRect)) - 1] of TRect;

This creates the largest possible array of TRect elements. Do not attempt to create 
a variable of this type because you will certainly exhaust system resources. It is 
meant to be used for typecasting variables only (or a pointer contents: 
TRectArrat(MyPrt^)[x] not TRectArray(MyPtr)[x]!). If we did declare a pointer to 
this structure, we will be able to typecast another pointer without having to 
dereference it: 

PRectArray = ^TRectArray;

Now the following statement is correct: PRectArray(MyPtr)[x]. We can make a 
variable of type PRectArray (let's say pr) point to Buffer with the simple 
statement: pr := @RgnData.Buffer; This technique can be used to simplify and 
clarify your code the same way it is used here. 

** Bitmap Orientation and Scanline Access 
The code: 

ScanLinePtr := bmp.ScanLine[0];
ScanLineInc := Integer(bmp.ScanLine[1]) - Integer(ScanLinePtr);

is very tricky. The first line gets a pointer to the first bitmap scanline. The 
second line gets the (signed) distance in bytes that separates the bitmap scanlines 
in memory (scanlines need not to be contiguos in memory). If the bitmap is 
bottom-up the distance will be negative. When we make 
Inc(Integer(ScanLinePtr),ScanLineInc) we are already taking into account this 
possibility (Inc with negative values actually decrements). 

The access to individual pixels are made using the techniques shown earlier on 
"Accessing the RGNDATA rects by index". We find the value of the xth character by 
typecasting ScanLinePrt to PByteArray at the index [x*SizeOf(TRGBQuad)]. TRGBQuad 
is an structure of red, green, blue, plus an extra byte that represents a pixel for 
a 32-bit RGB image. We then make b point to this element of the array. Since b is 
itself a PByteArray, we can access its individual bytes by index where the first 
(0) is red, the second is green, and the last (2) is blue (we do not use the fourth 
byte). The code is as follows: 
1   
2   b := @PByteArray(ScanLinePtr)[x * SizeOf(TRGBQuad)];
3   if (b[0] >= lr) and (b[0] <= hr) and
4     (b[1] >= lg) and (b[1] <= hg) and
5     (b[2] >= lb) and (b[2] <= hb) then

  ...

Rationales 

** Use of RGNDATA and ExtCreateRegion: 
Speed! Speed is one of the main concerns of this algorithm. We could have made the 
code much simpler if we used CreateRectRgn for every rect found and combined them 
one-by-one with CombineRgn instead of adding rects to a RGNDATA and later calling 
ExtCreateRegion to create the region at once, but ExtCreateRegion is much faster 
than the combined use of CreateRectRgn and ComineRgn. 

** Use of AllocUnit 
Performance again is the factor of choice of this added complexity. We could 
allocate memory as needed, one rect at a time, but it is much faster to allocate a 
chunk of rects (even if we ended up with unused memory) and only do memory 
reallocation (expansion) when we run out of space (we catch this when we test if 
RgnData^.rdh.nCount >= maxRects). In the end all data (even unused) is freed. 

** Use of Scanline Instead of Pixels property Scanline is a hundred times faster 
than Pixels property for accessing the pixels of a bitmap. 

** 32-bit Depth Conversion 
I have chosen to convert every bitmap to pf32bit first to be able to deal uniformly 
with the bitmap data, no need to have a special case for every pixel format and 
since the algorithm is meant to be used only with RGB images (not palette indexed) 
it was a matter of choosing between pf24bit and pf32bit. Second because Windows 
being a 32-Bit environment is faster when dealing with 32-bit per pixel bitmaps. 

Conclusion 

This article provided a very fast Delphi friendly routine to convert bitmaps into 
windows regions. These regions could be used to apply astonishing effects to your 
forms simply by making them non-rectangular and decorated art painted over the 
visible areas (if you ever saw the Quintessential CD player or the new apple 
Quicktime interface you know what I mean). 

You will also find inside a discussion on some Windows 98 limitations regarding 
complex region creation and the best (fastest) method to use the TBitmap Scanline 
property to access the pixels of an image. There's also some comments on how to 
access arbitrary sizeable structs often found in C/C++ code from within Delphi. 

Final Comments 

This function is used by a component which I wrote called TFormShapper to apply 
persintent non-regular shapes to forms. The component has a mask property of type 
TPicture that stores the picture along with the form. This picture can be any valid 
TGraphic descendant (including my own TPNGImage or TTGAImage implementations). All 
I did to use it as a bitmap was to create a temporary bitmap and draw the stored 
graphic over it with: tmpBMP.Canvas.Draw(0, 0, TheGraphic); then I passed this 
tmpBMP to the function, freeing it later to release memory and system resources. 
You can use this technique if you have any image that is not a TBitmap, but that 
could be drawn over one. 

My next post will regard the techniques used to extend the graphics capabilities of 
Delphi, adding new image file formats and creating a new derived TGraphic class. 
Stay tuned. 

--- CODE STARTS HERE --- 
6   
7   function BitmapToRegion(bmp: TBitmap; TransparentColor: TColor = clBlack;
8     RedTol: Byte = 1; GreenTol: Byte = 1; BlueTol: Byte = 1): HRGN;
9   const
10    AllocUnit = 100;
11  type
12    PRectArray = ^TRectArray;
13    TRectArray = array[0..(MaxInt div SizeOf(TRect)) - 1] of TRect;
14  var
15    pr: PRectArray; // used to access the rects array of RgnData by index
16    h: HRGN; // Handles to regions
17    RgnData: PRgnData; // Pointer to structure RGNDATA used to create regions
18    lr, lg, lb, hr, hg, hb: Byte; // values for lowest and hightest trans. colors
19    x, y, x0: Integer; // coordinates of current rect of visible pixels
20    b: PByteArray; // used to easy the task of testing the byte pixels (R,G,B)
21    ScanLinePtr: Pointer; // Pointer to current ScanLine being scanned
22    ScanLineInc: Integer; // Offset to next bitmap scanline (can be negative)
23    maxRects: Cardinal; // Number of rects to realloc memory by chunks of AllocUnit
24  begin
25    Result := 0;
26    { Keep on hand lowest and highest values for the "transparent" pixels }
27    lr := GetRValue(TransparentColor);
28    lg := GetGValue(TransparentColor);
29    lb := GetBValue(TransparentColor);
30    hr := Min($FF, lr + RedTol);
31    hg := Min($FF, lg + GreenTol);
32    hb := Min($FF, lb + BlueTol);
33    { ensures that the pixel format is 32-bits per pixel }
34    bmp.PixelFormat := pf32bit;
35    { alloc initial region data }
36    maxRects := AllocUnit;
37    GetMem(RgnData, SizeOf(RGNDATAHEADER) + (SizeOf(TRect) * maxRects));
38    try
39      with RgnData^.rdh do
40      begin
41        dwSize := SizeOf(RGNDATAHEADER);
42        iType := RDH_RECTANGLES;
43        nCount := 0;
44        nRgnSize := 0;
45        SetRect(rcBound, MAXLONG, MAXLONG, 0, 0);
46      end;
47      { scan each bitmap row - the orientation doesn't matter (Bottom-up or not) }
48      ScanLinePtr := bmp.ScanLine[0];
49      ScanLineInc := Integer(bmp.ScanLine[1]) - Integer(ScanLinePtr);
50      for y := 0 to bmp.Height - 1 do
51      begin
52        x := 0;
53        while x < bmp.Width do
54        begin
55          x0 := x;
56          while x < bmp.Width do
57          begin
58            b := @PByteArray(ScanLinePtr)[x * SizeOf(TRGBQuad)];
59            // BGR-RGB: Windows 32bpp BMPs are made of BGRa quads (not RGBa)
60            if (b[2] >= lr) and (b[2] <= hr) and
61              (b[1] >= lg) and (b[1] <= hg) and
62              (b[0] >= lb) and (b[0] <= hb) then
63              Break; // pixel is transparent
64            Inc(x);
65          end;
66          { test to see if we have a non-transparent area in the image }
67          if x > x0 then
68          begin
69            { increase RgnData by AllocUnit rects if we exceeds maxRects }
70            if RgnData^.rdh.nCount >= maxRects then
71            begin
72              Inc(maxRects, AllocUnit);
73              ReallocMem(RgnData, SizeOf(RGNDATAHEADER) + (SizeOf(TRect) * MaxRects));
74            end;
75            { Add the rect (x0, y)-(x, y+1) as a new visible area in the region }
76            pr := @RgnData^.Buffer; // Buffer is an array of rects
77            with RgnData^.rdh do
78            begin
79              SetRect(pr[nCount], x0, y, x, y + 1);
80              { adjust the bound rectangle of the region if we are "out-of-bounds" }
81              if x0 < rcBound.Left then
82                rcBound.Left := x0;
83              if y < rcBound.Top then
84                rcBound.Top := y;
85              if x > rcBound.Right then
86                rcBound.Right := x;
87              if y + 1 > rcBound.Bottom then
88                rcBound.Bottom := y + 1;
89              Inc(nCount);
90            end;
91          end; // if x > x0
92          { Need to create the region by muliple calls to ExtCreateRegion, 'cause }
93          { it will fail on Windows 98 if the number of rectangles is too large   }
94          if RgnData^.rdh.nCount = 2000 then
95          begin
96            h := ExtCreateRegion(nil, SizeOf(RGNDATAHEADER) + (SizeOf(TRect) *
97              maxRects), RgnData^);
98            if Result > 0 then
99            begin // Expand the current region
100             CombineRgn(Result, Result, h, RGN_OR);
101             DeleteObject(h);
102           end
103           else // First region, assign it to Result
104             Result := h;
105           RgnData^.rdh.nCount := 0;
106           SetRect(RgnData^.rdh.rcBound, MAXLONG, MAXLONG, 0, 0);
107         end;
108         Inc(x);
109       end; // scan every sample byte of the image
110       Inc(Integer(ScanLinePtr), ScanLineInc);
111     end;
112     { need to call ExCreateRegion one more time because we could have left    }
113     { a RgnData with less than 2000 rects, so it wasn't yet created/combined  }
114     h := ExtCreateRegion(nil, SizeOf(RGNDATAHEADER) + (SizeOf(TRect) * MaxRects),
115       RgnData^);
116     if Result > 0 then
117     begin
118       CombineRgn(Result, Result, h, RGN_OR);
119       DeleteObject(h);
120     end
121     else
122       Result := h;
123   finally
124     FreeMem(RgnData, SizeOf(RGNDATAHEADER) + (SizeOf(TRect) * MaxRects));
125   end;
126 end;
127 
128 {I've supplied a couple of simple examples of using this function for beginners: 
129 
130 This first example sets the region of a TForm}
131 
132 procedure TForm1.Button1Click(Sender: TObject);
133 var
134   ARgn: HRGN;
135   ABitmap: TBitmap;
136 begin
137   ABitmap := TBitmap.Create;
138   try
139     ABitmap.LoadFromFile('C:\MyImage.bmp');
140     ARgn := BitmapToRegion(ABitmap, clFuchsia);
141     SetWindowRgn(Form1.Handle, ARgn, True);
142   finally
143     ABitmap.Free;
144   end;
145 end;
146 
147 {This second example sets the region of a TPanel}
148 
149 procedure TForm1.Button1Click(Sender: TObject);
150 var
151   ARgn: HRGN;
152   ABitmap: TBitmap;
153 begin
154   ABitmap := TBitmap.Create;
155   try
156     ABitmap.LoadFromFile('C:\MyImage.bmp');
157     ARgn := BitmapToRegion(ABitmap, clFuchsia);
158     SetWindowRgn(Panel1.Handle, ARgn, True);
159   finally
160     ABitmap.Free;
161   end;
162 end;


From both examples, you can see how simple it is to simply specify the Handle of 
the window control that you wish to set the region of. Be it a TForm, TPanel, 
TMemo, etc.


			
Vote: How useful do you find this Article/Tip?
Bad Excellent
1 2 3 4 5 6 7 8 9 10

 

Advertisement
Share this page
Advertisement
Download from Google

Copyright © Mendozi Enterprises LLC