Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Specifying DPI of a GDI Device Context

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

  • an rclFrame of { 0, 0, 21590, 27940 } (dimensions of the metafile, in thousandths of a mm)
  • a szlDevice of { 1440, 900 } (dimensions of the reference device, in pixels)
  • a szlMillimeters of { 416, 260 } (dimensions of the reference device, in mm)

Okay, so the rclFrame tells me that the size of the EMF should be

  • 21590 / 2540 = 8.5 in wide
  • 27940 / 2540 = 11 in tall

Right on. Using this information, we can determine the physical DPI of my monitor, too, if my math is right:

  • (1440 * 25.4) / 416 = 87.9231 horizontal dpi
  • (900 * 25.4) / 260 = 87.9231 vertical dpi

The problem

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.

  • Is it possible to change the DPI of the reference device so that the information stored in the metafile used to calculate the DPI comes out to a nice integer?
  • Can I create my own device context and specify its DPI? Or do I really have to use a printer to do that?

Thanks for any insight.

like image 463
Nicholas Piasecki Avatar asked Oct 06 '09 20:10

Nicholas Piasecki


2 Answers

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.

like image 195
Nicholas Piasecki Avatar answered Nov 19 '22 11:11

Nicholas Piasecki


I'm curious as to how Windows knows the physical size of your monitor. You must have changed a configuration somewhere? Perhaps you can change it to more convenient values that divide out nicely.

As implied by the name, a "Device Context" must be connected to a system device. However this does not need to be a hardware driver, it could be a device emulator such as a PDF writer print driver. I've seen at least one that lets you set an arbitrary DPI.

like image 3
Mark Ransom Avatar answered Nov 19 '22 11:11

Mark Ransom