DrawingContext to Bitmap file

5k views Asked by At

I have a DrawingContext (part of a Visual or a DrawingGroup), where I draw a bunch of rectangles and/or 1-bit images on top of each other. Think of it as a masking 1-bit image. I would like to convert this into a bitmap image file.

Using RenderTargetBitmap is not an option because it can only render at 32bit pixel format, so if I have to render a 20MB 1-bit image, I will end up with a 640MB (20*32) of memory on my heap. This of course creates magnificent LOH fragmentation, and the application runs out-of-memory on the second shot.

So, I basically need a way to write a 1-bit bitmap file from a drawing context efficiently. Any ideas/suggestions/alternate methods would be appreciated.

3

There are 3 answers

2
mlemay On

What about PixelFormats

Edit: (thanks to Anders Gustafsson)

The lower is PixelFormats.BlackWhite, with 1bit per pixel.

So this way, you can convert any BitmapImage to a FormatConvertedBitmap, where you can modify the format to a lower bpp.

0
Erti-Chris Eelmaa On

I dont think you can do anything better. Since RenderTargetBitmap uses MILcore which you can't access. And I don't think there is any other way to copy Visual. However I think there is one option more. It won't be one-line but I think it will be good enough.

Basically you will render visual block by block(PGBRA32) and convert it into BlackWhite on the fly, and then concat it with Blackwhite buffer. I've started a little example code but in halfway decided that it's not gonna be so easy, but you can finish it.

 /// <summary>
/// Renders only part of the visual and returns byte[] array
/// which holds only black/white information.
/// </summary>
/// <param name="oVisual"></param>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="width"></param>
/// <param name="height"></param>
/// <returns>black/white pixel information. one pixel=one bit. 1=white, 0=black</returns>
public static byte[] RenderPartially(Visual oVisual, 
    int x, int y, double width, double height)
{
    int nWidth = (int)Math.Ceiling(width);
    int nHeight = (int)Math.Ceiling(height);

    RenderTargetBitmap oTargetBitmap = new RenderTargetBitmap(
        nWidth,
        nHeight,
        96,
        96,
        PixelFormats.Pbgra32
    );

    DrawingVisual oDrawingVisual = new DrawingVisual();
    using (DrawingContext oDrawingContext = oDrawingVisual.RenderOpen())
    {
        VisualBrush oVisualBrush = new VisualBrush(oVisual);

        oDrawingContext.DrawRectangle(
            oVisualBrush,
            null,
            new Rect(
                new Point(x, y),
                new Size(nWidth, nHeight)
            )
        );

        oDrawingContext.Close();
        oTargetBitmap.Render(oDrawingVisual);
    }

    //Pbgra32 == 32 bits ber pixel?!(4bytes)
    // Calculate stride of source and copy it over into new array.
    int bytesPerPixel = oTargetBitmap.Format.BitsPerPixel / 8;
    int stride = oTargetBitmap.PixelWidth * bytesPerPixel;
    byte[] data = new byte[stride * oTargetBitmap.PixelHeight];
    oTargetBitmap.CopyPixels(data, stride, 0);

    //assume pixels in byte[] are stored as PBGRA32 which means that 4 bytes form single PIXEL.
    //so the layout is like:
    // R1, G1, B1, A1,  R2, G2, B2, A2,  R3, G3, B3, A3, and so on.

    byte [] bitBufferBlackWhite = new byte[oTargetBitmap.PixelWidth
        * oTargetBitmap.PixelHeight / bytesPerPixel];

    for(int row = 0; row < oTargetBitmap.PixelHeight; row++)
    {
        for(int col = 0; col < oTargetBitmap.PixelWidth; col++)
        {
            //calculate concrete pixel from PBGRA32
            int index = stride * row + bytesPerPixel * col;
            int r = data[index];
            int g = data[index + 1];
            int b = data[index + 2];
            int transparency = data[index + 3];

            //determine whenever pixel is black or white.
            //note that I dont know the exact process how one converts
            //from PBGRA32 to BlackWhite format. But you should get the idea.
            bool isBlack = r + g + b + transparency == 0;

            //calculate target index and USE bitwise operators in order to
            //set right bits.
        }
    }

    return bitBufferBlackWhite;
}

So essentially, set up new WriteableBitmap with BlackWhite format, and then call this function like this:

WriteableBitmap blackWhiteFullBuffer = new WriteableBItmap(....., PIxelFormats.BlackWhite);
for(int x = 0; x < Visual.Width; x += <some uniform amount that divides correctly>)
{
    for(int y = 0; y < VIsual.Height; y+= <some uniform amount that divides correctly>)
    {
        byte[] partialBuffer = PartialRenderer.RenderPartially(Visual, x, y, <uniform amX>,
          <uniform amY>);
        //concat that partial blackWhite buffer with other blackWhite buffer.
        //you do this as long as needed and memory wont grow too much
        //hopefully it will be fast enough too.
        PartialRenderer.ConcateBuffers(blackWhiteFullBuffer, partialBuffer);
    }
}

//then save blackWhiteFullBuffer to HDD if needed.

0
Colin Smith On

A number of ideas, some are a bit convoluted...

Print to XPS then extract Bitmap

You could print the Visual to an XPS Document.

If you're lucky then it will combine the 1bit images that you drew in in the DrawingContext and produce a single bitmap in the XPS file.

For the Rectangles it might keep the Rectangles as vector based information in the XPS (the "Shape" based Rectangle or DrawingContext DrawRectangle might both do this)....if that happens then create a bitmap into which your draw the rectangle part and draw that bitmap into the context.

Once you have the XPS files, you could then parse it and extract the "image" if you are lucky that's what it produced inside. (XPS is just a ZIP file that uses XAML to describe content and uses subdirectories to store bitmap image data files, fonts, etc).

(I think you can use the Package class to access the raw parts of an XPS document, or XpsDocument for a higher level interpretation).

Or just display the XPS in an XPS viewer if your intention is just to provide a way to allow your combined 1bit images to be viewed in a different context.

Use a Printer Driver that provides Print to Image functionality

This isn't ideal...not least because you have to install a Printer Driver on the machine.....but you might be able to use a "Print to Image" printer driver and then use that to produce your bitmap. Here are some suggestions:

Create a GDI+ HBITMAP and do the drawing using GDI+ calls...then wrap it for use by WPF...or save out to disk.

[1] First create a GDI+ Bitmap large enough to hold your composed rendering

(there are a few different ways to do this...one way is to use a WriteableBitmap to provide the backbuffer bits/store...that's if you wanted access to the bits in memory...in your case I don't think you do/need to).

var bmp = new System.Drawing.Bitmap( pixelwidth, pixelheight, System.Drawing.Imaging.Format1bppIndexed );

Or this way if you want WriteableBitmap access.

[2] Convert any WPF BitmapSources to GDI+ Bitmaps so you can draw them onto the GDI+ Graphics context using DrawImage.

You can use CopyPixels to do that on your BitmapSources.

For your rectangles you can just use the GDI+ DrawRectangle rendering command.

[3] Save the GDI+ Bitmap to disk

Use the .Save method on System.Image.Bitmap to save it as a Bitmap.

[4] Use the saved image as the Source to an Image element

(note this will probably use masses of memory when it loads your image even though it's 1bpp...because the WPF rendering path is all 32bpp).

[4] OR wrap the GDI+ Bitmap for use in WPF

You can use InteropBitmap to wrap the GDI+ based bitmap so you can use it as a BitmapSource in WPF. (note it may have to be a 32bpp one....if it has to...then you are back at square 1...but try anyway).

Create a Bitmap "Service"

Create another process which acts as a service (doesn't have to be an NT service...could just be a child process you start) to render your combined 1bpp images....there are various ways to communicate with it to give it the rendering commands.

When memory consumption gets too high/the LOH gets fragmented, then you could restart this service.

Other Ideas

You could render using OpenGL (e.g. use OpenTK or SharpGL), or render using DirectX...via the 3D path with D3DImage or Direct2D (whether this behaves the same as RenderTargetBitmap in terms of memory usage...that's to find out).

Try out NET 4.5 as there have been a number of improvements in the LOH Heap: