C#

Metafileの解像度

Metafileの作成で難しいところがあったのでメモしておくことにした。Metafileを作ると、画像としてのサイズは指定した通りになるのだが、中の描画の縮尺がおかしい。さっぱり理由がわからなかった。

[DllImport("Gdi32.dll")]
static extern IntPtr CreateCompatibleDC(IntPtr hdc);

[DllImport("Gdi32.dll")]
static extern int DeleteDC(IntPtr hdc);

// *1
IntPtr hDC = CreateCompatibleDC(IntPtr.Zero);

try
{
    Rectangle FrameRect = new Rectangle(0, 0, 216, 216);

    using (Metafile _Metafile = new Metafile("sample.emf", hDC, FrameRect, MetafileFrameUnit.Point, EmfType.EmfPlusDual))
    {
        using (Graphics g = Graphics.FromImage(_Metafile))
        {
            g.PageUnit = GraphicsUnit.Point;

            // *2

            g.FillRectangle(Brushes.White, FrameRect); // 背景を白にする。
            g.FillRectangle(Brushes.Green, 72, 72, 72, 72); // 真ん中に緑の1インチの正方形を描画。
        }
    }
}
finally
{
    DeleteDC(hDC);
}
metafile-fig-01

figure 1: 真ん中に緑の正方形を描画したはずなのに、右下に寄っている。サイズは72 * Width / HorizontalResolutionなどを計算すると、合っている。

Metafileのプロパティを見ると、画像としてのサイズは合っているのだが、解像度がおかしいことに気付いた。よくわからないままにScaleTransform()で縮小したら、うまくいった。

g.ScaleTransform(0.846f, 0.846f); // *2 のところに挿入

例えばGraphicsの解像度が96.0dpi、Metafileの解像度が81.2dpiだとすると、Graphicsでは1.0インチが96.0ピクセルになり、それがMetafileでは96.0÷81.2≒1.18インチになってしまうのかなと思った。それなら81.2÷96.0≒0.846倍に縮小すれば、拡大されて描画されるときに元の長さに戻ってうまくいくだろう。

metafile-fig-02

figure 2: ScaleTransform()で縮小したら、うまくいったようだ。

ただ、元にするデバイスコンテキストが変わるとMetafileの解像度も変わるし、描画する前にMetafileの解像度を知ることもできない。メモリデバイスコンテキストはディスプレイデバイスと互換性があるので、実行環境によっても解像度が変わるはず。困ったものだ。それにしても、いったいどうしてMetafileの解像度はこんなに変な値なのだろう。いろいろ試したりしてるうちに、デバイスコンテキストの情報を調べて、ようやくわかった。MetafileではHORZSIZE, VERTSIZE, HORZRES, VERTRESから解像度が計算されるらしい。

// *1 のところに挿入
[DllImport("Gdi32.dll")]
static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
// *2 のところに挿入
{
    const int HORZSIZE = 4;
    const int VERTSIZE = 6;
    const int HORZRES = 8;
    const int VERTRES = 10;

    int HorzSize_Mm = GetDeviceCaps(hDC, HORZSIZE); // ミリメートル(mm)単位の画面の物理的な幅。
    int VertSize_Mm = GetDeviceCaps(hDC, VERTSIZE); // ミリメートル(mm)単位の画面の物理的な高さ。

    float HorzSize_In = HorzSize_Mm / 25.4f; // インチ単位の画面の物理的な幅。
    float VertSize_In = VertSize_Mm / 25.4f; // インチ単位の画面の物理的な高さ。

    int HorzRes = GetDeviceCaps(hDC, HORZRES); // ピクセル単位の画面の幅。
    int VertRes = GetDeviceCaps(hDC, VERTRES); // ピクセル単位(ラスタ行数)の画面の高さ。

    float DpiX = HorzRes / HorzSize_In; // 水平解像度:これがMetafileの解像度。
    float DpiY = VertRes / VertSize_In; // 垂直解像度:これがMetafileの解像度。

    float ScaleX = DpiX / g.DpiX;
    float ScaleY = DpiY / g.DpiY;

    g.ScaleTransform(ScaleX, ScaleY);
}

Metafileにこんな正確な解像度なんて必要あるのかな。素直にLOGPIXELSXとLOGPIXELSYを設定してくれても良かったのにと思うが、何か歴史的な背景でもあるのだろうか。


(2018/10/28 初稿)