I have now learned more than I cared to have known about metafiles.
1. Some of the Metafile
class's constructor overloads work poorly and will operate on a truncated DPI value.
Consider the following:
protected Graphics GetNextPage(SizeF pageSize)
{
IntPtr deviceContextHandle;
Graphics offScreenBufferGraphics;
Graphics metafileGraphics;
MetafileHeader metafileHeader;
this.currentStream = new MemoryStream();
using (offScreenBufferGraphics = Graphics.FromHwnd(IntPtr.Zero))
{
deviceContextHandle = offScreenBufferGraphics.GetHdc();
this.currentMetafile = new Metafile(
this.currentStream,
deviceContextHandle,
new RectangleF(0, 0, pageSize.Width, pageSize.Height),
MetafileFrameUnit.Inch,
EmfType.EmfOnly);
metafileGraphics = Graphics.FromImage(this.currentMetafile);
offScreenBufferGraphics.ReleaseHdc();
}
return metafileGraphics;
}
If you passed in a SizeF
of { 8.5, 11 }, you might expect to get a Metafile
that has an rclFrame
of { 21590, 27940 }. Converting inches to millimeters is not hard, after all. But you probably won't. Depending on your resolution, GDI+, it seems, will use a truncated DPI value when converting the inches parameter. To get it right, I have to do it myself in hundredths of a millimeter, which GDI+ just passes through since that's how it's natively stored in the metafile header:
this.currentMetafile = new Metafile(
this.currentStream,
deviceContextHandle,
new RectangleF(0, 0, pageSize.Width * 2540, pageSize.Height * 2540),
MetafileFrameUnit.GdiCompatible,
EmfType.EmfOnly);
Rounding error #1 solved--the rclFrame
of my metafile is now correct.
2. The DPI on a Graphics
instance recording to a Metafile
is always wrong.
See that metafileGraphics
variable that I set by calling Graphics.FromImage()
on the metafile? Well, it seems that that Graphics
instance will always have a DPI of 96 dpi. (If I had to guess, it's always set to the logical DPI, not the physical one.)
You can imagine that hilarity that ensues when you are drawing on a Graphics
instance that is operating under 96 dpi and recording to a Metafile
instance that has 87.9231 dpi "recorded" in its header. (I say "recorded" because its calculated from the other values.) The metafile's "pixels" (remember, the GDI commands stored in the metafile are specified in pixels) are bigger, and so you curse and mutter why your call to draw something one inch long ends up being one-and-something-beyond inches long.
The solution is to scale down the Graphics
instance:
metafileGraphics = Graphics.FromImage(this.currentMetafile);
metafileHeader = this.currentMetafile.GetMetafileHeader();
metafileGraphics.ScaleTransform(
metafileHeader.DpiX / metafileGraphics.DpiX,
metafileHeader.DpiY / metafileGraphics.DpiY);
Ain't that a hoot? But it seems to work.
"Rounding" error #2 solved--when I say draw something at "1 inch" at 88 dpi, that pixel had better be $%$^! recorded as pixel #88.
3. szlMillimeters
can vary wildly; Remote Desktop causes a lot of fun.
So, we discovered (per Mark's answer) that, sometimes, Windows queries the EDID of your monitor and actually knows how big it is physically. GDI+ helpfully uses this (HORZSIZE
etc) when filling in the szlMillimeters
property.
Now imagine that you go home to debug this code of remote desktop. Let's say that your home computer happens to have a 16:9 widescreen monitor.
Obviously, Windows can't query the EDID of a remote display. So it uses the age-old default of 320 x 240 mm, which would be fine, except that it happens to be a 4:3 aspect ratio, and now the exact same code is generating a metafile on a display that supposedly has non-square physical pixels: the horizontal DPI and vertical DPI are different, and I can't remember the last time that I saw that happen.
My workaround for this for now is: "Well, don't run it under remote desktop."
4. The EMF-to-PDF tool that I was using had a rounding error when looking at the rclFrame
header.
This was the principal cause of my problem that triggered this question. My metafile was "correct" all along (well, correct after I fixed the first two issues), and all of this search for creating a "high-resolution" metafile was a red herring. It is true that some fidelity is lost when recording the metafile on a low-resolution display device; that's because the GDI commands specified in the metafile are specified in pixels. It doesn't matter that it's a vector format and can scale up or down, some information is lost during the actual recording when GDI+ decides which "pixel" to snap an operation to.
I contacted the vendor and they gave me a corrected version.
Rounding error #3 solved.
5. The 'Summary' pane in Windows Explorer just so happens to truncate values when displaying the calculated DPI.
It just so happens that this truncated value represented the same erroneous value that the EMF-to-PDF tool was using internally. Aside from this, this quirk does not contribute anything meaningful to the discussion.
Conclusions
Since my question was about futzing with DPI on device contexts, Mark's is a good answer.