Search code examples
c#zplbitmapdata

Unnecessary black bar when convert a Bitmap to Byte Array


I wrote a easy program to convert a monochrome bitmap data to a byte array, and then reverse it (0->1, 1->0) in order to send it as a ZPL to a label printer.

Bitmap bmp = (Bitmap)pictureBox1.Image;
Rectangle rectangle = new Rectangle(0, 0, bmp.Width, bmp.Height);
BitmapData bd = bmp.LockBits(rectangle, ImageLockMode.ReadOnly, bmp.PixelFormat);
IntPtr ptr = bd.Scan0;
byte[] b = new byte[Math.Abs(bd.Stride) * bmp.Height];
Marshal.Copy(ptr, b, 0, b.Length);
bmp.UnlockBits(bd);
StringBuilder sb = new StringBuilder();
foreach (byte i in b)
{
    sb.Append((255 - i).ToString("X2"));

}
textBox1.Text = string.Format(@"^XA^FO100,0^GFA,{0},{0},{1},{2}^FS^XZ", b.Length, Math.Abs(bd.Stride), sb.ToString());

But it draws an unnecessary black bar that was originally not there!

(Curtesy of Line image) enter image description here

I first thought it might be because the rectangle I assigned to the BitmapData was too big so there included some "unused" areas, but apparently I was wrong and the 07FFFFs was still there!

I even tried to just replace it with empty string for the hack of it!

But it was wrong because it mess up the ZPL output!

Of course I could just store the replaced string and recalculate the length for the ^GFA command, but what if the picture "just so happen" to have some 07FFFFs in itself!? I essentially mess up the picture itself by doing so!

The 07FFFF was "reversed" by my code, so it was F80000 in the original byte array, so I figure it might be some thing similar to "\n" in the bitmap data to tell the picture to "keep drawing from the new line"!

And I can't find anything online explaining why there are F80000s in the bitmap files.

It's all good for the picture, but how could I "get rid of it"!?

I "only" want the data that was "the picture itself".

Could somebody please be so kind and help me out!?

Much appreciated!


Solution

  • The Reason

    The Stride property gives you the length of each scanline of the image in bytes, rounded up the next 4-byte boundary. And if a line in the image does not fill the whole scanline, it is padded with 0. Your code processed and included this padding data in the ^GFA command producing the black bars on the right side. If you don't invert the image, you won't notice the problem.

    You need to invert the image because the value of each pixel is not only black and white. It is an index to a two-color table. And each entry in the color table defines the actual color of the pixel in RGB. In your case Color[0]=black and Color[1]=white.

    With the pixel format of Format1bppIndexed you get additional complexity if the image width is not a multiple of 8. Then there are padding bits in the last used byte of each line in the image.

    Let's take your image as an example. You have an image width of 173. Each line need at least 22 (=ceil(173 / 8)) bytes to store the data of one line. In the last byte, only the first 5 bits (=173 % 8) have real pixel data. The next multiple of 4 above 22 is 24. So each line takes 24 bytes of memory. The additional 2 bytes are padding bytes filled with zeros.

    The ZPL Command ^GFA

    The Format: ^GFa,b,c,d,data where a, b, c, d and data are parameters to the command.

    • a: A for ASCII-HEX (Base16), B binary
    • b: binary byte count; if ASCII is used, b must be equal to c
    • c: graphic field count; the number of bytes comprising the image (width [in bytes]*height)
    • d: bytes per row
    • data: monochrome image data in the specified format

    As far as I understand the documentation of the command, it only supports image widths that are a multiple of 8, because you can only specify the number of bytes for a row/line. If the source image does not have the correct width, you can expand the image where all the padding bits set to 0/white/inactive.

    The Solution

    I have implemented two methods to solve the problem. The first one reads a monochrome bitmap and returns a StringBuilder object with the appropriate ^GFAcommand.

    private static StringBuilder ZPLCommandFromMonochromeFile(string pFileName)
    {
        var sb = new StringBuilder();
    
        using (var bmp = new Bitmap(pFileName))
        {
            if (bmp.PixelFormat != PixelFormat.Format1bppIndexed)
                throw new InvalidOperationException($"We only suppoert monochrome images, file {pFileName} is invalid.");
    
            var rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
            byte[] data = null;
            int stride = 0;
            var bd = bmp.LockBits(rect, ImageLockMode.ReadOnly, bmp.PixelFormat);
            try
            {
                stride = Math.Abs(bd.Stride);
                data = new byte[stride * bmp.Height];
                System.Runtime.InteropServices.Marshal.Copy(bd.Scan0, data, 0, data.Length);
            }
            finally
            {
                bmp.UnlockBits(bd);
            }
    
            ZPLCommandFromArray(sb, data, stride, bmp.Width, bmp.Height);
        }
    
        return sb;
    }
    

    ZPLCommandFromArray converts the byte array to the image data for the ^GFA command.

    private static void ZPLCommandFromArray(StringBuilder dest, byte[] data, int stride, int width, int height)
    {
        // calc the length of the destination line in bytes
        // this is the nr of bytes in a line where all 8 bits are used for the picture
        var len8 = width / 8;
        // this is the number of trailing pixels for a line if the width is not a multiple of 8
        var bits = width % 8; 
        // the minimum nr of bytes needed for a line 
        var len = len8 + (bits > 0 ? 1 : 0);
    
        // the number of bits we have to mask out if the width is not a multiple
        // of 8. e.g if the width is 19, we only may change 3 bits of the last byte
        var mask = (byte)(~(0xFF >> bits));
    
        // in the output graphics, the width will always be a multiple of 8, because
        // in the ZPL command ^GFA you can only specify the width of the imagen in 
        // number bytes.
        dest.AppendFormat("^GFA,{0},{1},{2},", len * height, len * height, len);
        dest.AppendLine();
    
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < len8; x++)
                dest.AppendFormat("{0:X2}", (byte)~data[y * stride + x]);
    
            if (bits > 0)
                dest.AppendFormat("{0:X2}", (byte)((~data[y * stride + len8]) & mask));
    
            dest.AppendLine();
        }
    }