GDI+でEMF+をアンチエイリアスなしでレンダリングする

EMF+をレンダリングしようとすると、GDIだけではどうにもならないものの、かといって、GDI+に全てお任せにすると、アンチエイリアスがデフォルトでかかってしまう。一応、GDI+には、アンチエイリアス系をOFFにするメソッド、Graphics::SetTextRenderingHintGraphics::SetSmoothingMode があるのだが、EMF+のレンダリング前にこのメソッドを

g.SetSmoothingMode(Gdiplus::SmoothingModeNone);
g.SetTextRenderingHint(Gdiplus::TextRenderingHintSingleBitPerPixelGridFit);

のように呼び出しておいても全く効果がない。どうやら、ちゃんとEMF+のレコードを解析しないといけないらしい。

GDI+でEMR構造体を使う

Graphics::EnumerateMetafileで指定するコールバック関数、EnumerateMetafileProcを見てみると、EnumEnhMetaFileのコールバック関数EnhMetaFileProcとの比較はそんなに難しくないことがわかる。

結局、recordTypeやflagsが増えたが、これはENHMETARECORDの一部のデータが表に出てきただけ。その代わりに、pStrの先頭から、EMR構造体が無くなった感じ。正直、それだと、GDIの定義する構造体に直接キャストができなくて困るのだが、デバッガなどでメモリの内容を直接のぞいた感じでは、実は、このアドレスから、-8すれば、EMR構造体になっているようだ。つまり、

EMR& emr = *(EMR*)(pStr - 8);

という処理をしても問題ないということ。

EMF+で文字をレンダリングしているのは?

EMF+のレコードをデバッガで追っていて、面白いことを発見した。GDI+では、文字の描画は、Graphics::DrawStringメソッドを使うのだが、実は、その内部では、ExtTextOutが使われているのだ。ということは、逆に言えば、フォントのアンチエイリアスは、CreateFont/CreateFontIndirectしているところで、LOGFONT構造体のlfQualityをいじって、強制的にアンチエイリアスをOFFにしてしまえば良いわけだ。そして、どうやら、EmfRecordTypeExtCreateFontIndirect (EMR_EXTCREATEFONTINDIRECTW/構造体だと、EMREXTCREATEFONTINDIRECTW)を処理すればこれができるらしい。


ということで書いてみたコードは次のような感じ。

Graphics g(...);
Metafile mf(...);
Rect rc(...); // レンダリングしたいサイズで初期化

// 事前にアンチエイリアスをOFFにしておく
g.SetSmoothingMode(Gdiplus::SmoothingModeNone);
g.SetTextRenderingHint(Gdiplus::TextRenderingHintSingleBitPerPixelGridFit);

// 描画
g.EnumerateMetafile(mf, rc, emfProc, &mf, NULL);

そして、本体から呼び出されるコールバックは次のような感じ。

static BOOL CALLBACK emfProc(
  Gdiplus::EmfPlusRecordType recordType,
  unsigned int flags,
  unsigned int dataSize,
  const unsigned char* pStr,
  void* callbackData)
{
  BYTE* buf = NULL;
  
  Metafile* mf = (Metafile*)callbackData;
  if(recordType == Gdiplus::EmfPlusRecordTypeSetAntiAliasMode)
  {
    // 無視する
    return TRUE;
  }
  else if(recordType == EmfRecordTypeExtCreateFontIndirect)
  {
    // pStrを直接書き換えるのはリスクが高いので、
    // コピーしてから書き換え、それをPlayRecordに渡す。
    buf = new BYTE[dataSize + 8];
    pStr = buf;

    CopyMemory(buf, pStr - 8, dataSize + 8);
    EMREXTCREATEFONTINDIRECTW& eecfi = *(EMREXTCREATEFONTINDIRECTW*)buf;
    
    // アンチエイリアスを強制的にOFFにする
    eecfi.elfw.elfLogFont.lfQuality = NONANTIALIASED_QUALITY;
  }
  
  BOOL ret = mf->PlayRecord(recordType, flags, dataSize, pStr);
  
  if(buf) delete[] buf; // バッファ解放
  
  return ret;
}

あとがき

僕の手元では、このコードは、Office 2007のExtending the Office (2007) Fixed-Format Export Featureが出力するEMFを解析する部分で使っているが、このコードは面白いほどちゃんと動作する。実は、EMF+のレコードとしては、他にもフォントレンダリング関連のものがあり、このコードでは全く考慮していないにもかかわらずだ。