Click here to Skip to main content
15,885,914 members
Articles / Programming Languages / C#

Working with Metafile Images in .NET

Rate me:
Please Sign up or sign in to vote.
4.91/5 (14 votes)
5 Apr 2011CPOL8 min read 78.5K   24   13
How to work with metafile images in .NET

What is a Metafile Image?

The Windows Metafile (WMF) is a graphics file format on Microsoft Windows systems, originally designed in the 1990s.

Internally, a metafile is an array of variable-length structures called metafile records. The first records in the metafile specify general information such as the resolution of the device on which the picture was created, the dimensions of the picture, and so on. The remaining records, which constitute the bulk of any metafile, correspond to the graphics device interface (GDI) functions required to draw the picture. These records are stored in the metafile after a special metafile device context is created. This metafile device context is then used for all drawing operations required to create the picture. When the system processes a GDI function associated with a metafile DC, it converts the function into the appropriate data and stores this data in a record appended to the metafile.

After a picture is complete and the last record is stored in the metafile, you can pass the metafile to another application by:

  • Using the clipboard
  • Embedding it within another file
  • Storing it on disk
  • Playing it repeatedly

A metafile is played when its records are converted to device commands and processed by the appropriate device.

There are two types of metafiles:

I had worked with Metafiles in Visual Basic 6 many years ago, when I worked for Taltech.com, a company that strives to produce the highest quality barcode images that Windows can create. As I remember it, this involved making lots of Windows API calls, and something called "Hi Metric Map Mode" (MM_HIMETRC). "Basically, the mapping mode system enables you to equate an abstract, logical drawing surface with a concrete and constrained display surface. This is good in principle but GDI had a major drawback inasmuch as the logical drawing area coordinates were based upon signed integers. This meant that creating drawing systems based upon some real-world measurement system such as inches or millimeters required you to use a number of integer values to represent a single unit of measure for example, in the case of MM_LOMETRC mapping, there are ten integer values to each linear millimeter and in the case of MM_LOENGLISH, there are 100 integer values to each linear inch." - Bob Powell. Bob has written a great article: Comparing GDI mapping modes with GDI+ transforms for anyone wanting to learn more about this.

Bob goes on to say that "Given the fact that matrix transformations have been recognized as the only sensible method to manipulate graphics for many years, GDI mapping modes were a very limited alternative and always a bit of a kludge", and he's probably right. To be honest, all that matrix stuff went way over my head. Luckily, today, the simplicity of matrix transformations is built into GDI+, and most of those API calls have been integrated into the System.Drawing namespaces of the .NET Framework. Having already found a way to draw a barcode as a bitmap using the .NET Framework, I wanted to see how easy it would be to create a barcode as a metafile, since bitmaps are a lossy format, and barcodes need to be as high quality as possible to ensure that the scanners read them correctly.

You might think that creating a metafile would be as easy as using the Save() method of System.Drawing.Image and giving the file a .wmf or .emf extension, but sadly this is not the case. If you do that, what you actually get, is a Portable Network Graphics (PNG) file, with a .wmf or .emf extension. Even if you use the ImageFormat overload, and pass in the filename and ImageFormat.Emf or ImageFormat.Wmf, you still end up with a PNG. It doesn't matter whether you create a Bitmap and call Save() or you go to the trouble of creating an in memory Metafile (more on that later) and then call Save(), you will never get a true Metafile. If you visit the MSDN documentation on the Metafile Class, you can see under 'Remarks' it casually states:

When you use the Save method to save a graphic image as a Windows Metafile Format (WMF) or Enhanced Metafile Format (EMF) file, the resulting file is saved as a Portable Network Graphics (PNG) file instead. This behavior occurs because the GDI+ component of the .NET Framework does not have an encoder that you can use to save files as .wmf or .emf files.

This is confirmed in the documentation for the System.Drawing.Image.Save Method:

If no encoder exists for the file format of the image, the Portable Network Graphics (PNG) encoder is used. When you use the Save() method to save a graphic image as a Windows Metafile Format (WMF) or Enhanced Metafile Format (EMF) file, the resulting file is saved as a Portable Network Graphics (PNG) file. This behavior occurs because the GDI+ component of the .NET Framework does not have an encoder that you can use to save files as .wmf or .emf files.

Saving the image to the same file it was constructed from is not allowed and throws an exception.

In order to save your in memory metafile as a true metafile, you must make some old fashioned API calls, and I will show you how to do this in due course, but first you need to know how to create an in memory Metafile. Let's assume that, like me, you already have some code that generates a bitmap image which looks just the way you want it. Here is some sample code distilled from a nice BarCode Library project written by Brad Barnhill.

C#
static void Main(string[] args)
{
  int width = 300;
  int height = 100;

  Bitmap b = new Bitmap(width, height);
  int pos = 0;
  string encodedValue =
    "1001011011010101001101101011011001010101101001011010101001101101
    0101001101101010100110110101101100101010110100110101010110011010101011
    00101011011010010101101011001101010100101101101";
  int barWidth = width / encodedValue.Length;
  int shiftAdjustment = (width % encodedValue.Length) / 2;
  int barWidthModifier = 1;

  using (Graphics g = Graphics.FromImage(b))
  {
    // clears the image and colors the entire background
    g.Clear(Color.White);

    // lines are barWidth wide so draw the appropriate color line vertically
    using (Pen pen = new Pen(Color.Black, (float)barWidth / barWidthModifier))
    {
      while (pos < encodedValue.Length)
      {
        if (encodedValue[pos] == '1')
        {
          g.DrawLine(
            pen,
            new Point(pos * barWidth + shiftAdjustment + 1, 0),
            new Point(pos * barWidth + shiftAdjustment + 1, height));
        }

        pos++;
      } // while
    } // using
  } // using

  b.Save(@"d:\temp\test.png", ImageFormat.Png);
}

As you can see, this code creates a new Bitmap image, creates a Graphics object from it, draws on it using the Pen class then saves it as a .png. The resulting image looks like this:

So far so good. As we have already established, simply rewriting the last line as:

b.Save(@"d:\temp\test.emf", ImageFormat.Emf);

is not enough to convert this image to a metafile. Sadly, substituting the word "Metafile" for "Bitmap" is not all it takes to create an in memory metafile. Instead, you will need to have a device context handle and a stream handy. If you are working on a Windows Forms application, you can create a Graphics object easily by simply typing Graphics g = this.CreateGraphics(); but if you are writing a class library or a console application, you have to be a bit more creative and use an internal method (FromHwndInternal) to create the Graphics object out of nothing:

C#
Graphics offScreenBufferGraphics;
Metafile m;
using (MemoryStream stream = new MemoryStream())
{
  using (offScreenBufferGraphics = Graphics.FromHwndInternal(IntPtr.Zero))
  {
    IntPtr deviceContextHandle = offScreenBufferGraphics.GetHdc();
    m = new Metafile(
      stream,
      deviceContextHandle,
      EmfType.EmfPlusOnly);
    offScreenBufferGraphics.ReleaseHdc();
  }
}

OK, so now your code looks like this:

C#
static void Main(string[] args)
{
  int width = 300;
  int height = 100;

  Graphics offScreenBufferGraphics;
  Metafile m;
  using (MemoryStream stream = new MemoryStream())
  {
    using (offScreenBufferGraphics = Graphics.FromHwndInternal(IntPtr.Zero))
    {
      IntPtr deviceContextHandle = offScreenBufferGraphics.GetHdc();
      m = new Metafile(
        stream,
        deviceContextHandle,
        EmfType.EmfPlusOnly);
      offScreenBufferGraphics.ReleaseHdc();
    }
  }

  int pos = 0;
  string encodedValue =
    "1001011011010101001101101011011001010101101001011010101001101101
    0101001101101010100110110101101100101010110100110101010110011010101011
    00101011011010010101101011001101010100101101101";
  int barWidth = width / encodedValue.Length;
  int shiftAdjustment = (width % encodedValue.Length) / 2;
  int barWidthModifier = 1;

  using (Graphics g = Graphics.FromImage(m))
  {
    // clears the image and colors the entire background
    g.Clear(Color.White);

    // lines are barWidth wide so draw the appropriate color line vertically
    using (Pen pen = new Pen(Color.Black, (float)barWidth / barWidthModifier))
    {
      while (pos < encodedValue.Length)
      {
        if (encodedValue[pos] == '1')
        {
          g.DrawLine(
            pen,
            new Point(pos * barWidth + shiftAdjustment + 1, 0),
            new Point(pos * barWidth + shiftAdjustment + 1, height));
        }

        pos++;
      } // while
    } // using
  } // using

  m.Save(@"d:\temp\test2.png", ImageFormat.Png);
 }

But wait, what happened to my barcode? It's all off center, yet the code used to draw it hasn't changed:

Luckily, this is easy to fix. We need to use a different overload when creating the metafile, so that we can specify a width and height, and a unit of measure:

C#
Graphics offScreenBufferGraphics;
Metafile m;
using (MemoryStream stream = new MemoryStream())
{
  using (offScreenBufferGraphics = Graphics.FromHwndInternal(IntPtr.Zero))
  {
    IntPtr deviceContextHandle = offScreenBufferGraphics.GetHdc();
    m = new Metafile(
      stream,
      deviceContextHandle,
      new RectangleF(0, 0, width, height),
      MetafileFrameUnit.Pixel,
      EmfType.EmfPlusOnly);
    offScreenBufferGraphics.ReleaseHdc();
  }
}

Now it looks the same when saved as a .png, but it may still look all wrong (and more importantly, be completely unreadable by a barcode scanner) if printed and the resolution of the printer does not match that of your desktop when you created the metafile. Furthermore, if I save this as a real EMF file and email it to you, when you view it, you may see a different rendering, because the desktop I created it on has a resolution of 1920x1080, but if your desktop has a higher or lower resolution, it will affect how it is displayed. Remember a metafile is a stored set of instructions on how to render the image and by default, it will use the stored resolution for reference. To correct this, we have to add some additional code to the Graphics object to ensure this doesn't happen (thanks go to Nicholas Piasecki and his blog entry for pointing this out):

C#
MetafileHeader metafileHeader = m.GetMetafileHeader();
g.ScaleTransform(metafileHeader.DpiX / g.DpiX, metafileHeader.DpiY / g.DpiY);
g.PageUnit = GraphicsUnit.Pixel;
g.SetClip(new RectangleF(0, 0, width, height));

So How Can We Save It As A Real Metafile Anyway?

Well, first we need to declare some old fashioned Win API calls:

C#
[DllImport("gdi32.dll")]
static extern IntPtr CopyEnhMetaFile(  // Copy EMF to file
  IntPtr hemfSrc,   // Handle to EMF
  String lpszFile // File
);

[DllImport("gdi32.dll")]
static extern int DeleteEnhMetaFile(  // Delete EMF
  IntPtr hemf // Handle to EMF
);

Then we can replace the m.Save(...); line with this:

C#
// Get a handle to the metafile
IntPtr iptrMetafileHandle = m.GetHenhmetafile();

// Export metafile to an image file
CopyEnhMetaFile(iptrMetafileHandle, @"d:\temp\test2.emf");

// Delete the metafile from memory
DeleteEnhMetaFile(iptrMetafileHandle);

and finally we have a true metafile to share. Why Microsoft failed to encapsulate this functionality within the framework as an image encoder is a mystery. Windows Metafiles, and Enhanced Metafiles are after all their own creation. So our final version of the code looks like this:

C#
static void Main(string[] args)
{
  int width = 300;
  int height = 100;

  Graphics offScreenBufferGraphics;
  Metafile m;
  using (MemoryStream stream = new MemoryStream())
  {
  using (offScreenBufferGraphics = Graphics.FromHwndInternal(IntPtr.Zero))
  {
    IntPtr deviceContextHandle = offScreenBufferGraphics.GetHdc();
    m = new Metafile(
    stream,
    deviceContextHandle,
    new RectangleF(0, 0, width, height),
    MetafileFrameUnit.Pixel,
    EmfType.EmfPlusOnly);
    offScreenBufferGraphics.ReleaseHdc();
  }
  }

  int pos = 0;
  string encodedValue =
  "10010110110101010011011010110110010101011010010110101010011011
  01010100110110101010011011010110110010101011010011010101011001101010
  101100101011011010010101101011001101010100101101101";
  int barWidth = width / encodedValue.Length;
  int shiftAdjustment = (width % encodedValue.Length) / 2;
  int barWidthModifier = 1;

  using (Graphics g = Graphics.FromImage(m))
  {
  // Set everything to high quality
  g.SmoothingMode = SmoothingMode.HighQuality;
  g.InterpolationMode = InterpolationMode.HighQualityBicubic;
  g.PixelOffsetMode = PixelOffsetMode.HighQuality;
  g.CompositingQuality = CompositingQuality.HighQuality;

  MetafileHeader metafileHeader = m.GetMetafileHeader();
  g.ScaleTransform(
    metafileHeader.DpiX / g.DpiX,
    metafileHeader.DpiY / g.DpiY);

  g.PageUnit = GraphicsUnit.Pixel;
  g.SetClip(new RectangleF(0, 0, width, height));

  // clears the image and colors the entire background
  g.Clear(Color.White);

  // lines are barWidth wide so draw the appropriate color line vertically
  using (Pen pen = new Pen(Color.Black, (float)barWidth / barWidthModifier))
  {
    while (pos < encodedValue.Length)
    {
    if (encodedValue[pos] == '1')
    {
      g.DrawLine(
      pen,
      new Point(pos * barWidth + shiftAdjustment + 1, 0),
      new Point(pos * barWidth + shiftAdjustment + 1, height));
    }

    pos++;
    } // while
  } // using
  } // using

  // Get a handle to the metafile
  IntPtr iptrMetafileHandle = m.GetHenhmetafile();

  // Export metafile to an image file
  CopyEnhMetaFile(iptrMetafileHandle, @"d:\temp\test2.emf");

  // Delete the metafile from memory
  DeleteEnhMetaFile(iptrMetafileHandle);
}

There is one more Metafile Gotcha I'd like to share. As part of my original Bitmap generating code, I had a boolean option to generate a label, that is the human readable text that appears beneath the barcode. If this option was selected, before returning the bitmap object, I would pass it to another method that looked something like this:

C#
static Image DrawLabel(Image img, int width, int height)
{
    Font font = new Font("Microsoft Sans Serif", 10, FontStyle.Bold); ;

    using (Graphics g = Graphics.FromImage(img))
    {
  g.DrawImage(img, 0, 0);
  g.SmoothingMode = SmoothingMode.HighQuality;
  g.InterpolationMode = InterpolationMode.HighQualityBicubic;
  g.PixelOffsetMode = PixelOffsetMode.HighQuality;
  g.CompositingQuality = CompositingQuality.HighQuality;

  StringFormat f = new StringFormat();
  f.Alignment = StringAlignment.Center;
  f.LineAlignment = StringAlignment.Near;
  int LabelX = width / 2;
  int LabelY = height - font.Height;

  //color a background color box at the bottom
  //of the barcode to hold the string of data
  g.FillRectangle(new SolidBrush(Color.White),
  new RectangleF((float)0, (float)LabelY, (float)width,
  (float)font.Height));

  //draw datastring under the barcode image
  g.DrawString("038000356216", font, new SolidBrush(Color.Black),
  new RectangleF((float)0, (float)LabelY, (float)width, (float)font.Height), f);

  g.Save();
    }

    return img;
}

When passing the bitmap, this works great, but when passing the metafile, the line using (Graphics g = Graphics.FromImage(img)) would throw a System.OutOfMemoryException every time. As a workaround, I copied the label generating code into the main method that creates the barcode. Another option might be to create a new metafile (not by calling m.Clone() - I tried that and still got the out of memory exception), send that to the DrawLabel() method, then when it comes back, create a third Metafile, and call g.DrawImage() twice (once for each metafile that isn't still blank) and return this new composited image. I think that will work, but I also think it would use a lot more resources and be grossly inefficient, so I think copying the label code into both the DrawBitmap() and DrawMetafile() methods, is a better solution.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Salem Web Network
United States United States
Robert Williams has been programming web sites since 1996 and employed as .NET developer since its release in 2002.

Comments and Discussions

 
QuestionExcellent Pin
Sing Abend31-Oct-14 6:13
professionalSing Abend31-Oct-14 6:13 
QuestionException: Parameter is not valid. Pin
nho_Osaka3-Jun-13 16:14
nho_Osaka3-Jun-13 16:14 
GeneralMy vote of 5 Pin
QuantumHive4-Feb-13 7:23
QuantumHive4-Feb-13 7:23 
GeneralMy vote of 5 Pin
Eric Ouellet8-Nov-12 10:27
professionalEric Ouellet8-Nov-12 10:27 
GeneralMy vote of 5 Pin
sbarnes20-Sep-12 9:49
sbarnes20-Sep-12 9:49 
SuggestionCopy metafile to clipboard Pin
InfiniteMort11-Sep-12 22:44
InfiniteMort11-Sep-12 22:44 
GeneralRe: Copy metafile to clipboard Pin
vishalrk1026-Apr-18 2:59
vishalrk1026-Apr-18 2:59 
GeneralRe: Copy metafile to clipboard Pin
InfiniteMort28-Jul-18 0:57
InfiniteMort28-Jul-18 0:57 
GeneralMy vote of 5 Pin
pistol35030-May-12 23:37
pistol35030-May-12 23:37 
GeneralMy vote of 5 Pin
neyerMat28-Mar-12 5:56
neyerMat28-Mar-12 5:56 
QuestionCannot delete the metafile created while working from asp.net webpage Pin
Member 840459615-Nov-11 9:26
Member 840459615-Nov-11 9:26 
AnswerRe: Cannot delete the metafile created while working from asp.net webpage Pin
ipanuju19-Nov-12 2:40
ipanuju19-Nov-12 2:40 
GeneralMy vote of 5 Pin
Member 840459615-Nov-11 9:04
Member 840459615-Nov-11 9:04 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.