拡張メタファイルの描画精度

Windows上でベクター画像を取り扱うプログラムを作成する場合、サイズを変更しても劣化のないメタファイル・拡張メタファイル形式は一定のメリットがある。
特にクリップボード経由の場合は便利なことが多い。
一方、メタファイルを用いる場合、ビットマップとは異なり解像度を意識してプログラムを行う必要がある。

ここからは拡張メタファイルに関しての考察になる。
拡張メタファイルは内部のサイズとして0.01mm単位で管理をしている。
ただし、MoveToなどの描画で与える座標値は0.01mm単位ではなく、拡張メタファイルを作成するときに与えるデバイスコンテキストの解像度単位であることに注意が必要だ。
以下のコードでは、デスクトップ画面のデバイスコンテキストで拡張メタファイルを作成した場合の解像度を計算する。

  HWND hWnd = ::GetDesktopWindow(); // デスクトップ画面のウィンドウハンドル
  HDC  hDc  = ::GetDC( hWnd );      // デスクトップ画面のデバイスコンテキスト

  int nMmH  = ::GetDeviceCaps( hDc, HORZSIZE ); // デスクトップ画面のサイズ(mm)
  int nResH = ::GetDeviceCaps( hDc, HORZRES  ); // 横ピクセル数(ピクセル)

  // 1ピクセルのサイズ(mm/ピクセル)
  double d = static_cast<double>(nMmH)/static_cast<double>(nResH); 

手元のパソコンで行った場合、nMmH=527(mm)、nResH=1920(ピクセル)で、d≒0.27447(mm/ピクセル)となった。
メタファイル中で指定する場合もピクセル単位で描画命令を発行するので、この場合はおおよそ0.27447(mm/ピクセル)で位置が指定され、0.01mm単位で位置を指定できない。

では、論理座標設定を行って0.01mm単位で描画コマンドを発行するようにするとどうなるか。
100mm×100mmのサイズで、精度0.01mmの拡張メタファイルを作りたいとする。

  int nWidth  = 10000; // 100mm
  int nHeight = 10000; // 100mm

  HWND hWnd = ::GetDesktopWindow(); // デスクトップ画面のウィンドウハンドル
  HDC  hDc  = ::GetDC( hWnd );      // デスクトップ画面のデバイスコンテキスト
  RECT R; 
  R.left   = 0;
  R.top    = 0; 
  R.right  = nWidth; 
  R.bottom = nHeight;
  HDC  hEmfDC = CreateEnhMetaFile( hDC, NULL, &R, NULL );
  
  double dPmmH = ::GetDeviceCaps( hEmfDc, HORZSIZE ) * 100.0; // 0.01mm単位
  double dResH = ::GetDeviceCaps( hEmfDc, HORZRES  );
  double dPmmV = ::GetDeviceCaps( hEmfDc, VERTSIZE ) * 100.0;
  double dResV = ::GetDeviceCaps( hEmfDc, VERTRES  );

  int nDevX = FWidth  * dResH / dPmmH;
  int nDevY = FHeight * dResV / dPmmV;

  ::SetMapMode(       hEmfDc, MM_ANISOTROPIC );
  ::SetWindowExtEx(   hEmf  , FWidth, FHeight, nullptr );
  ::SetViewportExtEx( hEmfDc, nDevX , nDevY  , nullptr );

確かに描画コマンドで0.01mm単位で指定できる。しかし内部でデバイス座標に変換するときにデバイス座標は整数で計算されるため、結局位置は上記の例だと0.27447単位に丸め込まれ、結果として位置がずれる。
例えば MoveTo(100,100)とすると、内部でのデバイス座標への変換で、100/27.447≒3.64となり、整数化で切り捨てが発生して実際のデバイス座標コマンドではMoveTo(3,3)が与えられる。元の0.01mmベースに戻すと、3*27.447≒82.341となり、意図した位置に描画されないことが分かるだろう。
つまり、この方法ではうまくいかない。

拡張メタファイルの生成時に与えるデバイスコンテキストが利用したい解像度になるよう調整できれば、簡単に解決できるが、Windowsの場合、アプリケーションプログラムで任意の解像度のデバイスコンテキストを作成できないはずなので、この方法は使えない。

解決するには、デバイス単位で描画し、その後求めるサイズにリサイズする。
VCLでの例を挙げる。以降では拡張メタファイルの廃棄のコードは記述していないことに注意。

  int nPmmWidth  = 10000; // 100mm
  int nPmmHeight = 10000; // 100mm

  HWND hWnd= ::GetDesktopWindow(); 
  HDC  hDc = ::GetDC( hWnd );

  TMetafile* mf = new TMetafile;
  mf->SetSize( nPmmWidth, nPmmHeight ); // ピクセルサイズ

  TMetafileCanvas* pMfCanvas = new TMetafileCanvas( mf, hDc );

  // 描画コマンド

  delete pCanvas;

  TMetafile* mf2 = new TMetafile;
  mf2->MMWidth  = nPmmWidth;  // 正しいサイズをセット
  mf2->MMHeight = nPmmHeight;

  double dPmmH = ::GetDeviceCaps( hEmfDc, HORZSIZE ) * 100.0;
  double dResH = ::GetDeviceCaps( hEmfDc, HORZRES  );
  double dPmmV = ::GetDeviceCaps( hEmfDc, VERTSIZE ) * 100.0;
  double dResV = ::GetDeviceCaps( hEmfDc, VERTRES  );

  int nDevX = nPmmWidth  * dResH / dPmmH;
  int nDevY = nPmmHeight * dResV / dPmmV;

  TMetafileCanvas* pMfCanvas2 = new TMetafileCanvas( mf2, hDc );
  pMfCanvas2->StretchDraw( TRect( 0, 0, nDevX, nDevY ), mf );
  delete pMfCanvas2;

Win32apiベースだと次のようなコードになる。
なお、以下のコードは動作検証をしていないので、間違いがあれば指摘して欲しい。

  int nPmmWidth  = 10000; // 100mm
  int nPmmHeight = 10000; // 100mm

  HWND hWnd= ::GetDesktopWindow(); 
  HDC  hDc = ::GetDC( hWnd );

  double dPmmH = ::GetDeviceCaps( hDc, HORZSIZE ) * 100.0;
  double dResH = ::GetDeviceCaps( hDc, HORZRES  );
  double dPmmV = ::GetDeviceCaps( hDc, VERTSIZE ) * 100.0;
  double dResV = ::GetDeviceCaps( hDc, VERTRES  );

  RECT R;
  R.left   = 0;
  R.top    = 0; 
  R.right  = nPmmWidth  * dPmmH / dResH; 
  R.bottom = nPmmHeight * dPmmH / dResH;

  HDC  hEmfDC = CreateEnhMetaFile( hDC, NULL, &R, NULL );

  // 描画コマンド

  HENHMETAFILE hEmf = CloseEnhMetaFile( hEmfDC );
  
  RECT R2;
  R2.left   = 0;
  R2.top    = 0; 
  R2.right  = nPmmWidth; 
  R2.bottom = nPmmHeight;

  HDC hEmfDC2 = CreateEnhMetaFile( hDC, NULL, &R2, NULL );

  RECT R3;
  R3.left   = 0;
  R3.top    = 0; 
  R3.right  = nPmmWidth  * dResH / dPmmH;
  R3.bottom = nPmmHeight * dResV / dPmmV;

  PlayEnhMetaFile( hEmfDC2, hEmf, &R3 );
  
  HENHMETAFILE hEmf2 = CloseEnhMetaFile( hEmfDC2 );

  // hEmf2を使って作業