Export (0) Print
Expand All

Saving a Control Image to a Bitmap File

.NET Compact Framework 1.0
 

Geoff Schwab
Excell Data Corporation

Contributions by Tim Gerken
Microsoft Corporation

October 2003

Applies to:
   Microsoft® .NET Compact Framework 1.0
   Microsoft® Visual Studio® .NET 2003

Download sample.


Summary: This sample demonstrates how to create a .bmp file containing 16 or 24 bit bitmap data from a control. This method can be used to indirectly save an Image object.

Introduction
Bitmap File Structure
Creating and Initializing Headers
Working with Device Contexts and the DIB Section
Saving the Bitmap Data
Conclusion

Introduction

This example demonstrates how to load a bitmap from a file, draw it to a surface, allow the user to modify it, and then save it to a file. In the .NET Compact Framework, there is no simple method for saving Bitmap objects or getting a bitmap handle from a Bitmap object so it is necessary to use a bit of trickery to access a bitmap's pixel data. This example will access a bitmap's pixel data through a device context created from an editor control.

The sample source code demonstrates the following concepts:

  • Loading a bitmap
  • Displaying a bitmap on a control
  • Using Graphics to draw directly to a Bitmap object
  • Accessing a Control's DIB section
  • Saving a properly formatted bitmap (.bmp) file

For clarity, all P/Invoke declarations are separated into a Windows namespace in Windows.cs, the BitmapFile class, responsible for saving images, is in BitmapFile.cs, and the editor interface is in EditorForm.cs. This document will follow the flow of the code as the control's image is accessed and saved to a file. Describing the code in its entirety would needlessly complicate this document and distract from its purpose.

Bitmap File Structure

The bitmap file structure is relatively well documented so this is by no means a comprehensive description of the format.

The bitmap file structure is made up of a file header (BITMAPFILEHEADER), information header (BITMAPINFOHEADER), (optional) color table, and pixel data.

The file header (BITMAPFILEHEADER) is defined as:

ushort   bfType;      // File type designator "BM"
uint      bfSize;      // File size in bytes
short      bfReserved1;   // Unused
short      bfReserved2;   // Unused
uint      bfOffBits;      // Offset to get to pixel info

The info header (BITMAPINFOHEADER) is defined as:

uint      biSize;       // Size of this header         
int      biWidth;      // Width of image (pixels)
int      biHeight;      // Height of image (pixels
short      biPlanes;      // Number of color planes
short      biBitCount;      // Pixel bit depth (bits per pixel)
uint      biCompression;   // Compression type
uint      biSizeImage;   // Size of image (can be 0)
int      biXPelsPerMeter;   // Pixels per meter in x direction
int      biYPelsPerMeter;   // Pixels per meter in y direction
uint      biClrUsed;      // Number of colors (can be 0)
uint      biClrImportant;   // Important colors (can be 0)

The color table consists of colors which are accessed by the pixel data as indices into the color table. The size of the table depends on the size of the pixel index stored in biBitCount (1, 4, or 8 for indexed pixels). For example, if biBitCount is 4 then the color table can contain 24, or 16 colors. Each color in the table is a 32 bit BGRA formatted color.

This sample will utilize compression type BI_BITFIELDS for 16-bit images created in memory, meaning that immediately following the info header there will be three uint values defined in the color table which define the masking for red, green, and blue. These masks are used to extract the color information from the pixel data and are the only entries in the color table. These masks are not saved to the file version of the bitmap.

uint   redMask;      // Bitmask for red component 
uint   greenMask;      // Bitmask for green component 
uint   blueMask;      // Bitmask for blue component 

Lastly, the file contains the actual pixel data. For indexed bitmaps, these are the indices into the color table which define each pixel. For direct bitmaps, these are the actual pixel colors. Regardless of the format, each row of pixels must be aligned to 4 byte boundaries. Therefore, if a bitmap has a bit depth of 8 and is 2 pixels high and 3 pixels wide, the data in the file must be padded as below, where P represents a single 8-bit pixel index.

Row 1PPP0

Row 2 PPP0

This example results in the contiguous memory map: PPP0PPP0.

Note   Pixel data is actually stored bottom-up so the top line of the image corresponds to the bottom line of pixel data in the file.

Creating and Initializing Headers

The example included with this paper defines a BitmapFile class which encapsulates all of the functionality required to save a bitmap from a control to a file. The entry point to the code responsible for gaining access to the control's device context and saving the image to a file is the static function SaveToFile of the BitmapFile class, declared as:

static public bool SaveToFile(Control cont, String fileName, short bpp,
    int width, int height)

The high-level function calling summary and progression for saving a bitmap file is as follows:

SaveToFile()

BITMAPINFOHEADER ()Initialize info header

BITMAPFILEHEADER ()Initialize file header

BITMAPFILEHEADER.Store()Place file header in byte stream

BITMAPINFOHEADER.Store()Place info header in byte stream

CopyImageSource()Place pixel data in byte stream

File.Create()Create a destination file

FileStream.WriteWrite the byte stream to the file

The SaveToFile function defines three variables related to data and file streaming. These are declared outside of a try block so that they can be automatically cleaned up in a finally block.

// File and data streaming
BinaryWriter bw = null;
MemoryStream ms = null;
FileStream fs = null;

Within the BitmapFile class definition is the definition of two other classes which encapsulate the BITMAPFILEHEADER and BITMAPINFOHEADER structure data and share the same names as their counterparts. The first step to create and save a bitmap file is the instantiation of these two classes as defined in the following code from the SaveToFile method.

// Create the necessary bitmap file and info headers
BITMAPINFOHEADER infoHdr = new BITMAPINFOHEADER(bpp, width, height);
BITMAPFILEHEADER fileHdr = new BITMAPFILEHEADER(infoHdr);

The parameters to BITMAPINFOHEADER: bpp, width, and height are the pixel bit depth, pixel width, and pixel height of the image respectively. BITMAPFILEHEADER simply requires a reference to an initialized info header. The constructors of these classes fill in the associated header data, thus resulting in initialized instances of the header classes. The BITMAPINFOHEADER function initializes the data with the following code:

biSize = kBitmapInfoHeaderSize;
biWidth = w;         // Set the width
biHeight = h;         // Set the height
biPlanes = 1;         // Only use 1 color plane
biBitCount = bpp;         // Set the bpp
biCompression = 0;      // No compression for file bitmaps
biSizeImage = 0;         // No compression so this can be 0
biXPelsPerMeter = 0;      // Not used
biYPelsPerMeter = 0;      // Not used
biClrUsed = 0;         // Not used
biClrImportant = 0;      // Not used

Note   In order to minimize the complexity of the sample, only 16 and 24 bit direct formatted bitmaps are supported.

The associated BITMAPFILEHEADER is then created based on the info header as such:

bfType = fBitmapFileDesignator;
bfOffBits = fBitmapFileOffsetToData;
bfReserved1 = 0;
bfReserved2 = 0;

uint bytesPerPixel = (uint)(infoHdr.biBitCount >> 3);
uint extraBytes = ((uint)infoHdr.biWidth * bytesPerPixel) % 4;
uint adjustedLineSize = bytesPerPixel * ((uint)infoHdr.biWidth + 
  extraBytes);

sizeOfImageData = (uint)(infoHdr.biHeight) * adjustedLineSize;
bfSize = bfOffBits + sizeOfImageData;

This initialization code calculates the image size based on the pixel height, 4 byte aligned pixel width, and bit depth and stores the size for later reference.

The next step is the creation of a binary stream which will be used to store the file data.

byte[] bytes = new byte[fileHdr.bfSize];
ms = new MemoryStream(bytes);
bw = new BinaryWriter(ms);

Once the binary stream is created, the headers can be written with calls to their respective Store methods. Of course, these must be written in the order they appear in the file.

fileHdr.Store(bw);
infoHdr.Store(bw, false);

The code for these functions quite simply calls bw.Write for each parameter required in the file. The BITMAPFILEHEADER Store method is rather simple, as seen below.

public void Store(BinaryWriter bw)
{
    // Must, obviously, maintain the proper order for file writing
    bw.Write(bfType);
    bw.Write(bfSize);
    bw.Write(bfReserved1);
    bw.Write(bfReserved2);
    bw.Write(bfOffBits);
}

Meanwhile, the BITMAPINFOHEADER Store function is a slight bit more complicated. This method takes a bool as the second parameter which is used to determine whether to specify compression. This is only required when creating the DIB section, the details of which are discussed below. If compression is required and the bitmap is 16 bpp then the color masks will be specified immediately following the header. This function is shown in its entirety below.

public const int BI_BITFIELDS = 3;

public void Store(BinaryWriter bw, bool bFromDIB)
{
    // Must, obviously, maintain the proper order for file writing
    bw.Write(biSize);
    bw.Write(biWidth);
    bw.Write(biHeight);
    bw.Write(biPlanes);
    bw.Write(biBitCount);

    // Only use compression for memory DIB
    if (bFromDIB && biBitCount == 16)
        bw.Write(Windows.BI_BITFIELDS);
    else
        bw.Write(biCompression);

    bw.Write(biSizeImage);
    bw.Write(biXPelsPerMeter);
    bw.Write(biYPelsPerMeter);
    bw.Write(biClrUsed);
    bw.Write(biClrImportant);

    // RGBQUAD bmiColors[0]
    if (bFromDIB && biBitCount == 16)
    {
        bw.Write((uint)0x7c00);      // red
        bw.Write((uint)0x03e0);      // green
        bw.Write((uint)0x001f);      // blue
    }
}

Working with Device Contexts and the DIB Section

The complexity of this example resides in the creation of a device independent bitmap (DIB) section from a control and using this to copy pixel data to a buffer. This functionality is implemented in the CopyImageSource method of the BitmapFile class. This method involves an unsafe code block (for fixed pointer manipulation) and platform invoking of several GDI functions.

The first step in accessing the pixel data of the control is accessing the associated DeviceContext and creating a compatible context in memory. GetCapture is utilized here to access the HWND of the control. The P/Invoke of GetCapture is defined in Windows.cs.

[DllImport("coredll.dll", EntryPoint="GetCapture")]
public static extern IntPtr GetCapture();

bool captureState = cont.Capture;
cont.Capture = true;
IntPtr hwnd = Windows.GetCapture();
cont.Capture = captureState;

This handle is then passed to the method CopyImageSource of the BitmapFile class. This function is fully responsible for accessing the DeviceContext (DC) and DIB section, and then copying the pixel data to the byte stream. This function is declared as:

static protected bool CopyImageSource(IntPtr hwnd, byte[] bytes, 
    BITMAPFILEHEADER fileHdr, BITMAPINFOHEADER infoHdr) 

The HWND handle passed to the CopyImageSource function is used to access the control's device context and create a compatible memory device context.

[DllImport("coredll.dll", EntryPoint="GetDC")]
public static extern IntPtr GetDC(IntPtr hwnd);

[DllImport("coredll.dll", EntryPoint="CreateCompatibleDC")]
public static extern IntPtr CreateCompatibleDC(IntPtr hdc);

IntPtr hdc = Windows.GetDC(hwnd);
IntPtr hdcComp = Windows.CreateCompatibleDC(hdc);

Next, a dummy bitmap info header is created in order to gain access to the DIB data of the control's image. This dummy structure will be created in the style of the target bitmap format to force the GDI functions to do the necessary work in converting the pixel data. In this instance, the Store method of the BITMAPINFOHEADER class requires true as the second parameter to specify that this is a call to create a header for a DIB section, and thus requires compression information. This will ultimately specify to the Store function that if 16 bit pixels are the target format then it should write the proper compression information for the CreateDIBSection function.

byte[] dummyBitmapInfo = new byte[52];
MemoryStream msDummy = new MemoryStream(dummyBitmapInfo);
BinaryWriter bwDummy = new BinaryWriter(msDummy);
infoHdr.Store(bwDummy, true);

The next section of code requires some pointer manipulation and therefore must be done in an unsafe code block. The code first accesses a pointer to the dummy info structure, as well as a pointer to the position in the binary stream where the pixel data should be written via the file header's bfOffBits member.

fixed (byte* pBitmapInfo = &dummyBitmapInfo[0], pPixelDest = 
    bytes[fileHdr.bfOffBits])

Next a byte pointer is created. This pointer will be set by CreateDIBSection to point to the pixel data of the control's device context.

byte* pPixelSource;

The following call to CreateDIBSection will set the aforementioned pointers to the appropriate addresses based on what is stored in the supplied info header.

public const int DIB_RGB_COLORS = 0;

[DllImport("coredll.dll", EntryPoint="CreateDIBSection",
    SetLastError=true)]
public unsafe static extern IntPtr CreateDIBSection(IntPtr hdc,
    byte* pbmi, uint iUsage, byte** ppvBits, IntPtr hSection,
    uint dwOffset);
 
IntPtr hDibSect = Windows.CreateDIBSection(hdc, pBitmapInfo, 
    Windows.DIB_RGB_COLORS, &pPixelSource, (IntPtr)0, (uint)0);

CreateDIBSection's parameters probably require some explanation:

  • hdc – Handle to the control's device context
  • pBitmapInfo – Pointer to the dummy BITMAPINFOHEADER used by CreateDIBSection to determine the format of the output pixels
  • Windows.DIB_RGB_COLORS – Specifies that format of the color masks immediately following the info header is direct not palettized
  • &pPixelSource – The function will set this pointer to point at the place where the pixel data will be created (our copy source)

The DIB section has been created but still needs to be associated with a device context. The following code associates the DIB section with the device context that was created in memory with CreateCompatibleDC.

[DllImport("coredll.dll", EntryPoint="SelectObject")]
public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);

IntPtr hbmOld = Windows.SelectObject(hdcComp, hDibSect);

Now there is a memory device context with a DIB section so the pixel data in the control can be copied to it.

public const int SRCCOPY = 0x00CC0020;

[DllImport("coredll.dll", EntryPoint="BitBlt")]
public static extern bool BitBlt(IntPtr hdcDest, int nXDest,
    int nYDest, int nWidth, int nHeight, IntPtr hdcSrc,
    int nXSrc, int nYSrc, uint dwROP);

Windows.BitBlt(hdcComp, 0, 0, infoHdr.biWidth, infoHdr.biHeight,
    hdc, 0, 0, Windows.SRCCOPY);

Now that the memory DC contains the pixel data in its device context, it can be copied to the binary stream using the pPixelSource pointer. The destination of the copy is, of course, pPixelDest. This destination pointer was assigned to point at the current offset (immediately after the info header) in the byte stream so this copy will complete the bitmap file data.

[DllImport("coredll.dll", EntryPoint="memcpy")]
public unsafe static extern void CopyMemory(byte *pDest,
    byte *pSrc, int length);
 
Windows.CopyMemory(pPixelDest, pPixelSource,
    (int)fileHdr.sizeOfImageData);

Finally, it is time to restore and clean the contexts and resources.

[DllImport("coredll.dll", EntryPoint="DeleteObject")]
public static extern bool DeleteObject(IntPtr hObject);
 
[DllImport("coredll.dll", EntryPoint="DeleteDC")]
public static extern bool DeleteDC(IntPtr hdc);
 
[DllImport("coredll.dll", EntryPoint="ReleaseDC")]
public static extern int ReleaseDC(IntPtr hwnd, IntPtr hdc);
 
Windows.SelectObject(hdcComp, hbmOld);
Windows.DeleteObject(hDibSect);
bwDummy.Close();
msDummy.Close();
Windows.DeleteDC(hdcComp);
Windows.ReleaseDC(hwnd, hdc);

Saving the Bitmap Data

Once the binary stream fully contains the data required to save the bitmap it is a simple matter of creating a file stream, writing the bitmap data to it, and cleaning up.

fs = File.Create(fileName);
fs.Write(bytes, 0, (int)fileHdr.bfSize);

if (fs != null)
    fs.Close();
if (bw != null)
    bw.Close();
if (ms != null)
    ms.Close();

Conclusion

Accessing the DIB section of a control is by no means trivial, however the methods used in this example should provide a basis for expanding this sample to:

  • create a screen capture utility,
  • save Bitmap objects.

The latter of which being accomplished through the creation of a dummy control whose sole purpose is to be used as a temporary blitting surface for a Bitmap object.

The sample code also includes a function that saves an Image directly to a file by casting it to a Bitmap object and using GetPixel to access the pixel data. This method is extremely slow but is provided because of the simplicity of the method.

Show:
© 2014 Microsoft