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.
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))
{
g.Clear(Color.White);
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++;
}
}
}
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:
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:
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))
{
g.Clear(Color.White);
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++;
}
}
}
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:
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):
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:
[DllImport("gdi32.dll")]
static extern IntPtr CopyEnhMetaFile(
IntPtr hemfSrc,
String lpszFile
);
[DllImport("gdi32.dll")]
static extern int DeleteEnhMetaFile(
IntPtr hemf
);
Then we can replace the m.Save(...);
line with this:
IntPtr iptrMetafileHandle = m.GetHenhmetafile();
CopyEnhMetaFile(iptrMetafileHandle, @"d:\temp\test2.emf");
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:
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))
{
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));
g.Clear(Color.White);
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++;
}
}
}
IntPtr iptrMetafileHandle = m.GetHenhmetafile();
CopyEnhMetaFile(iptrMetafileHandle, @"d:\temp\test2.emf");
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:
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;
g.FillRectangle(new SolidBrush(Color.White),
new RectangleF((float)0, (float)LabelY, (float)width,
(float)font.Height));
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.