views:

181

answers:

3

Hello,

I'm trying to convert a Drawing.Bitmap to an Imaging.Metafile for the purposes of inserting the metafile into a Forms.RichTextBox. (For reference, embedding a bitmap in a metafile is the recommended practice for putting a bitmap into richtext (see Rich Text Format (RTF) Specification Version 1.9.1, p. 149.) Unfortunately it also appears to be the only way to embed an image into a Forms.RichTextBox, as I can't get any device-dependent or device-independent methods of inserting an bitmap into a RichTextBox to work.)

Later, I must retrieve the pixel-data from the metafile. I require that the pixels of the metafile exactly match those of the bitmap. When I perform the conversion, however, the pixels are slightly altered. (Perhaps due to GDI Image Color Management (ICM)?)

Here is my technique:

public static Imaging.Metafile BitmapToMetafileViaGraphicsDrawImage(Forms.RichTextBox rtfBox, Drawing.Bitmap bitmap)
{
    Imaging.Metafile metafile;
    using (IO.MemoryStream stream = new IO.MemoryStream())
    using (Drawing.Graphics rtfBoxGraphics = rtfBox.CreateGraphics())
    {
        IntPtr pDeviceContext = rtfBoxGraphics.GetHdc();

        metafile = new Imaging.Metafile(stream, pDeviceContext);
        using (Drawing.Graphics imageGraphics = Drawing.Graphics.FromImage(metafile))
        {
            //imageGraphics.DrawImage(bitmap, new Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height));
            imageGraphics.DrawImageUnscaled(bitmap, new Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height));
        }
        rtfBoxGraphics.ReleaseHdc(pDeviceContext);
    }
    return metafile;
}

In this case I access the pixels of the metafile in this way:

metafile.Save(stream, Imaging.ImageFormat.Png);
Bitmap bitmap = new Bitmap(stream, false);
bitmap.GetPixel(x, y);

I have also tried to use a BitBlt technique with no success.

BitBlt technique:

[System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
static extern int BitBlt(
  IntPtr hdcDest,     // handle to destination DC (device context)
  int nXDest,         // x-coord of destination upper-left corner
  int nYDest,         // y-coord of destination upper-left corner
  int nWidth,         // width of destination rectangle
  int nHeight,        // height of destination rectangle
  IntPtr hdcSrc,      // handle to source DC
  int nXSrc,          // x-coordinate of source upper-left corner
  int nYSrc,          // y-coordinate of source upper-left corner
  System.Int32 dwRop  // raster operation code
  );

public static Imaging.Metafile BitmapToMetafileViaBitBlt(Forms.RichTextBox rtfBox, Drawing.Bitmap bitmap)
{
    const int SrcCopy = 0xcc0020;

    Graphics bitmapGraphics = Graphics.FromImage(bitmap);
    IntPtr pBitmapDeviceContext = bitmapGraphics.GetHdc();

    RectangleF rect = new RectangleF(new PointF(0, 0), new SizeF(bitmap.Width, bitmap.Height));
    Imaging.Metafile metafile = new Imaging.Metafile(pBitmapDeviceContext, rect);
    Graphics metafileGraphics = Graphics.FromImage(metafile);
    IntPtr metafileDeviceContext = metafileGraphics.GetHdc();

    BitBlt(pBitmapDeviceContext, 0, 0, bitmap.Width, bitmap.Height, 
        metafileDeviceContext, 0, 0, SrcCopy);

    return metafile;
}

I'm not even sure this technique is correctly copying the pixel-data. This technique fails when I try to access the data in the metafile later:

IntPtr h = metafile.GetHenhmetafile(); // ArgumentException "Parameter is not valid."
byte[] data;
uint size = GetEnhMetaFileBits(h, 0, out data);
data = new byte[size];
GetEnhMetaFileBits(h, size, out data);
stream = new IO.MemoryStream(data);

How do I convert a bitmap into a metafile without altering the pixels, and then retrieve the pixel-data again later? Thank you!


Setting Bitmap Resolution

This is how I try to set the bitmap resolution so that the metafile's resolution matches:

Drawing.Bitmap bitmap = new Drawing.Bitmap(width, height, 
Imaging.PixelFormat.Format32bppArgb); // Use 32-bit pixels so that each component (ARGB) matches up with a byte
// Try setting the resolution to see if that helps with conversion to/from metafiles
Drawing.Graphics rtfGraphics = rtfBox.CreateGraphics();
bitmap.SetResolution(rtfGraphics.DpiX, rtfGraphics.DpiY);

// Set the pixel data
...

// Return the bitmap
return bitmap;

The rtfBox is the same one sent to BitmapToMetafileViaGraphicsDrawImage.

A: 

This is just a shot in the dark, but have a look at some of the ImageFlags flag values and see if they can be incorporated into the generation or rendering of the metafile.

Mark Ransom
I looked at them and none of them seemed to help based upon their names. Thank you.
DGGenuine
+3  A: 

You can create and process the metafile manually (e.g. enumerate its records), in which case you can actually insert a bitmap with its exact data into the metafile stream. However, this is not as simple as it may sound.

When playing a GDI metafile, all operations are internally converted into GDI+, which is actually a completely different API and which handles a lot of things differently. Unfortunately, the built-in way to do it as soon as the metafile has some complexity or the output needs to be transformed is to let GDI render the metafile on a low-res bitmap and then draw this, which never gives you the results you're looking for.

For a project (closed-source - so don't bother asking for the source ;) ) I had to implement a complete GDI-to-GDI+ playback engine similar to the functionality of EMFExplorer, but using managed code only and with single character re-aligning for exact textual output. That is only to say that it can be done, if you're willing to invest some time dealing with all the metafile records you need youself (which should be a small subset only, but still).


Edit to address the questions asked in the comments:

The metafile is nothing but a series of drawing instructions, basically a recording of GDI(+) operations performed. So you should be able to construct a metafile as binary data consisting basically only of the header and the bitmap, and in that case (since you don't pass through drwing operations but always handle the bitmap at a binary level) you will be able to retrieve the exact same data from the metafile you wrote into it.

Fortunately, since a few years Microsoft has been documenting and making their file formats and protocols available to the public, so that a precise documentation of the WMF/EMF file format is available. The metafile format is explained by MS here: http://msdn.microsoft.com/en-us/library/cc230514(PROT.10).aspx

It structure is outlined here: http://msdn.microsoft.com/en-us/library/cc230516(v=PROT.10).aspx

The bitmap records are described here: http://msdn.microsoft.com/en-us/library/cc231160(v=PROT.10).aspx

Using this information, you should be able to put together binary data (maybe using a BinaryWriter) and then also load/enumerate the file to get the data back (e.g. finding the bitmap again in the metafile and extract the required data from it).

Lucero
Can you please elaborate upon "you can actually insert a bitmap with its exact data into the metafile stream"? Which methods or gdi32 calls would I use to perform this? Also what do you mean by "single character re-aligning for exact textual output"? Is this similar to what I am attempting with pixel-perfect conversion between Drawing.Bitmap and Imaging.Metafile? Thank you.
DGGenuine
"the built-in way to do it ... render the metafile on a low-res bitmap" I think this might explain the errors I am seeing, because the pixel transformations for the metafile are identical to those between two bitmaps if I do not use useIcm=false in the bitmap constructor. Can you think of any way around this? Note: for my purposes I don't really care how the Metafile renders; but when I access it's byte data in the Rich Text, I require that the data there be pixel perfect so that I can retrieve data that was encoded in the pixels of the bitmap. Thank you.
DGGenuine
I edited the answer regarding the questions at hand. Since it is off-topic, here's the answer to the "single character re-aligning" question. GDI+ renders text completely different than GDI, even when using the very same font and settings. The kerning is different, and the exact positioning of the characters. Now the metafiles I has to convert contained many text strings, and they were misaligned since the GDI+ rendering did not match the positioning of the text blocks. Therefore I implemented a char-by-char position computation, using the kerning of GDI+ but positioning them correctly to fit.
Lucero
For GDI/GDI+ test rendering differences, see the KB http://support.microsoft.com/kb/307208 and articles such as http://www.bobpowell.net/formattingtext.htm or also the info on that topic in the EMFExplorer article.
Lucero
This is ultimately the route I took. Thanks for the good references.
DGGenuine
You're welcome, thank you.
Lucero
A: 

it's a funny way to solve it, but if all you need is to put it inside RichTextBox - you can use the Clipboard:

    private void button1_Click(object sender, EventArgs e)
    {
        System.Drawing.Bitmap bmp = new Bitmap("g.bmp");
        Clipboard.SetData(DataFormats.Bitmap, bmp);

        richTextBox1.Paste();
    }

but I'm not sure about the opposite way (reading the bitmap from RTF text)

itsho
Thank you but I must not disturb the clipboard contents.
DGGenuine
Clipboard horrors!!...sigh!
Nayan