I have an application that generates metafiles (EMFs). It uses the reference device (aka the screen) to render these metafiles, so the DPI of the metafile changes depending on what machine the code is running on.
Let's say my code is intending to create a metafile that is 8.5 in x 11 in. Using my development workstation as a reference, I end up with an EMF that has
Okay, so the rclFrame tells me that the size of the EMF should be
Right on. Using this information, we can determine the physical DPI of my monitor, too, if my math is right:
Anything that plays back this metafile--an EMF-to-PDF conversion, the "Summary" page when right-click on the EMF in Windows Explorer, etc--seems to truncate the calculated DPI value, displaying 87 instead of 87.9231 (even 88 would be fine).
This results in a page that is physically sized as 8.48 in x 10.98 in (using 87 dpi) instead of 8.5 in x 11 in (using 88 dpi) when the metafile is played back.
Thanks for any insight.
I have now learned more than I cared to have known about metafiles.
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.
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.
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."
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.
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.
Since my question was about futzing with DPI on device contexts, Mark's is a good answer.