Details from the Anatomy of a BitMap (from the .Net BitMap Class)

Posted on 2013-01-09
Last Modified: 2013-01-16
Howdy, Experts!

     Here's the situation: it has been discovered that - to properly send a graphic image to a Zebra ZPL printer - all one really needs to know is its [Width], its [Height], and have a byte array of the image data to send to the printer.  Nothing more esoteric than that.

The data that is you need to send isn't a PNG. You need to take the image and convert it to black and white and send the data to the printer. For example, if you have an image that is 40x50px you would take the image, and strip out the color information so you would have a total of 2000 bits of data. Then send your ZPL down like ^GFB,250,250,5,{2000 bits of data}.

I got 250 by taking 2000 bits / 8 (bits / byte) to get 250 bytes. I got 5 by dividing 40 by 8. The number of rows will be calculated automatically.

Something that I find useful when dealing with Zebra printers is to think in terms of bits. All graphics are done on a bit level.

Remember that the image you are going to send down will change size depending on the DPI of the printer. A 203 DPI printer will show my example at about .2in x .25in. On a 300 DPI printer it will show at about .13in by .16in. This is because the printer will just place raw data onto the format and the number of px is the number of dots the image will be.

References: [1] ZPL Manual on page 208 (^GF page2).

     ...until I decided to accomplish this from an interface in my class/object that accepts a BitMap.  Makes sense, right?  Send the class a BitMap, and it will handle getting it over to the printer, get it printed.  Only it's not that easy.

     I need to somehow figure out where the header ends and the (raw?) image data begins - perhaps somehow making sure that the image data is 1BPP monochrome (a thermal printer doesn't print greyscales very well, yet...) - only I can't seem to figure out this information from the BitMap class.  Frustrating!

     The hitch: this thing has to operate from the BitMap class: no files included.  Think about it: what if the image being "fed" comes from a resource file?  Or was 'punched in' to a PictureBox from a user-selected image, perhaps pasted off the clipboard?  Because these are all "realistic" scenarios, I am informed that it must perform its mastery in memory, preferrably with the BitMap which has been passed to it.  According to the powers that be, "it's enough information to put that image on a screen somewhere, so it should be more than plenty to send to the printer".  (An almost direct quote.)

     So I think I've got a two-fold mission, here: figure out where the header ends in a given BitMap in order to (somehow) grab a byte array representing the 1BPP image data from that same BitMap, and (somehow) figure out if said BitMap is the correct format in the first place.

     I'm not sure the BitMap PixelFormat can be trusted: I converted a PNG image to 1BPP color, and it (somehow) saved as a 16-color indexed format - yet the data was pure b/w image.  So I'm not sure if that's a realistic expectation, especially based on limitations of file formats.

     So I'm more interested in figuring out the "dividing line" between the header and the raw image data, and how I might pluck that raw image data into a Byte Array for transmission to a ZPL printer for other nefarious purposes.

* I included the back information on the Zebra ZPL printers just in case some inquiring mind in similar straits can benefit from this question as well.  Sometimes it helps when you know what I plan to do with this information...      ...ultimately, when I am finished with this segment of the application, I will be able to grab an image, make sure it's 1BPP (or convert it/dither it until it is), get the size of it, and transfer just the 1's and 0's to the ZPL printer in order to cause it to properly display that image, somewhere on the printout.  I know how to send the data, I even know what data I need to send.  I just need to somehow distill the data from a BitMap class entity --- and that's not as easy as it seems.
Question by:LongFist
  • 2
  • 2
LVL 37

Accepted Solution

TommySzalapski earned 500 total points
ID: 38760152
Since you likely aren't processing dozens of images per second, you could just use the height and width and the GetPixel method do extract all the data into your own array. This way you know what the data is and it doesn't really matter what format it is in. You can also force it to be monochrome by putting whatever values come out for the pixels through a threshold.

Author Comment

ID: 38760170
An interesting concept.  Not exactly what I had in mind, but incredibly efficient nonetheless.  That just might work.  Let me test that...

Thresholding.  Thresholding.  I'm going to have to see if I've already visited that particular issue before - I may have, once or twice before...

Thank you for your prompt response.  Let me see if I can make that work...
LVL 37

Expert Comment

ID: 38760336
By threshold, I just mean that you convert any image to monochrome. For example, say you have one byte per pixel greyscale image. If GetPixel returns something > 128 use white else use black. Or if 128 isn't the best threshold value for you, use something else.

Also if you are converting everything to bits, you can't use the raw bitmap data even if you could find it. You'll have to use bit operators.

Would look something like this in C++
const int THRESHOLD = 128;
// Get the size (in bits)
int size = bmp.Height*bmp.Width;
// Convert to bytes
int buffer_size = (size + 7)/8; //+7 to make sure it rounds up.

// Create the array and zero it out
char* buffer = new buffer[buffer_size];
memset(buffer, 0, buffer_size);

for(int i = 0; i < size; ++i)
   // i/width gives the row, i&width gives the column
   if(bmp.GetPixel(i/bmp.Width, i%bmp.Width) > THRESHOLD)
      buffer[i/8] |= 1 << (7-i%8);


delete[] buffer;

Open in new window

Breaking down buffer[i/8] |= 1 << (7-i%8);
i/8 goes from the pixel to the byte so you are changing the correct byte
|= will make whatever bits are set on the right side to be set on the left side as well
<< is bit shift (1<<5 gives this in binary 00100000)
So for the third pixel, i = 2 so (7-i%8) is 5 so it sets the third bit in that byte.
LVL 16

Expert Comment

ID: 38762932
A bitmap file consists of 4 structures or data blocks one followed by the other:

RGB Info
Scan Lines

If you're talking about a bitmap in memory, there is only the last three structures (i.e. no BITMAPFILEHEADER).

Years ago I wrote a subroutine that would decode all this stuff for me.  I defined my own structure call 'BitmapScanLines' consisting of the pertanent data about the bitmap I (and likly you) wanted.  The subroutine takes a pointer to 'BitmapScanLines' and a pointer to a BITMAPINFO and decodes the data in BITMAPINFO to populate 'BitmapScanLines'.  The code is written in C, but you should be able to decode it for C#.

typedef struct                    //All the pertenent data needed to efficiently process a Bitmap
    //The Entire Bitmap
    BITMAPINFO* pBitmap;

    //Cached Data
    long RGBCount;              //Number of Colors in RGB table
    unsigned char* pImage;      //Pointer to the Image Data (Scan Lines)
    long Width;                 //Width of Bitmap (FullBytesPerScanLine * 8 + LastByteBitCount)
    long Height;                //Number os Scanlines in a Bitmap
    long DPIX;                  //Approximate DPI in X direction
    long DPIY;                  //Approximage DPI in Y direction

    //Calculated ScanLine Data
    long FullBytesPerScanLine;  //Number of Bytes per scan line that all 8 bits contain Image Data
    long LastByteBitCount;      //Number of Bits in the last Byte of each scan line that contains ImageData
                                //    (Significant bits in Byte#     FullBytesPerScanLine + 1)(can be zero if Width is evenly divisable by 8)
    long ImageBytesPerScanLine; //Number of Bytes with ANY Image Data (excludes Extra Bytes)
    long ExtraBytesPerScanLine; //Number of unused Bytes at the end of a scan line (Scan Lines must end on DWORD boundary)
    long BytesPerScanLine;      //Number of Bytes per scan line (including Extra Bytes)
    long BytesPerImage;         //Number of Bytes for the entire image (including all Extra Bytes)
} BitmapScanLines;

Open in new window

//Given a BitmapInfo and a BitmapScanLines Structure
//    Initialize the BitmapScanLines Structure with data from BitmapInfo
//    Return 0 on success, a Negative Number on Failure
long __declspec(dllexport) FAR PASCAL LoadBitmapScanLines( BitmapScanLines* pScanLines, BITMAPINFO* pBitmap )

    //Validate Compression
    if( pHeader->biCompression != BI_RGB )
        return -1;    //Only Uncompressed RGB bitmaps are currently supported

    //Store location of Bitmap
    pScanLines->pBitmap = pBitmap;

    //Find Raw Image Data by finding number of RGB values 
    switch( pHeader->biBitCount )
        case 0:  return -2;    //JPEG and PNG formats are not currently supported
        case 1:  pScanLines->RGBCount = 2;                                                                                break;
        case 4:  pScanLines->RGBCount = pHeader->biClrUsed; if( pScanLines->RGBCount == 0 ) pScanLines->RGBCount = 16;    break;
        case 8:  pScanLines->RGBCount = pHeader->biClrUsed; if( pScanLines->RGBCount == 0 ) pScanLines->RGBCount = 256;   break;
        case 16: pScanLines->RGBCount = pHeader->biClrUsed; if( pScanLines->RGBCount == 0 ) pScanLines->RGBCount = 65536; break;
        case 24: pScanLines->RGBCount = 0; /* No Color Pallet because each pixel is an RGB color */                       break;    
        case 32: pScanLines->RGBCount = 0; /* No Color Pallet because each pixel is an RGB color */                       break;
        default: return -3;    //Unknown BitCount
    pScanLines->pImage = (unsigned char*) pBitmap + sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * pScanLines->RGBCount;

    //Load Cached Values
    pScanLines->Width  =  pHeader->biWidth;
    pScanLines->Height =  pHeader->biHeight;
    pScanLines->DPIX   = (pHeader->biXPelsPerMeter * 254 + 5000) / 10000;   //+5000 to force Rounding
    pScanLines->DPIY   = (pHeader->biYPelsPerMeter * 254 + 5000) / 10000;    //+5000 to force Rounding

    //Load Calculated ScanLine Data
    pScanLines->FullBytesPerScanLine = pScanLines->Width / 8;
    pScanLines->LastByteBitCount = pScanLines->Width - pScanLines->FullBytesPerScanLine * 8;
    pScanLines->ImageBytesPerScanLine = pScanLines->FullBytesPerScanLine + (pScanLines->LastByteBitCount ? 1 : 0);
    pScanLines->ExtraBytesPerScanLine = 4 - pScanLines->ImageBytesPerScanLine % 4; 
    if( pScanLines->ExtraBytesPerScanLine == 4 ) pScanLines->ExtraBytesPerScanLine = 0;
    pScanLines->BytesPerScanLine = pScanLines->ImageBytesPerScanLine + pScanLines->ExtraBytesPerScanLine;
    pScanLines->BytesPerImage = pScanLines->Height * pScanLines->BytesPerScanLine;

    return 0;

Open in new window


Author Closing Comment

ID: 38783756
Sometimes the simplest answers are the best.  Processing the image down to 1BPP, then running through it for b/w (0/1) values was the answer.  As we are only sending 1 graphic label at a time, and the application is not time-intensive, this solution works best.

Took some time to get it working properly, but it's pretty bullet-proof.  Thank you!

Featured Post

Independent Software Vendors: We Want Your Opinion

We value your feedback.

Take our survey and automatically be enter to win anyone of the following:
Yeti Cooler, Amazon eGift Card, and Movie eGift Card!

Question has a verified solution.

If you are experiencing a similar issue, please ask a related question

Suggested Solutions

In this post we will learn how to make Android Gesture Tutorial and give different functionality whenever a user Touch or Scroll android screen.
This article aims to explain the working of CircularLogArchiver. This tool was designed to solve the buildup of log file in cases where systems do not support circular logging or where circular logging is not enabled
I've attached the XLSM Excel spreadsheet I used in the video and also text files containing the macros used below.…

685 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question