Windows Imaging Componentで画像を読み込む

Windows Imaging Component (WIC)は、.NET Framework 3.0でも利用されている画像処理のフレームワークで、Vistaでは標準インストールされており、Windows フォトギャラリーなどでも利用されているほか、Windows XPでも、.NET Framework 3.5をインストールしたり、または、WICを直接インストールしたり、あるいは、Windows Live フォトギャラリーをインストールしたりすることによって利用できるようになる。

また、Windows Imaging Componentは、サードパーティによって拡張可能なように設計されており、実際、カメラメーカーからは、RAW画像用のコーデックが公開されていたりもするため、今後、画像処理のコードを書くに当たっては、是非とも利用を検討したいフレームワークだ。

C++からだと、GDI+を使って画像を読み込むことは結構あると思うが、GDI+の画像ローダはバグも多く、Old JPEGなどの比較的難しいフォーマットは読めないことが多い。それから比べると、WICの完成度はかなりのもので、特に何の問題も感じずに利用することができる。.NETからであれば、BitmapDecoder クラスを使って画像のロードを行えるが、C++/CLIではなく、C++から使いたい場合には、COM経由で利用できる。その際、ほとんどのクラスは、IWIC + .NETのクラス名という形で存在している。下記は、利用例(エラーチェックは省いているので注意)。

CComPtr<IWICImagingFactory> imagingFactory;
CoCreateInstance(
  CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER,
  __uuidof(IWICImagingFactory), (LPVOID*)&imagingFactory);

// input.tifを開く
CComPtr<IStream> stream;
SHCreateStreamOnFile(_T("input.tif"), STGM_READ, &stream);

imagingFactory->CreateDecoderFromStream(
  stream, NULL, WICDecodeMetadataCacheOnDemand, &m_decoder);

UINT frameCount = 0;
m_decoder->GetFrameCount(&frameCount);
for(UINT idx = 0; idx < frameCount; idx++)
{
  CComPtr<IWICBitmapFrameDecode> frameDecode;
  m_decoder->GetFrame(idx, &frameDecode);

  UINT width, height;
  frameDecode->GetSize(&width, &height);

  double resX, resY;
  frameDecode->GetResolution(&resX, &resY);
  
  CComQIPtr<IWICBitmapSource> bmpSrc = frameDecode;

  WICPixelFormatGUID fmt;
  bmpSrc->GetPixelFormat(&fmt);
  
  UINT stride;
  if(fmt == GUID_WICPixelFormatBlackWhite)
  {
    stride = (w + 7) ~7;
  }
  else if(fmt == GUID_WICPixelFormat8bppGray)
  {
    stride = w;
  }
  else if(fmt == GUID_WICPixelFormat24bppBGR)
  {
    stride = w * 3;
  }
  else if(fmt == GUID_WICPixelFormat24bppRGB)
  {
    stride = w * 3;
  }
  else
  {
    // Unsupported pixel format
  }
  
  UINT size = stride * height;
  BYTE* buf = new BYTE[size];
  WICRect rc = {0, 0, width, height};
  HRESULT hr = bmpSrc->CopyPixels(&rc, stride, size, buf);
  
  ...
    
  delete[] buf;
}

カラーマネジメント

WICでは、カラーマネジメントシステム(CMS)の利用もかなり簡単になっており、これらは、IWICColorTransformを利用して行う。また、上記のサンプルで、IWICBitmapFrameDecodeがIWICBitmapSourceにキャスト(QueryInterface)可能だったように、IWICColorTransformもIWICBitmapSourceにキャスト可能である。これによって、入力画像に複数の加工を加えて最終出力を得ることができるようになっている。

次の例は、読み込んだ形式がどんな形式であれ、sRGBに変換するコード。ただし、元画像にはカラープロファイルがついていることが前提*1

UINT ret;

// 入力側のカラープロファイルを取得
CComPtr<IWICColorContext> srcCc;
frameDecode->GetColorContexts(1, &srcCc, &ret);

// sRGBのカラープロファイルを作成
CComPtr<IWICColorContext> dstCc;
imagingFactory->CreateColorContext(dstCc);
dstCc->InitializeFromExifColorSpace(1);

// sRGBに変換するトランスフォームの作成
CComPtr<IWICColorTransform> transform;
imagingFactory->CreateColorTransformer(&transform);
transform->Initialize(frameDecode, srcCc, dstCc, GUID_WICPixelFormat24bppBGR);

もし、カラープロファイルがないようだったら、無理矢理変換することも可能だ。ただし、CMYK->RGBなどの変換までちゃんと行えるかどうかについては見検証。

CComPtr<IWICFormatConverter> converter;
imagingFactory->CreateFormatConverter(&converter);

converter->Initialize(
  frameDecode,GUID_WICPixelFormat24bppBGR,
  WICBitmapDitherTypeNone,
  NULL,
  0.0,
  WICBitmapPaletteTypeCustom);

*1:.NET Framework上では、PixelFormatからColorContextを作成することができるのだが、残念ながら、COM上のAPIでは、sRGB, scRGBのColorContextしか作れない

画像を保存

処理が完了した画像を保存するには、IWICBitmapEncoderを利用する。
IWICBitmapEncoderのインスタンス作成時には、WIC GUIDs and CLSIDsにあるGUID_ContainerFormat*の中から、自分が使いたいフォーマットのエンコーダを選択する。

// TIFFエンコーダを作成
CComPtr<IWICBitmapEncoder> encoder;
imagingFactory->CreateEncoder(GUID_ContainerFormatTiff, NULL, &encoder);

次に、書き出し先のIStreamが必要になるが、これは、適宜、作成し、IWICBitmapEncoder::Initializeに渡して初期化する。

// output.tifに書き出す
CComPtr<IStream> stream;
// SHCreateStreamOnFile(_T("output.tif"), STGM_WRITE, &stream);
SHCreateStreamOnFileEx(_T("output.tif"), STGM_WRITE | STGM_FAILIFTHERE, 0, TRUE, NULL, &stream);

encoder->Initialize(stream, WICBitmapEncoderNoCache);

画像自体は、読み込み時と同様、フレームという単位で書き込む。TIFFだと複数ページを保存できるので、フレームを複数持つことができる。その他の画像フォーマットでは、1つのみ。いずれにしても、このフレームは、IWICBitmapEncoder::CreateNewFrameで行う。

// フレーム(ページ)を追加
CComPtr<IWICBitmapFrameEncode> frameEncode;
CComPtr<IPropertyBag2> propBag;
encoder->CreateNewFrame(&frameEncode, &propBag);

このとき、一緒に、IPropertyBag2のインスタンスが取得できるが、エンコードの品質などを設定しない場合には何もしなく良い。このインスタンスを、IWICBitmapEncoder::Initializeに直接渡すことによって、デフォルトの設定を利用できる。

// デフォルトの設定でエンコーダを初期化
frameEncode->Initialize(propBag);

IWICBitmapFrameEncodeに対しては、IWICBitmapFrameEncode::WriteSourceを呼び出して、すでに持っているIWICBitmapSourceのインスタンスを渡せばよい。ここでは、上記のサンプルでsRGBに変換した画像をそのまま利用する。

UINT width, height;
transform->GetSize(&width, &height);
frameEncode->SetSize(width, height);

GUID fmtOut = GUID_WICPixelFormat24bppBGR;
frameEncode->SetPixelFormat(&fmtOut);

WICRect rcSave = {0, 0, width, height};
frameEncode->WriteSource(transform, &rcSave);
frameEncode->Commit();
frameEncode = NULL; // もう必要ないので解放

最後には、IWICBitmapEncoder::Commitを呼び出して作業終了。

// 作成終了
encoder->Commit();
encoder = NULL; // もう必要ないので解放

WIC一覧の取得

上記の方法で、いろいろなファイルが読み込めることがわかったが、一方で、WIC経由で開くことのできる(はずの)ファイル形式の一覧がほしいことがある。端的に言えば、「ファイルを開く」ダイアログのフィルターを作ったりするときに、*.*では格好悪すぎる。
下記のコードは、レジストリの情報を読み込んで、WIC対応のファイル拡張子一覧を取得する。

#include <windows.h>
#include <shlwapi.h>
#include <shlguid.h>
#include <tchar.h>
#include <clocale>
#include "atlbase.h"
#include "atlstr.h"

static CString RegGetString(HKEY hKey, LPCTSTR path, LPCTSTR name)
{
  CRegKey regKey;
  if(regKey.Open(hKey, path, KEY_READ) != ERROR_SUCCESS)
    return _T("");
  ULONG sz = 0;
  if(regKey.QueryStringValue(name, NULL, &sz) != ERROR_SUCCESS)
    return _T("");
  CString s;
  regKey.QueryStringValue(name, s.GetBufferSetLength(sz), &sz);
  s.ReleaseBuffer();
  return s;
}

int main()
{
  std::setlocale(LC_ALL, "");
  
  CString keyPath = _T("CLSID\\{7ED96837-96F0-4812-B211-F13C24117ED3}\\Instance");
  CRegKey regKey;
  if(regKey.Open(HKEY_CLASSES_ROOT, keyPath, KEY_READ) == 0)
  {
    for(DWORD i = 0;; i++)
    {
      CString name;
      LPTSTR pName = name.GetBufferSetLength(MAX_PATH);
      ULONG sz = name.GetLength();
      if(regKey.EnumKey(i, pName, &sz, NULL) != 0)
        break;
      name.ReleaseBuffer();
      
      CString clsid = RegGetString(regKey, name, _T("CLSID"));
      if(clsid.GetLength() == 0)
        continue;
      
      keyPath.Format(_T("CLSID\\%s"), clsid);
      CString exts = RegGetString(HKEY_CLASSES_ROOT, keyPath, _T("FileExtensions"));
      if(exts.GetLength() == 0)
        continue;
      
      CString filters, prevTypeName;
      for(int p = 0; p >= 0;)
      {
        int np = exts.Find(_T(","), p);
        CString ext;
        if(np >= p)
        {
          ext = exts.Mid(p, np - p);
          p = np + 1;
        }
        else
        {
          ext = exts.Mid(p);
          p = -1;
        }
        
        CComPtr<IQueryAssociations> qa;
        AssocCreate(
          CLSID_QueryAssociations, __uuidof(IQueryAssociations),
          (void**)&qa);
        qa->Init(ASSOCF_INIT_DEFAULTTOSTAR, CT2W(ext), NULL, NULL);
        
        DWORD dwSize = 0;
        qa->GetString(ASSOCF_NOTRUNCATE, ASSOCSTR_FRIENDLYDOCNAME,
          NULL, NULL, &dwSize);
        CStringW typeNameW;
        qa->GetString(ASSOCF_NOTRUNCATE, ASSOCSTR_FRIENDLYDOCNAME,
          NULL, typeNameW.GetBufferSetLength(dwSize), &dwSize);
        typeNameW.ReleaseBuffer();
        
        if(typeNameW.GetLength() == 0)
          continue;
        
        CString typeName = CW2T(typeNameW);
        if(prevTypeName == typeName)
        {
          filters += _T(";*") + ext;
        }
        else
        {
          if(prevTypeName.GetLength())
            _tprintf(_T("%s (%s)\n"), prevTypeName, filters);
          prevTypeName = typeName;
          filters = _T("*") + ext;
        }
      }
      if(prevTypeName.GetLength())
        _tprintf(_T("%s (%s)\n"), prevTypeName, filters);
    }
    regKey.Close();
  }
  return 0;
}

これで表示される拡張子一覧の例:

GIF イメージ (*.gif)
PNG イメージ (*.png)
LizardTech DjVu File (*.djvu;*.djv;*.sdjvu;*.sdjv)
ビットマップ イメージ (*.bmp;*.dib)
ペイントブラシの絵 (*.rle)
JPEG イメージ (*.jpeg;*.jpe;*.jpg;*.jfif)
EXIF ファイル (*.exif)
高画質の写真 (*.wdp)
TIFF イメージ (*.tiff;*.tif)
アイコン (*.ico)
ICON ファイル (*.icon)