Title: Optimized Bitmap to Region (HRGN) Conversion
Question: How to convert a bitmap with any pixel depth to HRGN (Region), choosing a color and specifying a color tolerance to be made transparent. Very fast!
Answer:
UPDATE 25/05/2000 - Please, don't forget to see article #1009 for the Delphi-like replacement for this function.
This article describes how to create a Window Region from any bitmap (of any color depth).
The code is extremely fast, processing 32-bit true color bitmaps in milliseconds. The code wasn't originally created by me, but the translation to Object Pascal is my work.
This article is also a very good example on how to convert C++ code to Object Pascal. The translated code was deliberately left as close to the original as possible for you to understand the subtilities of translating C++ to Delphi.
The source code could be made much smaller and easier to understand (for the beginner) if we used delphi classes (TBitmap) to do the work (using TBitmap.ScanLines property among others) instead of using API functions, but then this article would not be used as a reference to converting C++ code to Delphi. I can submit the "Delphi-Like" version if there is interest on it. Please let me know if it is of your interest. Also, this procedure is used in a component I wrote called TFormShapper which allows you to apply some predefined shapes to a form (circle, ellipsis, rounded rectangle, etc.) or choose a bitmap to be used as a mask for the shape of the form. I will submit an article about TFormShapper later.
What the function does is very simple (the implementation is not, as you can see for yourself):
1. Creates a 32-bit true color bitmap the same size of the original bitmap
passed in hBmp. This bitmap is a DIBSection and is stored in hBmp32
2. Allocs memory for an area to store PRgnData which will be passed to
ExCreateRegion. This memory area will contain TRect's created to
describe the region.
3. Find the consecutive pixels of the same color or under the tolerance
defined and creates a rect on the area described above with the
dimensions of the area of equal pixels. After doing if for all the
image pixels we will have the "Rects" for every "transparent to-be"
areas of the Bitmap.
4. Returns the HRGN returned from the ExCreateRegion, which received the
PRgnData with all the rects to create our region.
For some reason and limitations on Windows 98 ExtCreateRegion may fail if the number of rectangles is too large (ie: 4000). The solution was to call it in multiple spets and combining the regions afterwards.
It is very simple to call this routine, but since the first parameter is a HBITMAP, some people could get confused on how to use it. It is easy: just call it passing the TBimap.Handle property, e.g.:
procedure TForm1.ApplyShapeToFormFromFile(const FileName: string); // what a
long procedure name...
var
Bitmap: TBimap;
rgn: HRGN;
begin
Bitmap := TBitmap.Free;
try
Bitmap.LoadFromFile(FileName);
rgn := CreateRegion(Bitmap.Handle);
SetWindowRgn(Self.Handle { form (window) handle }, rgn, True { repaint
window });
// do not DeleteObject the HRGN because it is now owned by the operating
system
finally
Bitmap.Free;
end;
end;
The procedure default values for TransparenColor is black and for tolerance is 0 (which means no tolerance). So all black pixels will be turned transparent as default. Simply pass the other two optional parameters to change this behavior.
I classified this procedure Delphi 4+ compatible because it uses optional parameters, if you ommit them then this procedure will work fine with Delphi 3.
Thank you for you interest, and please study the code carefully if you want to really understand it.
Felipe Rocha Machado
***
*** Delphi code begins here (following it is the original C++ code for your
*** reference...)
***
{---
BitmapToRegion - The code below is a translation from the original C
language version by Jean-Edouard Lachand-Robert
hBmp : Source bitmap
cTransparentColor: Color base for the "transparent" pixels(default black)
cTolerance : Color tolerance for the "transparent" pixels.
A pixel is assumed to be transparent if the value of each of its 3 components (blue, green and red) is greater or equal to the corresponding value in cTransparentColor and is lower or equal to the corresponding value in cTransparentColor + cTolerance.
--- }
function BitmapToRegion(hBmp: HBITMAP; cTransparentColor: COLORREF=0;
cTolerance: COLORREF=$101010): HRGN;
const
AllocUnit = 100;
MaxRects: DWORD = AllocUnit;
type
TRectArray = Array[0..0] of TRect;
LONG = LongInt;
PLONG = ^LONG;
var
h, Rgn: HRGN;
hMemDC, h_DC: HDC;
hBmp32, HoldBmp: HBITMAP;
bm, bm32: BITMAP;
RGB32BITSBITMAPINFO: BITMAPINFOHEADER;
BITMAP_INFO: ^BITMAPINFO;
pbBits32: pointer;
hData: THandle;
pData: PRgnData;
lr, lg, lb, hr, hg, hb: Byte;
b: Byte;
p32: PByte;
p: PLONG;
x,y, x0: Integer;
pr: ^TRectArray;
begin
Rgn := 0;
{ Create a memory DC inside which we will scan the bitmap content }
hMemDC := CreateCompatibleDC(0);
if hMemDC 0 then
begin
{ get Bitmap size }
GetObject(hBmp, SizeOf(bm), @bm);
{ Create a 32-bit depth bitmap and select it into the memory DC }
with RGB32BITSBITMAPINFO do
begin
biSize := sizeof(BITMAPINFOHEADER); // biSize
biWidth := bm.bmWidth; // biWidth;
biHeight := bm.bmHeight; // biHeight;
biPlanes := 1; // biPlanes;
biBitCount := 32; // biBitCount
biCompression := BI_RGB; // biCompression;
biSizeImage := 0; // biSizeImage;
biXPelsPerMeter := 0; // biXPelsPerMeter;
biYPelsPerMeter := 0; // biYPelsPerMeter;
biClrUsed := 0; // biClrUsed;
biClrImportant := 0; // biClrImportant;
end;
BITMAP_INFO := @RGB32BITSBITMAPINFO; // points to the previous structure
hBmp32 := CreateDIBSection(hMemDC, BITMAP_INFO^, DIB_RGB_COLORS,
pbBits32, 0, 0);
if hBmp32 0 then
begin
HoldBmp := SelectObject(hMemDC, hBmp32);
{ Create DC just to copy bitmap into the memory DC }
h_DC := CreateCompatibleDC(hMemDC);
if h_DC 0 then
begin
{ Get how many bytes per row we have for the bitmap bits (rounded up
to 32 bits) }
GetObject(hBmp32, SizeOf(bm32), @bm32);
while (bm32.bmWidthBytes mod 4 0) do
Inc(bm32.bmWidthBytes);
{ Copy the bitmap into the memory DC }
HoldBmp := SelectObject(h_DC, hBmp);
BitBlt(hMemDC, 0, 0, bm.bmWidth, bm.bmHeight, h_DC, 0, 0, SRCCOPY);
{ For better performance, we will use the ExtCreateRegion() function
to create the region. This function take a RGNDATA structure on
entry. We will add rectangles by amount of ALLOC_UNIT number in this
structure. }
MaxRects := AllocUnit;
hData := GlobalAlloc(GMEM_MOVEABLE, SizeOf(RGNDATAHEADER) +
(SizeOf(TRect) * maxRects));
pData := GlobalLock(hData);
with pData^.rdh do
begin
dwSize := SizeOf(RGNDATAHEADER);
iType := RDH_RECTANGLES;
nCount := 0;
nRgnSize := 0;
SetRect(rcBound, MAXLONG, MAXLONG, 0, 0);
end;
{ Keep on hand highest and lowest values for the "transparent"
pixels }
lr := GetRValue(cTransparentColor);
lg := GetGValue(cTransparentColor);
lb := GetBValue(cTransparentColor);
hr := Min($ff, lr + GetRValue(cTolerance));
hg := Min($ff, lg + GetGValue(cTolerance));
hb := Min($ff, lb + GetBValue(cTolerance));
{ Scan each bitmap row from bottom to top (the bitmap is inverted
vertically) }
p32 := PByte(Integer(bm32.bmBits) + (bm32.bmHeight - 1) *
bm32.bmWidthBytes);
for y := 0 to bm.bmHeight - 1 do
begin
{ Scan each bitmap pixel from left to right }
// for x := 0 to bm.bmWidth - 1 do
x := 0;
while x begin
{ Search for a continuos range of "non transparent pixels" }
x0 := x;
p := PLONG(Integer(p32)+x*SizeOf(LONG));
while x begin
b := GetRValue(p^);
if (b = lr) and (b begin
b := GetGValue(p^);
if (b = lg) and (b begin
b := GetBValue(p^);
if (b = lb) and (b begin
Break; // this pixel is transparent
end;
end;
end;
inc(p);
inc(x);
end; // while x
if x x0 then
begin
{ Add the pixels (x0, y) to (x, y+1) as a new rectangle in
the region }
if pData^.rdh.nCount = maxRects then
begin
GlobalUnlock(hData);
Inc(maxRects,AllocUnit);
hData := GlobalReAlloc(hData, SizeOf(RGNDATAHEADER) +
(SizeOf(TRect) * MaxRects), GMEM_MOVEABLE);
pData := GlobalLock(hData);
end;
pr := @pData^.Buffer;
SetRect(pr^[pData^.rdh.nCount], x0, y, x, y+1);
with pData^.rdh do
begin
if x0 if y if x rcBound.Right then rcBound.Right := x;
if y+1 rcBound.Bottom then rcBound.Bottom := y+1;
Inc(nCount);
end;
end;
{ On Windows98, ExtCreateRegion() may fail if the number of
rectangles is too large (ie: 4000). Therefore, we have to
create the region by multiple steps. }
if pData^.rdh.nCount = 2000 then
begin
h := ExtCreateRegion(nil, SizeOf(RGNDATAHEADER) +
(SizeOf(TRect) * maxRects), pData^);
if Rgn 0 then
begin
CombineRgn(Rgn, Rgn, h, RGN_OR);
DeleteObject(h);
end
else
Rgn := h;
pData^.rdh.nCount := 0;
SetRect(pData^.rdh.rcBound, MAXLONG, MAXLONG, 0, 0);
end;
Inc(x);
end; { for x := 0 to bm.Width - 1 (used a While loop to be able
to make Inc(x);) }
{ Go to next row (remember, the bitmap is inverted vertically) }
Dec(p32, bm32.bmWidthBytes);
end; // for y := 0 to bm.Height - 1
{ Create or extend the region with the remaining rectangles }
h := ExtCreateRegion(nil, SizeOf(RGNDATAHEADER) + (SizeOf(TRect) *
MaxRects), pData^);
if Rgn 0 then
begin
CombineRgn(Rgn, Rgn, h, RGN_OR);
DeleteObject(h);
end
else
Rgn := h;
GlobalFree(hData);
SelectObject(h_DC, holdBmp);
DeleteDC(h_DC);
end; // if h_DC 0
DeleteObject(SelectObject(hMemDC, holdBmp));
end; // if hBmp32 0
DeleteObject(hMemDC);
end; // if hMemDC 0
Result := Rgn;
end;
***
*** END OF DELPHI CODE ***
***
*** C++ Code - Reference only - you may skip this if you wish ***
//
// BitmapToRegion : Create a region from the "non-transparent"
pixels of a bitmap
// Author : Jean-Edouard Lachand-Robert (http:
//www.geocities.com/Paris/LeftBank/1160/resume.htm), June 1998.
//
// hBmp : Source bitmap
// cTransparentColor : Color base for the "transparent" pixels
(default is black)
// cTolerance : Color tolerance for the "transparent"
pixels.
//
// A pixel is assumed to be transparent if the value of each of its 3
components (blue, green and red) is
// greater or equal to the corresponding value in cTransparentColor and
is lower or equal to the
// corresponding value in cTransparentColor + cTolerance.
//
HRGN BitmapToRegion (HBITMAP hBmp, COLORREF cTransparentColor = 0, COLORREF
cTolerance = 0x101010)
{
HRGN hRgn = NULL;
if (hBmp)
{
// Create a memory DC inside which we will scan the bitmap content
HDC hMemDC = CreateCompatibleDC(NULL);
if (hMemDC)
{
// Get bitmap size
BITMAP bm;
GetObject(hBmp, sizeof(bm), &bm);
// Create a 32 bits depth bitmap and select it into the memory DC
BITMAPINFOHEADER RGB32BITSBITMAPINFO = {
sizeof(BITMAPINFOHEADER), // biSize
bm.bmWidth, // biWidth;
bm.bmHeight, // biHeight;
1, // biPlanes;
32, // biBitCount
BI_RGB, // biCompression;
0, // biSizeImage;
0, // biXPelsPerMeter;
0, // biYPelsPerMeter;
0, // biClrUsed;
0 // biClrImportant;
};
VOID * pbits32;
HBITMAP hbm32 = CreateDIBSection(hMemDC, (BITMAPINFO *)
&RGB32BITSBITMAPINFO, DIB_RGB_COLORS, &pbits32, NULL, 0);
if (hbm32)
{
HBITMAP holdBmp = (HBITMAP)SelectObject(hMemDC, hbm32);
// Create a DC just to copy the bitmap into the memory DC
HDC hDC = CreateCompatibleDC(hMemDC);
if (hDC)
{
// Get how many bytes per row we have for the bitmap bits (rounded
up to 32 bits)
BITMAP bm32;
GetObject(hbm32, sizeof(bm32), &bm32);
while (bm32.bmWidthBytes % 4)
bm32.bmWidthBytes++;
// Copy the bitmap into the memory DC
HBITMAP holdBmp = (HBITMAP)SelectObject(hDC, hBmp);
BitBlt(hMemDC, 0, 0, bm.bmWidth, bm.bmHeight, hDC, 0, 0, SRCCOPY);
// For better performances, we will use the ExtCreateRegion()
function to create the
// region. This function take a RGNDATA structure on entry. We
will add rectangles by
// amount of ALLOC_UNIT number in this structure.
#define ALLOC_UNIT 100
DWORD maxRects = ALLOC_UNIT;
HANDLE hData = GlobalAlloc(GMEM_MOVEABLE, sizeof(RGNDATAHEADER) +
(sizeof(RECT) * maxRects));
RGNDATA *pData = (RGNDATA *)GlobalLock(hData);
pData-rdh.dwSize = sizeof(RGNDATAHEADER);
pData-rdh.iType = RDH_RECTANGLES;
pData-rdh.nCount = pData-rdh.nRgnSize = 0;
SetRect(&pData-rdh.rcBound, MAXLONG, MAXLONG, 0, 0);
// Keep on hand highest and lowest values for the "transparent"
pixels
BYTE lr = GetRValue(cTransparentColor);
BYTE lg = GetGValue(cTransparentColor);
BYTE lb = GetBValue(cTransparentColor);
BYTE hr = min(0xff, lr + GetRValue(cTolerance));
BYTE hg = min(0xff, lg + GetGValue(cTolerance));
BYTE hb = min(0xff, lb + GetBValue(cTolerance));
// Scan each bitmap row from bottom to top (the bitmap is inverted
vertically)
BYTE *p32 = (BYTE *)bm32.bmBits + (bm32.bmHeight - 1) *
bm32.bmWidthBytes;
for (int y = 0; y {
// Scan each bitmap pixel from left to right
for (int x = 0; x {
// Search for a continuous range of "non transparent pixels"
int x0 = x;
LONG *p = (LONG *)p32 + x;
while (x {
BYTE b = GetRValue(*p);
if (b = lr && b {
b = GetGValue(*p);
if (b = lg && b {
b = GetBValue(*p);
if (b = lb && b // This pixel is "transparent"
break;
}
}
p++;
x++;
}
if (x x0)
{
// Add the pixels (x0, y) to (x, y+1) as a new rectangle in
the region
if (pData-rdh.nCount = maxRects)
{
GlobalUnlock(hData);
maxRects += ALLOC_UNIT;
hData = GlobalReAlloc(hData, sizeof(RGNDATAHEADER) +
(sizeof(RECT) * maxRects), GMEM_MOVEABLE);
pData = (RGNDATA *)GlobalLock(hData);
}
RECT *pr = (RECT *)&pData-Buffer;
SetRect(&pr[pData-rdh.nCount], x0, y, x, y+1);
if (x0 rdh.rcBound.left)
pData-rdh.rcBound.left = x0;
if (y rdh.rcBound.top)
pData-rdh.rcBound.top = y;
if (x pData-rdh.rcBound.right)
pData-rdh.rcBound.right = x;
if (y+1 pData-rdh.rcBound.bottom)
pData-rdh.rcBound.bottom = y+1;
pData-rdh.nCount++;
// On Windows98, ExtCreateRegion() may fail if the number of
rectangles is too
// large (ie: 4000). Therefore, we have to create the
region by multiple steps.
if (pData-rdh.nCount == 2000)
{
HRGN h = ExtCreateRegion(NULL, sizeof(RGNDATAHEADER) +
(sizeof(RECT) * maxRects), pData);
if (hRgn)
{
CombineRgn(hRgn, hRgn, h, RGN_OR);
DeleteObject(h);
}
else
hRgn = h;
pData-rdh.nCount = 0;
SetRect(&pData-rdh.rcBound, MAXLONG, MAXLONG, 0, 0);
}
}
}
// Go to next row (remember, the bitmap is inverted vertically)
p32 -= bm32.bmWidthBytes;
}
// Create or extend the region with the remaining rectangles
HRGN h = ExtCreateRegion(NULL, sizeof(RGNDATAHEADER) +
(sizeof(RECT) * maxRects), pData);
if (hRgn)
{
CombineRgn(hRgn, hRgn, h, RGN_OR);
DeleteObject(h);
}
else
hRgn = h;
// Clean up
GlobalFree(hData);
SelectObject(hDC, holdBmp);
DeleteDC(hDC);
}
DeleteObject(SelectObject(hMemDC, holdBmp));
}
DeleteDC(hMemDC);
} // if hMemDC
} // if hBmp
return hRgn;
}