将Drawing.Bitmap转换为Imaging.Metafile - Pixel Perfect

时间:2010-10-02 15:48:56

标签: c# winforms gdi+ richtextbox

我正在尝试将Drawing.Bitmap转换为Imaging.Metafile,以便将图元文件插入Forms.RichTextBox。 (作为参考,在元文件中嵌入位图是将位图放入richtext的推荐做法(参见Rich Text Format(RTF)Specification Version 1.9.1,p.149。)不幸的是它似乎也是嵌入的唯一方法将图像转换为Forms.RichTextBox,因为我无法获得任何依赖于设备或与设备无关的方法将位图插入到RichTextBox中以便工作。)

稍后,我必须从图元文件中检索像素数据。我要求元文件的像素与位图的像素完全匹配。但是,当我执行转换时,像素会略有改变。 (也许是由于GDI图像色彩管理(ICM)?)

这是我的技巧:

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;
}

在这种情况下,我以这种方式访问​​元文件的像素:

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

我也试过使用BitBlt技术但没有成功。

BitBlt技术:

[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;
}

我甚至不确定这种技术是否正确复制了像素数据。当我稍后尝试访问元文件中的数据时,此技术失败:

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);

如何在不改变像素的情况下将位图转换为图元文件,然后再次检索像素数据?谢谢!


设置位图分辨率

这就是我尝试设置位图分辨率以便元文件的分辨率匹配的方法:

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;

rtfBox与发送到BitmapToMetafileViaGraphicsDrawImage的rtfBox相同。

4 个答案:

答案 0 :(得分:5)

您可以手动创建和处理元文件(例如枚举其记录),在这种情况下,您实际上可以将包含其确切数据的位图插入元文件流中。然而,这并不像听起来那么简单。

在播放GDI图元文件时,所有操作都在内部转换为GDI +,这实际上是一个完全不同的API,它以不同的方式处理很多事情。不幸的是,一旦元文件有一些复杂性或输出需要转换,内置的方法就是让GDI在低分辨率位图上渲染图元文件然后绘制它,这从来没有给你结果你正在寻找。

对于一个项目(闭源 - 所以不要求来源;))我必须实现一个类似于EMFExplorer功能的完整GDI到GDI +回放引擎,但是使用托管仅限代码并使用单个字符重新对齐以获得精确的文本输出。这只是说,如果你愿意投入一些时间来处理你自己需要的所有元文件记录(这应该只是一个小子集,但仍然如此),那就可以说它可以完成。


编辑以解决评论中提出的问题:

元文件只是一系列绘图指令,基本上是执行GDI(+)操作的记录。所以你应该能够构建一个元文件作为二进制数据,基本上只包含标题和位图,在这种情况下(因为你没有通过drwing操作但总是处理二进制级别的位图)你将能够从您写入的元文件中检索完全相同的数据。

幸运的是,几年以来,Microsoft一直在向公众提供文档格式和协议的文档编制和协议,因此可以获得WMF / EMF文件格式的精确文档。元文件格式由MS解释: http://msdn.microsoft.com/en-us/library/cc230514(PROT.10).aspx

这里概述了它的结构: http://msdn.microsoft.com/en-us/library/cc230516(v=PROT.10).aspx

这里描述了位图记录: http://msdn.microsoft.com/en-us/library/cc231160(v=PROT.10).aspx

使用此信息,您应该能够将二进制数据(可能使用BinaryWriter)放在一起,然后加载/枚举文件以获取数据(例如,在元文件中再次查找位图并提取来自它的所需数据。)

答案 1 :(得分:2)

这里有许多可能的答案,两个最有可能的罪魁祸首是别名和分辨率。我的猜测是分辨率,因为当你在没有指定分辨率的情况下保存位图时,我相信它会自动设置为120 DPI,这可能会影响锯齿。

  

在有人评论决议无关紧要并且DPI仅用于打印和等等等等之前,请拒绝发表评论的冲动。我很高兴能就你自己的问题与你进行辩论。

使用图元文件的分辨率设置要转换回的位图的分辨率。

答案 2 :(得分:0)

这只是黑暗中的一个镜头,但是看看一些ImageFlags标志值,看看它们是否可以合并到元文件的生成或渲染中。

答案 3 :(得分:0)

这是解决它的一种有趣方式,但如果您只需要将它放在RichTextBox中 - 您可以使用剪贴板:

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

        richTextBox1.Paste();
    }

但我不确定相反的方式(从RTF文本中读取位图)