はじめに (対象読者・この記事でわかること)

この記事は、Windowsデスクトップアプリケーションで「TIFF画像を表示したい」と悩んでいるC++開発者を対象にしています。
特に、OpenCVのような巨大ライブラリを依存に加えたくない、あるいはリソースが限られた組み込み系環境でTIFFを扱いたい方に最適です。

この記事を読むと、libtiffを使ってTIFFファイルをビットマップ化し、GDI+で画面に描画する一連の流れが実装できるようになります。
サンプルコードはVisual Studio 2022 Communityでコンパイル・動作確認済みです。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - Visual StudioでのWin32/MFCプロジェクト作成とコンパイルの基本 - C++での動的ライブラリ(DLL)リンクとインクルードディレクティブの扱い - GDI+の初期化・終了処理の仕組み

TIFFは標準で読めない:なぜ専用ライブラリが必要なのか

Windows APIやGDI+はBMP/JPEG/PNG/GIFをネイティブに読み込めますが、TIFF(Tagged Image File Format)は対象外です。
TIFFは1ビットモノクロから32ビット浮動小数点、圧縮方式もLZW、ZIP、CCITT、JPEGなどバリエーションが豊富なため、OS側で一律に扱うのが難しいのが理由です。

その結果、WindowsネイティブアプリでTIFFを表示するには、

  1. 専用デコーダを自前で実装する(工数が膨大)
  2. 既存のオープンソースライブラリを組み込む

のどちらかを選ぶ必要があります。
本記事では、2.の代表格であるlibtiffを採用し、最小限の依存でTIFFを扱う方法を説明します。

libtiff+GDI+で最短実装する

vcpkgでlibtiffを導入

vcpkgを使えば、面倒なCMakeビルドや手動でのDLLコピーを回避できます。
管理者権限でDeveloper PowerShell for VS 2022を開き、以下を実行します。

Powershell
> vcpkg install tiff:x64-windows

プロジェクトに統合するには、以下のいずれかを選びます。

  • プロパティページ → 「VC++ディレクトリ」 → インクルード/ライブラリディレクトリにvcpkg\installed\x64-windows\includelibを追加
  • あるいはCMakeを使っている場合はfind_package(TIFF REQUIRED)で解決

インクルードは#include <tiffio.h>だけでOKです。

TIFF→GDI+ビットマップへの変換コード

libtiffはスキャンライン単位で画像を返すため、一旦バッファに展開してからGDI+のBitmapオブジェクトへコピーします。
以下は、モノクロ~24ビットカラー、圧縮非圧縮問わず読める汎用ルーチンです。

Cpp
#include <windows.h> #include <gdiplus.h> #include <tiffio.h> #include <vector> #pragma comment(lib, "gdiplus.lib") using namespace Gdiplus; Bitmap* LoadTiffToBitmap(const wchar_t* filePath) { // 1. TIFFファイルを開く TIFF* tif = TIFFOpenW(filePath, "r"); if (!tif) return nullptr; // 2. 画像サイズとビット数を取得 uint32 w = 0, h = 0; uint16 bpp = 0, samples = 0; TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, &w); TIFFGetField(tif, TIFFTAG_IMAGELENGTH, &h); TIFFGetField(tif, TIFFTAG_BITSPERSAMPLE, &bpp); TIFFGetField(tif, TIFFTAG_SAMPLESPERPIXEL, &samples); // 3. 読み込みバッファを用意(1スキャンライン) const tmsize_t scanSize = TIFFScanlineSize(tif); std::vector<uint8_t> scanline(scanSize); // 4. GDI+用24ビットBMPとして展開 Bitmap* bmp = new Bitmap(w, h, PixelFormat24bppRGB); BitmapData data; Rect lockRc(0, 0, w, h); bmp->LockBits(&lockRc, ImageLockModeWrite, PixelFormat24bppRGB, &data); for (uint32 y = 0; y < h; ++y) { TIFFReadScanline(tif, scanline.data(), y); uint8_t* dst = static_cast<uint8_t*>(data.Scan0) + y * data.Stride; if (bpp == 24 && samples == 3) { memcpy(dst, scanline.data(), w * 3); // BGRのままコピー } else if (bpp == 1) { // 1ビットモノクロ → 24ビットに拡張 for (uint32 x = 0; x < w; ++x) { bool bit = (scanline[x >> 3] >> (7 - (x & 7))) & 1; dst[x * 3 + 0] = dst[x * 3 + 1] = dst[x * 3 + 2] = bit ? 255 : 0; } } // 他のビット深度も同様に変換... } bmp->UnlockBits(&data); TIFFClose(tif); return bmp; }

ハマった点:16ビットグレーで色が反転した

環境依存で16ビットTIFFを扱った際、ピクセル値が0=白、65535=黒という逆順で格納されているケースがありました。
libtiffはPHOTOMETRICタグを見てTIFFTAG_PHOTOMETRICPHOTOMETRIC_MINISWHITEかを返すため、それをチェックして反転処理を入れる必要があります。

Cpp
uint16 photo; TIFFGetField(tif, TIFFTAG_PHOTOMETRIC, &photo); bool reverse = (photo == PHOTOMETRIC_MINISWHITE);

解決策:Photometricを考慮した変換

上記のフラグを見て、16ビット→8ビットへ縮小する際に255 - (val >> 8)とすれば、白黒が反転することなく表示できました。

まとめ

本記事では、libtiffとGDI+を組み合わせることで、WindowsネイティブアプリにTIFF表示機能を追加する手法を解説しました。

  • vcpkgでlibtiffを一発導入
  • TIFFスキャンラインをGDI+ビットマップへ変換
  • ビット深度・Photometricの違いに対応

この方法を使えば、OpenCVなどの大規模ライブラリを依存に加えることなく、約100行程度のコードでTIFF対応が完了します。
次回は、複数ページ(多層)TIFFのページめくり表示と、ズーム/パン機能を追加する方法を紹介する予定です。

参考資料