使ってみたときの感想みたいなもの。 教科書的には役立たないが初めて使ってみるときの視点(いかにsetjmpをなしで済ますかとか)考えたことメモしておきました。 枝葉の間違いもありますが訂正してません。
2007年11月4日 libpng BI_BITFIELDSでRGB48の「最適近似」 (2)
16-16-16のRGB48またはRGBA64のPNGを11-11-10のRGB32のビットフィールドBMPに変換する。 (注: これは普通の変換ではなく興味本位の実験。水増しでない24ビット超の深度をWindows APIが解釈するところがおもしろい。) 前回のはいまいちだったので、もう一度やってみる。 前提として、変数名はそれぞれいかにもな内容で定義されていて、 エラーが起きたらcharまたはWCHAR文字列を投げると適切にクリーンアップされるようになっている、とする。 またこれまでメモリーは好きに確保解放という意味で「確保する」とか言葉で書いていたが、 以下では単純にプロセスヒープから取るように書いてある。(*AllocはHeapAllocの省略)
png_ptr = png_create_read_struct( PNG_LIBPNG_VER_STRING,
(png_voidp) hWnd,
user_error_fn, // This is why we don't need setjmp
user_warning_fn );
if( NULL == png_ptr ) throw L"No memory, png_ptr";
info_ptr = png_create_info_struct( png_ptr );
if( NULL == info_ptr ) throw L"No memory, info_ptr";
png_set_read_fn( png_ptr, (png_voidp) hFile, user_read_data_fn );
png_set_sig_bytes( png_ptr, cbPngSignature );
png_read_info( png_ptr, info_ptr );
int iBitDepth, iColorType;
png_get_IHDR( png_ptr, info_ptr, &ulWidth, &ulHeight,
&iBitDepth, &iColorType, NULL, NULL, NULL );
if( 16 != iBitDepth ) throw L"Not 16 bit/channel";
// png_set_swap( png_ptr );
// png_set_bgr( png_ptr );
libpngで入力PNGを読み込む。 下のスワップとRGB→BGRの変更は今回は後でついでに処理する。
// 10+11+11 = 32 bits totally for RGB, no room for alpha
if( iColorType & PNG_COLOR_MASK_ALPHA ||
png_get_valid( png_ptr, info_ptr, PNG_INFO_tRNS ) )
{
const int response = MessageBox( hWnd,
L"This PNG file has an alpha channel and/or transparency information, "
L"which will be discarded in the output color space (11-11-10 RGB32). Continue?"
, &ofn.lpstrFile[ ofn.nFileOffset ]
, MB_ICONQUESTION | MB_YESNO );
if( IDNO == response ) throw L"";
if( iColorType & PNG_COLOR_MASK_ALPHA ) png_set_strip_alpha(png_ptr);
}
if( ! ( iColorType & PNG_COLOR_MASK_COLOR ) ) png_set_gray_to_rgb( png_ptr );
png_read_update_info( png_ptr, info_ptr );
const png_uint_32 rowbytes = png_get_rowbytes( png_ptr, info_ptr );
if( rowbytes != ulWidth * 6 ) throw L"Failed to convert to 6 bytes/pixel";
row_pointers =
(BYTE **) HeapAlloc( GetProcessHeap(), 0, sizeof( *row_pointers ) * ulHeight );
if( NULL == row_pointers ) throw L"No memory, row_pointers";
for( i = 0; i < ulHeight; i++ )
{
row_pointers[ i ] = BytesAlloc( rowbytes );
if( NULL == row_pointers[ i ] ) throw L"No memory, one of row_pointers";
}
// output color space is RGB32, 4 bytes/px
const ULONG cbRow = ulWidth * 4;
pDIB = (BYTE *) HeapAlloc( GetProcessHeap(), 0, cbRow * ulHeight );
if( NULL == pDIB ) throw L"No memory, pDIB";
png_read_image( png_ptr, row_pointers );
png_read_end( png_ptr, NULL );
今回はRGBA64だったらアルファを落としてRGB48に変換する。
BITMAPV5HEADER bi; // V5 to make use of RedMask etc.
{
memset( &bi, 0, sizeof( bi ) );
bi.bV5Size = sizeof( BITMAPINFOHEADER ); // pretend to be an old header just in case
bi.bV5Width = (LONG) ulWidth;
bi.bV5Height = (LONG) ulHeight; // positive, we make rows 'upside down'
bi.bV5Planes = 1;
bi.bV5BitCount = (WORD) ( 4 * CHAR_BIT ); // 4 bytes / px
bi.bV5Compression = BI_BITFIELDS;
bi.bV5SizeImage = cbRow * ulHeight;
bi.bV5ClrUsed = 0;
const DWORD b10 = (0x1|0x2|0x4|0x8 | 0x10|0x20|0x40|0x80 | 0x100|0x200);
const DWORD b11 = ( b10 << 1 | 0x1 );
bi.bV5BlueMask = b10; // 10 bits
bi.bV5GreenMask = ( b11 << 10 ); // 11 bits
bi.bV5RedMask = ( b11 << (11+10) ); // another 11 bits
}
BITMAPFILEHEADER bfh;
memset( &bfh, 0, sizeof( bfh ) );
bfh.bfType = MAKEWORD( 'B', 'M' );
// DWORD * 3 for color masks
bfh.bfOffBits = sizeof( bfh ) + sizeof( BITMAPINFOHEADER ) + sizeof( DWORD ) * 3;
bfh.bfSize = bfh.bfOffBits + ulHeight * ulWidth * 4;
ビットフィールドなので、 BITMAPINFOHEADERとその直後にDWORD 3個、という構造が要る。 そういう構造体を自分で定義してもいいし、動的に確保してもいいのだが、 ここでは手抜きをしてV5ヘッダを流用した。V5ヘッダのマスクの位置はちょうど旧式ヘッダの後ろにDWORDを置いた場所にあるので。 bV5Sizeには旧式のサイズを指定して旧式ヘッダのふりをしておく。 仕様上、後でBITMAPINFOへのポインタを渡す場所があって、BITMAPINFOの前半は正式には旧式ヘッダだから。 前回はマスクを魔法的に書いたが、今回は何をやってるのか分かるようにした。 b10は2進数で1が10個並んでいる数、b11は同11個並んでいる数。 分かりやすく逆転させたが、メンバーの順序は実際にはbV5RedMaskが先でそこの三つはメモリー上ひっくり返しになっている。
ここで出力ファイル(BMP)をfoutとして開いたとする。 Windows APIのファイルIOでも同じことなので、そこは適当に。。
fwrite( &bfh, sizeof( bfh ), 1, fout );
fwrite( &bi, sizeof( BITMAPINFOHEADER ) + sizeof( DWORD ) * 3, 1, fout );
for( i = 0; i < ulHeight; i++ )
{
ULONG h = ulHeight - 1 - i; // upside down
for( j = 0; j < ulWidth; j++ )
{
const BYTE * const p = &row_pointers[ h ][ j * 6 ];
// UINT b = ( (UINT) *(p+0) | (UINT) *(p+1) << 8 );
// UINT g = ( (UINT) *(p+2) | (UINT) *(p+3) << 8 );
// UINT r = ( (UINT) *(p+4) | (UINT) *(p+5) << 8 );
const UINT r = ( (UINT) *(p+0) << 8 | (UINT) *(p+1) );
const UINT g = ( (UINT) *(p+2) << 8 | (UINT) *(p+3) );
const UINT b = ( (UINT) *(p+4) << 8 | (UINT) *(p+5) );
const DWORD dw= (
r >> 5 << (11+10)
|
g >> 5 << 10
|
b >> 6
);
fwrite( &dw, sizeof(dw), 1, fout );
memcpy( &pDIB[ i * cbRow + j * 4 ], &dw, sizeof(dw) );
}
}
hBitmap = CreateDIBitmap( hdc, (BITMAPINFOHEADER *) &bi, CBM_INIT,
pDIB, (BITMAPINFO *) &bi, DIB_RGB_COLORS );
biはBITMAPINFOHEADERのふりをしているが実はV5ヘッダなのでDWORD 3個分、余計にやっても問題ない。そこにマスクが書いてあるのだった。 マスクは書く順番は赤が先だがデータ本体はBGRの順なので赤のマスクは上位バイトでマスク自体は降順になる (ビットフィールドなので両方逆にしても動作するが)。 libpngにBGRに変換させ、スワップもさせると、コメントアウトした行のようになるが、 どうせ右辺でビットをいじるので、ついでに自分でやるといくらか節約になる。 BGRに変換してないのでバイト単位で読むとRGBの順番に出てくる。RGB48なので色チャンネルごとに2バイト(16ビット)ある。 そしてスワップも指定しなかったので、ビッグエンディアンであり、先に来るやつが上位バイト。 前回は冗長にマスクの論理積を取っていたが、b、g、rは自分自身の色チャンネルに関するビットしか持ってないのでマスクは必要ない (やっても害はない)。1ピクセルの色が32ビットにパックされてDWORDでファイルに行くのが分かる。 一方、pDIBへのmemcpyだが、これはファイルの変換には必要ないがAPIが11-11-10を扱えることのデモとしてついでにつけておいた。 要はファイルに書いたのと同じDWORDをオフセット(1列のバイト数x縦座標)+(横座標のピクセル位置x4bpp)の位置のメモリ上に書いているだけ。 一つ注意としてDIBとDDBで縦座標の方向があべこべなので、i=0のとき最終の段から始めて逆順で走査線を取っている。 APIだけならヘッダの高さを負にしてもいいのだが、そこを負にしたBMPファイルはいかにも手抜きなので、ちゃんと本来の順番にした。
最後のhBitmapが成功したかどうかはDCに選択してみれば分かる。V5ヘッダを第2変数では旧式ヘッダと称して、 第5変数ではBITMAPINFOと称して使い回しているが、実際に必要な全メンバーは正当なので問題ない(構造体のサイズだけはあらかじめうそをセットしているが、 正直に書いても動作する)。V5だと未使用の変数があって厳密には少し無駄がある。
2007年11月3日 BI_BITFIELDSでRGB48の「最適近似」 (1)
RGB48のPNGをWindowsのRGB24にマップするとき、普通は16:16:16を8:8:8にしている。 これは音楽で言えば16ビットWAVを8ビットでインポートするような暴挙だ。 ところで、渡す先の変数は32ビットなので、8:8:8まで撤退しなくても、11:11:10の受け皿はある。
// 単に切り捨てる普通のアプローチに代わって・・・
// if( 16 == iBitDepth ) png_set_strip_16( png_ptr );
// 以下、色チャンネルあたり16ビット深度を仮定する
if( 16 == iBitDepth ) png_set_swap( png_ptr );
// 11:11:10合計32ビット
const DWORD dwColorMasks[ 3 ] = { 0xFFE00000, 0x1FFC00, 0x3FF };
// ここは64ビットの色からr/g/bを抜き出しているだけ。本当は
// GetRValue32とかMakeRGB32とか定義してきれいに書くところだろうがインデントさえ適当で放置
WORD b = (WORD)( row_pointers[ h ][ j*8 ] |
row_pointers[h][j*8+1] << 8 );
WORD g = (WORD)( row_pointers[ h ][ j*8+2 ]|
row_pointers[h][j*8+2+1] << 8 );
WORD r = (WORD)( row_pointers[ h ][ j*8+4 ] |
row_pointers[ h ][ j*8+4+1 ] << 8 );
// RGBの詰め込み方 一応これで精度が3ビットアップ(8倍精度)
// LSB5~6ビット捨てて11:11:10と並べている
// 右シフトと左シフトの間で論理和を取った方が少し見やすいかも(このマスクは冗長)
// どうしてrとgが1ビット多いのかは恣意的。「32が3で割り切れないから」しわ寄せ
DWORD dw=
(
((DWORD)r>>5<<21)&dwColorMasks[0]
|
((DWORD)g>>5<<10)&dwColorMasks[1]
|
((DWORD)b>>6)&dwColorMasks[2]);
// 上の詰め込みを各列各ピクセルにやってpDIBを用意(略)
BITMAPINFO * pbi = (BITMAPINFO *) 確保( sizeof(BITMAPINFOHEADER)
+ sizeof(DWORD) * 3 );// マスクの分DWORD 3個
// pbi->bmiHeader を普通に用意する。ただし、
// pbi->bmiHeader.biCompression = BI_BITFIELDS
memcpy( &pbi->bmiColors[ 0 ], &dwColorMasks[0], sizeof(DWORD) * 3 );
hBitmap = CreateDIBitmap( hdc, &pbi->bmiHeader, CBM_INIT,
pDIB, pbi, DIB_RGB_COLORS );
解放(pbi)
BI_BITFIELDSのときのCreateDIBitmapはドキュメントが整っていないが、 まずDIB_RGB_COLORSなのかDIB_PAL_COLORSなのか。 カラーテーブルを参照するという意味ではパレットに近い部分もある。 やってみると、どちらでも動作してしまうようだ。 biClrUsedは3にするのか? これも0でも3でも動作してしまう。 分かりにくいのは、CreateDIBitmapではbiを渡す場所とbihを渡す場所があるが、 マスクテーブルはbiを渡す場所(第5変数)で読まれる。 V4やV5のヘッダーにはマスクを書く場所があるのでbihでマスクを渡せそうだが、それはできない。 またマスクは場所が確保されているだけではだめで、正当なマスク(RGBのマスク間に切れ目やオーバーラップがないこと。 必ずしも32ビット使い切る必要はない)でないと動作しない(ある意味当たり前)。 V4やV5のヘッダーでマスクを書いても駄目だと言ったが、第5変数でbih*にキャストしてアドレスを渡すのなら、 V4/5ヘッダのマスクの位置はちょうどsizeof(BITMAPINFOHEADER)+sizeof(DWORD)*n (n=0,1,2)なので、動作する。 (biSizeに当たるメンバーの値が違うので保証はない) 分かりにくいが第2変数のbihでやるのは駄目。
このケチ方式11:11:10を.bmpファイルに保存することもできる。 普通bmpファイルのRGB32はRGB24の無意味な水増しだが、 上記の場合は増加した8ビットが1ビットも余さずsignificantで理論的に気持ちいい。 しかしハードウェアデバイスがそもそも2^24色しか表示できないとしたら、あまり意味はないのだが。
補足として、 ((DWORD)b>>5<<21)&dwColorMasks[0] のようなのは、 PNGから持ってきたビットでビットの順序は大丈夫なのか、というと、 デバイスレベルではさかさまだったりするだろうけれど、 Cのシフト演算子の左右は抽象レベルなのでバイトオーダーさえあっていれば、普通には問題は見えない。 (構造体のUINT...:1みたいなビットフィールドを書いてビットに直接触ろうとすれば問題になる。) ※さかさまというのはプログラマーの認識に対してさかさまなだけで、 Windowsネイティブのリトルエンディアンがバイトレベルだけでなくビットレベルでも通常の感覚と違うだけで、 エンディアンネスのさかさまの問題ではない。 ただし上記ではlibpngにpng_set_swap( png_ptr )させて、あとからビットやバイトをこねくっているが、 どうせこねくらないといけないならpng_set_swapせず、こねくるついでに自分で逆転したほうが合理的だろう。
2007年11月3日 RLEなBMP
この画像はBMP形式だが、500×600ピクセルの大きさのわりに、 サイズが24.0 KiB しかない。BMPのくせになまいきだ。 (画像はユーロ通貨の説明図だが、内容はどうでもよく、単なるフォーマットのサンプル。)
じつはBMPといっても無圧縮BMPではなく、RLEで圧縮してある(といっても標準BMPの仕様の範囲内)。 BMPもなかなかやるなと思われただろうか。 確かに無圧縮BMPなら最適の4ビット(16色パレット)でも48 KiB、フルカラーBMPなら879 KiBだから、 同じBMPで24 KiBまで縮むのは注目に値する。 とはいえ、GIFにすれば、12.0 KiB まで小さくなるし、 PNGにすれば8.8 KiB になる。あくまで「BMPにしては」すごい、ということ。
RLE圧縮については、詳細はドキュメントに出ているが、 パレット形式のBMPを作ったあと、biCompression を BI_RLE8 または BI_RLE4 に設定する。 大雑把に、「ピクセルごとに色がばらばらのところは1ピクセルずつ記録」「色が並んでいるところはv番の色をn個並べる」と2バイトで速記する。
// RLE8の概念
while( pos < width )
{
if(色が不連続){
// 以下、色がばらばら区間がlenピクセル続くから、1ピクセルずつ色番号を書くのでよろしく
fputc( 0x00, fout );
fputc( (BYTE) len, fout );
// 色コードをべたで書く
fwrite( &pLine[ start ], sizeof(BYTE), len, fout );
}
else // v番の色がnピクセル連続だからよろしく
{
fputc( (BYTE) n, fout );
fputc( (BYTE) v, fout );
}
} // while
16色パレットの場合は、RLE4方式も使える。 上のサンプルは色は16色以内なのでRLE4と8もどちらも可能で、上のはRLE4。 色は16色以内だが形式的に256色パレットにして RLE8 を使っても、このサンプルでは24.6 KiB になる。 RLE4だと連続する色だけでなく、121212…のように交互に並ぶ色をうまく扱えるが、この例ではそういう(擬似中間色的な)場所がほとんどなく、 あまり差が出なかった。 下の鳥のBMPもRLE8にすればもう少し小さくなる。
2007年11月2日 libpng (4) BMPの方が小さいことも。パレット形式のBMP vs. RGB形式のPNG
このサンプルは200色しか使っていないのでパレット(インデックスカラー)が効率的だが、PNGはあえてRGBでしかも圧縮を弱くしてある。 一方、BMPの方をパレットにして、ぎっしり詰め込んだ。 入力に対していずれもロスレスだが、PNGは97KiB、BMPは89KiBと同じデータがPNGの方がかえって大きくなってしまっている。 必要に応じてパレットを使うことの重要性が分かる。
pngのファイル保存は、ファイルを開くのを逆にしたような感じで、 row_pointersを高さピクセルと同じ本数用意してライブラリに渡せばいい。 まずパレットを使わない普通の方法。
png_init_io( png_ptr, fp );
//圧縮率の調整。デフォルトは6になっている
//png_set_compression_level( png_ptr, Z_BEST_COMPRESSION );
row_pointers = (BYTE**) 確保( sizeof( *row_pointers ) * height );
for( unsigned int i = 0; i < height; ++i )
{
unsigned int row = ( height - 1 ) - i;
row_pointers[ row ] = pDIB + cbRow * i;
}
png_set_IHDR(
png_ptr,
info_ptr,
(png_uint_32) width,
(png_uint_32) height,
CHAR_BIT,
PNG_COLOR_TYPE_RGB,
PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT,
PNG_FILTER_TYPE_DEFAULT );
png_set_rows( png_ptr, info_ptr, row_pointers );
png_write_png( png_ptr, info_ptr, PNG_TRANSFORM_BGR, NULL );
PNG_TRANSFORM_BGRはバイトの順序を逆にする。 ほとんど何もやることがない。 pDIBに保存するべきビットが入っているとする。 WindowsのDIBは縦座標が上から下なので、row_pointersのインデックスを逆さまに振っている。 保存元のpDIBは例えば GetDIBits で hBitmap から変換すれば良い。 また hBitmap は、原理的にはhdcが与えられているときに GetCurrentObject( hdc, OBJ_BITMAP ) でいきなり抜いてもいいが、 保存などのビット転送中に他のスレッドから変更されたりすると困るので、 作業用コピーで進めよう。
hdcDst = CreateCompatibleDC( hdcSrc ); // 作業用(後で削除)
hBitmap = CreateCompatibleBitmap( hdcSrc, cx, cy ); // 作業用(後で削除)
hOldBitmap = SelectObject( hdcDst, hBitmap );
BitBlt( hdcDst, 0, 0, cx, cy, hdcSrc, 0, 0, SRCCOPY );
SelectObject( hdcDst, hOldBitmap );
さてフルカラーで保存はこれでいいとして、パレットの場合。 パレットで保存できるとしたら(256色以内)、pDIBでなくてピクセルごとに色のインデックスが並んでいるrow_pointersを渡すことになる。 RGBで書いてあるpDIBから色番号に変換する。 実際問題、やってみないことには256色で間に合うのかどうか分からないが、 やってみて、駄目ならフルカラーにしておけばいい。
row_pointers = (BYTE**) 確保( sizeof( *row_pointers ) * height );
// pointers pointing nothing so far
for( i = 0; i < height; ++i ) row_pointers[ i ] = NULL;
for( i = 0, row = height - 1; i < height; ++i, --row )
{
row_pointers[ row ] = (BYTE*) 確保( sizeof( **row_pointers ) * width );
if( NULL == row_pointers[ row ] ) throw エラー処理
for( j = 0; j < width; ++j )
{
const BYTE * pByte = pDIB + cbRow * i + nBytesPerPixel * j;
ThisPixel.blue = *(pByte);
ThisPixel.green = *(pByte+1);
ThisPixel.red = *(pByte+2);
unsigned int idx;
for( idx = 0; idx < nColors; ++idx )
{
if( 0 == memcmp( &ThisPixel, &palette[ idx ], sizeof(png_color) ) ) break;
}
if( idx == nColors )
{
if( nColors < nMaxColors )
{
memcpy( &palette[ nColors ], &ThisPixel, sizeof( png_color ) );
++nColors;
}
else throw パレット作戦中止 // more than 256 colors found
}
// index to the color of this pixel
row_pointers[ row ][ j ] = (BYTE) idx;
}
}
row_pointersにはheight個のマス目があって、 成功時にはどのマス目にもそれぞれの縦座標に応じた横一列の画像情報を格納した場所へのポインターが入る。 row_pointersを確保した直後にはどのマス目もゴミ値なので明示的にNULLにしている。 (いちいちNULLを入れるのは面倒なので、できればここは最初から0に初期化される確保を使った方がいい。) 深い意味はなく、NULLを入れておかないと、途中まで確保して色数オーバーなどで終了するとき、 どのマス目が確保したポインターでどのマス目がゴミ値なのか分からない(どれを解放していいのか分からない)、 というだけ。何個確保したか基本的には、ループ変数から分かるのでそっちを見てもいいがループがブレイクするのは、 色オーバー(列確保後)だけでなくメモリー不足(列確保前)もあるのでNULLかどうかで見れるほうがいい。
1ピクセルごとにカラーテーブルを全部調べるとはいかにも効率が悪く泥臭いが(※純粋理論的にはほかにどうしようもないが、 現実に人間が使うビットマップは経験的に色がとなりのピクセルと連携しており、 インデックスのふりかたの工夫そのほか、パレット1番から順にチェックするより確率論的に平均速度が速い検索が考えられる)、 新しい色が現れたらパレットに追加して番号をつける。色番号をrow_pointersのしかるべき番地に書いていく。 途中で256色オーバーしちゃったらあきらめて普通に保存に切り替える。 一見遅そうだが、やってみるとそんなことはなく、PNGの圧縮の時間の誤差に吸収されてしまう感じなのでまあ実用になるということで。 このアルゴリズムをかませておくことで、かなりの確率でPNGが他のソフトより小さくなる。 もちろん専用のPNG最適化ソフトには、かなわないが「色を数えて256以下ならパレットにしろ」という地道な努力をやるかやらないかでも、 場合によってずいぶん差が出る。 memcmpやmemcpyを使わなくても、色は32ビットの識別子として例えばDWORDでもう少し何とかなるが、 RGBの順番が分かりにくいので構造体のメンバーにべたに突っ込んでおいた。 Windowsでは番地の若い方からB、G、Rと並んでいる。無事全部インデックス番号に変換できたら、
png_set_PLTE( png_ptr, info_ptr, palette, (int) nColors );
// 8 4 2 or 1
const int depth = ( nColors > 16 )? 8 : ( nColors > 4 )? 4 : ( nColors > 2 )? 2 : 1 ;
png_set_IHDR( png_ptr,
info_ptr,
(png_uint_32) width,
(png_uint_32) height,
depth,
PNG_COLOR_TYPE_PALETTE,
PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT,
PNG_FILTER_TYPE_DEFAULT );
png_set_rows( png_ptr, info_ptr, row_pointers );
png_write_png( png_ptr, info_ptr,
PNG_TRANSFORM_PACKING, // pack if less than 8 bits; eg 4-bit (16-color) palette
NULL // reserved
);
パレットを設定してから(BMPのRGBQUADに当たる部分)、PNG_COLOR_TYPE_PALETTEで本体を書き込む。 そのときPNG_TRANSFORM_PACKINGを指定しておくと、256色どころか16色で足りる場合、 つまり色番号が0xFまでで表現しつくせる場合、1バイトを2つに分けて、1バイトに2ピクセル(4ビットに1ピクセル)詰めてくれる。 単純計算でファイルサイズが半分で済む。 パレットなので(RGBはないので)PNG_TRANSFORM_BGRは必要ない。 なお、実際にはあまりないだろうが、同じ原理で、もっと詰められればもっと詰めてくれる。 10色くらいは、簡単な図表などであるだろう。 BMPファイルでもパレット方式の(カスタムカラーの)256色や16色は使えるが、もちろんPNGの方が収納効率が良い。
2007年10月31日 libpng (3)
一つ修正した。入力の色深度が1チャンネル16ビットの場合、出力がRGB24/32でも背景色はRGB48で設定しないと、正しく透過しない。 ライブラリ側の仕様も分かりにくい。「RGB24でこの色とアルファブレンドしろ」という「この色」をRGB48で通知しろ、 というのは直観に反している。
png_color_16 bg;
bg.red = GetRValue( rgbBgColor );
bg.green = GetGValue( rgbBgColor );
bg.blue = GetBValue( rgbBgColor );
if( 16 == iBitDepth ) bg.red <<= 8, bg.green <<= 8, bg.blue <<=8; // この1行で解決
png_set_background( png_ptr, &bg, PNG_BACKGROUND_GAMMA_SCREEN, 0, 1.0 );
追加として、色データと別にtRNSとして透過色情報を持っているファイルがある。 対応するには、
if( png_get_valid( png_ptr, info_ptr, PNG_INFO_tRNS ) )
{
png_set_tRNS_to_alpha( png_ptr );
}
switch文で色タイプごとに場合分けして書いたが、 変換の内容を整理すると、
これだけなので、こう書ける。
if( iColorType & PNG_COLOR_MASK_PALETTE )
{
png_set_palette_to_rgb( png_ptr );
}
else
{
if( ! ( iColorType & PNG_COLOR_MASK_COLOR ) ) png_set_gray_to_rgb( png_ptr );
if( 16 == iBitDepth ) png_set_strip_16( png_ptr );
}
最後に、Windowsプログラミングである以上、 ファイルからの読み込みにC標準の入出力でなく、Windows APIを使う場合があるだろう。 これをカスタマイズする場合、 dataというバッファに新たにlengthバイト読み込め、と通知してくるので、それに対応するようなハンドラーを書く。 エラー時にはpng_errorを呼び出す形にしておく。
static void user_read_data_fn( png_structp png_ptr, png_bytep data, png_size_t length )
{
DWORD check;
if( ! ReadFile( (HANDLE) png_ptr->io_ptr, data, (DWORD) length, &check, NULL )
|| check != length ) png_error(png_ptr, "Read error" );
}
後は、それをこう登録する。 2番目の変数はユーザー定義で自由に使えるが、 hFileをここで渡しておくことで、 ハンドラーからは上のようにpng_ptr->io_ptrとしてアクセスできる。 (もちろんhFileは最終的に自分で閉じる。)
hFile = CreateFile( szPngFile, ... );
png_set_read_fn( png_ptr, (png_voidp) hFile, user_read_data_fn );
2007年10月30日 libpng (2)
前回触れたようにWindowsアプリから使う場合、「低水準」の方法の方がかえって簡単になる。 次のように、png_read_info を呼んで png_get_IHDR でメインヘッダーから形式などの情報を取得する。
// const size_t cbPngSignature = 8;
png_bytepp row_pointers = NULL;
try
{
fp = 開く
if( NULL == fp ) throw "Can't open file";
//BYTE h[ cbPngSignature ];
//fread( h, sizeof( BYTE ), cbPngSignature, fp );
//if( ! png_check_sig( h, cbPngSignature ) ) throw "Not a PNG file";
png_ptr = png_create_read_struct( PNG_LIBPNG_VER_STRING,
(png_voidp) NULL,
(png_error_ptr) user_error_fn,
(png_error_ptr) user_warning_fn
);
if( NULL == png_ptr ) throw "No memory, png_ptr";
info_ptr = png_create_info_struct( png_ptr );
if( NULL == info_ptr ) throw "No memory, info_ptr";
png_init_io( png_ptr, fp );
//png_set_sig_bytes( png_ptr, cbPngSignature );
png_read_info( png_ptr, info_ptr );
png_uint_32 ulWidth, ulHeight;
int iBitDepth, iColorType;
png_get_IHDR( png_ptr, info_ptr, &ulWidth, &ulHeight,
&iBitDepth, &iColorType, NULL, NULL, NULL );
コメントアウトしているSignatureの検証は普通はアプリ側でやるが、ライブラリにやらせてもいい。 ファイルの形式が分かったら、次のように何でもかんでも1チャンネルが8ビットのTrue Colorに変換する。
switch( iColorType )
{
case PNG_COLOR_TYPE_GRAY: // 1 2 4 8 16
case PNG_COLOR_TYPE_GRAY_ALPHA: // 8 16
if( 16 == iBitDepth ) png_set_strip_16( png_ptr );
png_set_gray_to_rgb( png_ptr );
break;
case PNG_COLOR_TYPE_RGB: // 8 16
case PNG_COLOR_TYPE_RGB_ALPHA: // 8 16
if( 16 == iBitDepth ) png_set_strip_16( png_ptr );
break;
case PNG_COLOR_TYPE_PALETTE: // 1 2 4 8
png_set_palette_to_rgb( png_ptr );
break;
default:
throw "Unsupported color type";
break;
}
グレースケールの場合、1チャンネルが16ビットだったら8ビットに劣化圧縮する。それ以外は、 8ビット以外だった場合に8ビットに水増しするまでもなく、RGBに変換すれば最終結果はそうなる。 RGBの場合は、8か16なので16ビットを切り捨てる以外何もしない。 パレットの場合、16ビットはないのでRGBに変換させるだけ。ここまで登録した変換でアルファがあれば32ビット、なければ24ビットで出力だが、 前回見たように24ビットは扱いにくいので、次のように32ビットに水増しする。 あと、エンディアンのひっくり返し。
png_set_bgr( png_ptr );
png_set_filler( png_ptr, 0xFF, PNG_FILLER_AFTER );
0xFFは0x00でも何でもいい。未使用の最上位バイトで、リトルエンディアンでは色のチャンネルのAfterになる。 png_set_fillerはもともとアルファチャンネルがある場合には何もせず、ない場合に24→32ビットに水増しする。 そういう内部の約束に頼らず、こういうふうに書いたほうが安心そうだが、、、
// WRONG
png_read_update_info( png_ptr, info_ptr );
if( 3 == png_get_channels( png_ptr, info_ptr ) )
{
png_set_filler( png_ptr, 0xFF, PNG_FILLER_AFTER );
}
前回触れたようにlibpngは情報を取得するだけで構造体の内部状態が変わってしまうトリッキーな部分があって、 上記ではうまくいかない。PNG_COLOR_TYPE_GRAYとPNG_COLOR_TYPE_RGBは3チャンネル、 PNG_COLOR_TYPE_GRAY_ALPHAとPNG_COLOR_TYPE_RGB_ALPHAは4チャンネルでそれらは問い合わせる必要ないが、 PNG_COLOR_TYPE_PALETTEは透過があれば4、なければ3チャンネルに変換されていて単純には分からない。
このあたりで、描画先の背景色を登録しておく。 libpngは透過やアルファを処理できるが、当然ながら透過したらそこは何色なのか教えてあげないと、透過させられない。 以下はWindowsのシステムカラーでボタン(というかオブジェクト一般の)表面の色(要するに今のシステムカラーのデフォルト)を背景とした場合(ボタンの上にPNG画像が乗るような場合)。 このように背景が単一色の場合、アルファブレンドまでライブラリで面倒を見てもらえる。
DWORD rgbBgColor = GetSysColor( COLOR_3DFACE );
png_color_16 bg;
bg.red = GetRValue( rgbBgColor );
bg.green = GetGValue( rgbBgColor );
bg.blue = GetBValue( rgbBgColor );
if( 16 == iBitDepth ) bg.red <<= 8, bg.green <<= 8, bg.blue <<=8;
png_set_background( png_ptr, &bg, PNG_BACKGROUND_GAMMA_SCREEN, 0, 1.0 );
redなどのメンバーは16ビットで、ファイルのデータがRGB48/64の場合16ビット精度で背景を教える必要がある。 同時にこの辺でpng_set_gammaを呼んでガンマ補正をかけることができる。 PNG専用のライブラリなので当然だが、GDI+より強力だ。 今回は前回と違い、自分でrow_pointersを割り当てて、渡すやり方。 (※ライブラリに確保解放をさせる最初の方法ではrow_pointersはtry内のブロック変数でいいが、 自分で確保解放する場合にはcatchの下で解放できるようにtryより上からスコープを作る。 最初の方法から移行するときブロック変数の宣言を消し忘れるとcatchの下のポインタはtryの上の初期状態のままで、 解放されないので要注意)
png_read_update_info( png_ptr, info_ptr );
const png_uint_32 rowbytes = png_get_rowbytes( png_ptr, info_ptr );
if( rowbytes != ulWidth * 4 ) throw "Failed to convert to 4 bytes/pixel";
pBits = 割り当てるバイト数は( rowbytes * ulHeight );
if( NULL == pBits ) throw "No memory, pBits";
row_pointers = (png_bytepp) 割り当てる( sizeof( png_bytep ) * ulHeight );
if( NULL == row_pointers ) throw "No memory, row_pointers";
for( png_uint_32 row = 0; row < ulHeight; ++row )
{
row_pointers[ row ] = &pBits[ rowbytes * row ];
}
png_read_image( png_ptr, row_pointers );
png_read_end( png_ptr, NULL );
前回のpng_read_pngは今回のpng_read_info、png_read_image、png_read_endをまとめてやってくれたが、 それだと細かな扱いができず、かえってポインターを受け取ってからビット数の調整などやっかいだった。 今回のやり方だと、row_pointersには読み込む画像のフォーマットによらず常に32ビット/ピクセルで入っているので、後の処理がやりやすい。 なお、row_pointersは前回と同じく実体はunsigned char **であり、pBitsで確保ずみの領域のうち、 各rowの先頭バイトを格納すべきアドレスを保管(してlibpngに通知する)ものである。
png_read_image( png_ptr, row_pointers );
・・・で「この配列に詰めて」と通知し、呼び出しから戻るとそうなっている(配列の長さはulHeight)。 実際のバッファはpBitsで今回のrow_pointersはpBitsのいくつかのアドレスの別名なのだが、 その「別名を保管する配列」を動的に確保しているので、こちらも後で解放しなければならない。 当たり前だろうが、libpngの使い方を解説しているページの中に、こちらを解放し忘れている例があったので、念のために注記しておく。 前回のやり方ではライブラリ側がrow_pointersの確保・解放の責任を持ったが、今回はそうではない。
// 実際にはBITMAPINFOHEADERだけで問題ないが、一応ちゃんと書いた。RGBQUAD 1個が放置されている。
BITMAPINFO bi = {0};
bi.bmiHeader.biSize = sizeof( BITMAPINFOHEADER );
bi.bmiHeader.biWidth = (LONG) ulWidth;
bi.bmiHeader.biHeight = - (LONG)( ulHeight );
bi.bmiHeader.biPlanes = 1;
bi.bmiHeader.biBitCount = (WORD) ( 4 * CHAR_BIT );
bi.bmiHeader.biCompression = BI_RGB;
bi.bmiHeader.biSizeImage = rowbytes * ulHeight;
bi.bmiHeader.biClrUsed = 0;
bi.bmiHeader.biClrImportant = 0;
hBitmap = CreateDIBitmap( hdc, (BITMAPINFOHEADER *) &bi, CBM_INIT,
pBits, &bi, DIB_RGB_COLORS );
}
catch( const char * error_msg )
{
MessageBoxA( NULL, error_msg, "Error", MB_OK | MB_ICONEXCLAMATION );
}
if( row_pointers ) 解放せよ( row_pointers );
if( pBits ) 解放せよ( pBits );
if( NULL != png_ptr || NULL != info_ptr )
{
png_destroy_read_struct( ( NULL != png_ptr )? &png_ptr : NULL,
( NULL != info_ptr )? &info_ptr : NULL,
NULL );
}
if( NULL != fp ) fclose( fp );
後は行き先のhdcでこのhBitmapをSelectObjectするだけ。
png_destroy_read_structの呼び方がちょっと変だが、これは、tryの中が
png_ptr = png_create_read_struct...
if( NULL == png_ptr ) throw...
info_ptr = png_create_info_struct( png_ptr );
if( NULL == info_ptr ) throw...
こうなっていて、あり得ないことだがライブラリが初期化に失敗したとき、NULLが返る。 そのさい、 1回めでNULLが返ったら両方NULLでpng_destroy_read_structは必要ないが、 もし2回めでNULLが返ったら、png_ptrだけ解放する。 どちらか任意の一方がNULLでも良い、とまでしなくても、info_ptrがNULLならpng_ptrもNULLなので 実際にはもう少し簡単に書ける。
2007年10月29日 libpng (1)
PNG画像をWindowsプログラムで扱うのに、読み込みだけなら、GDI+が(低速・肥大だが)手軽だ。
#undef WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <gdiplus.h>
#pragma comment(lib, "gdiplus.lib")
using namespace Gdiplus;
GdiplusStartupInput gs; // 以下4行は構造体の説明で実際には不要
gs.GdiplusVersion = 1;
gs.DebugEventCallback = NULL;
gs.SuppressBackgroundThread = FALSE;
gs.SuppressExternalCodecs = FALSE;
ULONG_PTR gdiplusToken;
GdiplusStartup( &gdiplusToken, &gs, NULL );
Bitmap * gdiBmp = new Bitmap( L"file.png" );
HBITMAP hBitmap;
gdiBmp->GetHBITMAP( 0, &hBitmap );
SelectObject( m_hCompatDC, hBitmap );
DeleteObject( hBitmap );
delete gdiBmp;
GdiplusShutdown( gdiplusToken );
デフォルトでWIN32_LEAN_AND_MEANが定義されている処理系では、それを無効にしないと、 gdiplus.hをincludeするときエラーがいっぱい出る。 GDI+も一長一短だが、Windows 2000のデフォルトで動作しない、というのは気になる。 DLLをつければ良いが、PNGを読みたいだけで1MBも肥大化するのは無駄なので、ここでは、普通にlibpngを使ってみる。 libpngなら、ガンマ補正とか、GDI+を超えてPNGの全てを引き出せるし、 何より勉強になる。
libpngもクセのあるライブラリーだ。 たぶん真に創造的な人は、優等生的にきれいなコードを書かないのだろう。 例えば、png_read_png で画像の読み込みを行う前に画像サイズを取得するか、と png_read_info を呼ぶと、 構造体の内部状態が動いてしまって、png_read_png が失敗する、というのは、今どきのこぎれいなプログラマーには信じられない現象だろう。 横幅くらいstaticに(constに)取得させろーと。 情報を取得するときファイルポインターが動いてしまうのだが、最高速度にチューニングされているといえないこともない。 しかしそんなことより、今の人が最も困惑するのは、 ライブラリを呼び出す関数は、setjmpを設定しろ、という点だろう。 最近では、gotoでエラー処理に飛ぶのさえあまり使わない。 setjmpなど使ったことがない人も多いのでは…。
setjmpは普通に使っても大丈夫だし、ライブラリをコンパイルするときに PNG_SETJMP_NOT_SUPPORTED と ABORT を定義して使わない手もあるが、 いずれにしても、それだと標準エラーにエラーメッセージが送られる。 WindowsのGUIプログラムでstderrに文字列など送られても何が起きたのかユーザーは分からないので、 もっと手前でカスタマイズする必要がある。といっても試すと簡単で、こういうハンドラーを書いて、
void user_error_fn( png_structp png_ptr, png_const_charp error_msg )
{
throw (const char *) error_msg;
}
// (const char *) は単なる注釈で意味はない
ここで渡すだけ。
png_ptr = png_create_read_struct( PNG_LIBPNG_VER_STRING,
(png_voidp) NULL,
(png_error_ptr) user_error_fn, // ここ
(png_error_ptr) user_warning_fn
);
そうすると、ライブラリー内でエラーが起きたときに、ジャンプでなくてこのハンドラーが呼ばれる。 ハンドラーは自分で MessageBoxAなどを呼んでエラー表示してもいいし、 ライブラリ関数を最初に呼び出したアプリ側のcatchにエラー文字列の表示を委ねてもいいが、 とにかく制御を戻さずthrowなどで脱出する。 PNGでないファイルを無理やり開かせライブラリからエラーを飛ばさせテストすると、ちゃんと動作する。 これでsetjmpも不要になった。
FILE * fp = NULL; // PNGファイルを読むポインター
BYTE * pBits = NULL; // ビット列を保持するバッファ
HBITMAP hBitmap = NULL; // 変換先
png_structp png_ptr = NULL; // ライブラリのインターフェイスみたいなもの
png_infop info_ptr = NULL;
try
{
fp = ファイルを開く( ファイル名 );
if( NULL == fp ) throw "Can't open file";
png_ptr = png_create_read_struct( PNG_LIBPNG_VER_STRING,
(png_voidp) NULL,
(png_error_ptr) user_error_fn,
(png_error_ptr) user_warning_fn
);
info_ptr = png_create_info_struct( png_ptr );
png_init_io( png_ptr, fp );
png_read_png( png_ptr, info_ptr, 0
| PNG_TRANSFORM_STRIP_16 // If 16-bit/channel, to 8
| PNG_TRANSFORM_PACKING // If 1,2,4-bit, to 8
| PNG_TRANSFORM_EXPAND // If palette, to rgb(a)
| PNG_TRANSFORM_BGR // to LE: byte 'B''G''R'['A']
, NULL );
png_bytepp row_pointers = png_get_rows( png_ptr, info_ptr );
const png_uint_32 width = png_get_image_width( png_ptr, info_ptr ); // 横ピクセル数
const png_uint_32 height = png_get_image_height( png_ptr, info_ptr ); // 縦ピクセル数
const png_uint_32 rowbytes = png_get_rowbytes( png_ptr, info_ptr ); // 1列が何バイトか?
// ここでpBitsを確保し、row_pointersからビット列を詰め替える
BITMAPINFO bi ... ビットマップの内容に合わせて初期化
hBitmap = CreateDIBitmap( hdc, (BITMAPINFOHEADER *) &bi, CBM_INIT,
pBits, &bi, DIB_RGB_COLORS );
}
catch( const char * error_msg )
{
MessageBoxA( NULL, error_msg, "Error", MB_OK | MB_ICONEXCLAMATION );
}
if( pBits ) ...pBitsを解放
if( png_ptr || info_ptr )
{
png_destroy_read_struct( ( NULL != png_ptr )? &png_ptr : NULL,
( NULL != info_ptr )? &info_ptr : NULL,
NULL );
}
if( NULL != fp ) fclose( fp );
PNGを読むには2通りの道があって(ドキュメントで高水準・低水準と呼んでいるが、たいして違いはない)、 上は高水準をWindowsから使った場合のスケルトン例だが、 パレットからRGBへの展開とか簡単にやってくれて、GDI+と比べ面倒ではない。 row_pointersは画像の各列のデータを保持しているポインタの配列で、縦のピクセル数だけ要素がある。 上の順番でライブラリを呼んだとき、row_pointersの解放は、最後のpng_destroy_read_struct呼び出しで、 ライブラリ側がやってくれる。Windows APIなのにfpなのか?というと、 hFileでもできるが、fpはそのままライブラリに渡せる標準の方法だし、PNGは(常識的には)4GBを超えないので問題ない。
ビットの詰め替え。 WindowsのAPIに渡すときは単一のビット列が必要だが、 libpngから返されるのはポインタの配列で、前の列から次の列へアドレスが連続している保証はない。 基本的には、
// 横が4の倍数なら
for( png_uint_32 row = 0; row < height; ++row )
{
memcpy( &pBits[ rowbytes * row ], row_pointers[ row ], rowbytes );
}
RGB24にはWindows上ではアラインメントの問題があるので、正しくは…。 rowbytesは詰め替え元の1列のバイト数、 cbRowは詰め替え先の1列のバイト数として、
const unsigned int nChannels = rowbytes / width;
switch( nChannels )
{
case 1:
iBitCount = 4 * CHAR_BIT; // 32-bit
cbRow = rowbytes * 4;
break;
case 2:
iBitCount = 4 * CHAR_BIT; // 32-bit
cbRow = rowbytes * 2;
break;
case 3:
iBitCount = 3 * CHAR_BIT; // 24-bit
cbRow = ( ( rowbytes + 3 ) & ~3 );
break;
case 4:
iBitCount = 4 * CHAR_BIT; // 32-bit
cbRow = rowbytes;
break;
default:
throw "Unsupported number of channels";
break;
}
pBits = 確保するバイト( cbRow * height );
if( NULL == pBits ) throw "No memory, pBits";
if( 1 == nChannels )
{
// グレースケールの処理。
}
else if( 2 == nChannels )
{
// アルファチャンネルつきグレースケール
}
else // nChannels 3 or 4
{
for( png_uint_32 row = 0; row < height; ++row )
{
memcpy( &pBits[ cbRow * row ], row_pointers[ row ], rowbytes );
}
}
グレースケールの処理は、自分で簡易的にやるには1チャンネル0~255をRGBの(0,0,0)~(255,255,255)に対応させて、 同じバイトを3回コピーして、DWORDの最上位バイト(未使用)は適当に放置。 アルファチャンネルはないのでRGB24に変換するのが論理的だが、 それだとまたアラインメントの問題が出る。
if( 1 == nChannels )
{
BYTE * dst = pBits;
for( png_uint_32 row = 0; row < height; ++row )
{
// 1-byte to 4-byte mapping (number of columns == rowbytes)
for( png_uint_32 column = 0; column < width; ++column, dst += 4 )
{
memset( dst, row_pointers[ row ][ column ], 3 ); // RGB
*( dst + 3 ) = 0; // highest byte of DWORD, unused
}
}
}
else if( 2 == nChannels )
{
BYTE * dst = pBits;
for( png_uint_32 row = 0; row < height; ++row )
{
// 2-byte to 4-byte mapping (number of columns == rowbytes/2)
for( png_uint_32 idx = 0; idx < rowbytes; idx += 2, dst += 4 )
{
memset( dst, row_pointers[ row ][ idx ], 3 ); // RGB
*( dst + 3 ) = row_pointers[ row ][ idx + 1 ]; // A
}
}
}
このように、「高水準」のアプローチを使うと、Windowsでは、ビットの詰め替えのところで、かえって自分でたくさん低水準な処理が必要になる。 PNGで言うところの「低水準」の方法では、これらの処理をライブラリ側に任せて、かえって何でもRGB32で受け取ることができる。 普通の(カラーの)PNGだけなら、どっちでも手間は変わらない。
本当はビットを詰め替えないで、最初からpBitsを確保して「ここに詰めろ」とライブラリに渡すやり方の方が良い(メモリーのコピーの手間が省ける)。 画像の各列がぴったり入るように「各列の頭」のアドレスを計算し、 その先頭アドレスの配列をlibpngに渡すと、呼び出しから戻ったときにはすでにAPIに渡せる姿になっている。 問題は、1列が何バイトなのか、1ピクセルが何バイトなのか?で、上の方法では透過色のあるパレットは4バイト/px、透過色のないパレットは3バイト/pxに変換されるなど、 どれだけ確保したらいいのか事前に分かりにくい(確保自体は多めに取るのでもいいが、ぴっちり詰められないと意味がない)。 この順番でやるときは、この方法でやった方がよく、 こっちから配列を渡すやり方はドキュメントにいう「低水準」のやり方のほうがいい。
pBitsが用意できたらbiまたはbihを用意して、APIにhBitmapを作らせる。
普通のWindows的処理なので省くが、
縦座標がさかさまなのでbiHeightの符号はマイナスとする。
hBitmap = CreateBitmap( width, height, 1, 24 or 32, pBits );
ではいけないのか?というと、ビットマップを選択するhdcとpBitsでビット深度が一致すればそれでも動作する可能性が高いが、
一般には動作しない。
CreateDIBitmap なら、
可能な限り確実に引数のhdcと互換のDDBにしてくれる。
2007年9月23日 2K/XPのフォント非互換
FW_NORMAL (400) と FW_BOLD (700) つまり本来、普通と太字の2種類のグリフがあるフォントで、 XPは、FW_SEMIBOLD (600) というセミボールドを「勝手に」動的に自作する。 また、FW_NORMAL (400) しかないフォントでも、FW_SEMIBOLD (600)を自作する。 だから、太字と普通があるフォントで {\b600} を指定した場合や、 一種類しか太さがないフォントで {\b1} == {\b700} を指定した場合、2KとXPで結果が非互換になる。 前者({\b600})は一般的でないが、後者の問題はよく発生する。 XPが自作した改変書体は、フォントデザイナーがていねいに作ったものでなく、あくまで既存の書体をプログラム的に太字にしているので、 品位的にも注意を要する(実用上まったく問題ないときもあれば、そうでないときもある)。