boost::gil+libpngでタイル画像への結合ツール作ってみた

ということで、早速boost::gilを試してみた。
手元のboost v1.55.0 には普通に含まれているようだ。

makefile

ここは特筆すべきこと無し。

CC		= clang
LIB_TOP	= C:\maminus\lib
BO_TOP	= $(LIB_TOP)\boost_1_55_0
INC		=							\
	-I $(BO_TOP)							\
	-I .								\
	-I $(LIB_TOP)\libpng						\
	-I $(LIB_TOP)\zlib\include					\

LIBS	=								\
	$(LIB_TOP)\libpng\libpng16-0.dll				\
	$(LIB_TOP)\zlib\zlib1.dll					\
	-lstdc++							\

CFLAGS	= -Wall -W -std=c++11 -Wno-c++11-narrowing
LFLAGS	= -static
SRCS	= 					\
	main.cpp				\

OBJS	= $(SRCS:.cpp=.o)
TARGET	= tiled-png.exe
RM		= del /f /q


.PHONY: all debug clean delete_objs delete_target

all: CFLAGS += -O2
all: $(TARGET)

debug: CFLAGS += -g
debug: LFLAGS += -g
debug: $(TARGET)

$(TARGET): $(OBJS)
	$(CC) -o $@ $(OBJS) $(LFLAGS) $(LIBS)

clean: delete_objs delete_target

delete_objs:
	$(RM) $(OBJS)

delete_target:
	$(RM) $(TARGET)


.SUFFIXES: .cpp .o

.cpp.o:
	$(CC) -c $(INC) $(CFLAGS) -o $*.o $<

ツールのソース

ここもコメントに書いた通りなので書くことが無い。
注意点はint_p_NULLの定義がlibpng側から消滅してるって箇所くらい。
boost::gilの使い方も参考URL側がとてもわかりやすい。

//-----------------------------------------------------------------------------
#include <boost/format.hpp>
#include <boost/lexical_cast.hpp>
namespace
{		// コマンドライン引数系
const int	INVALID_ARG_VAL	= -1;
int		column_count	= INVALID_ARG_VAL;	// タイル画像の列数
int		row_count	= INVALID_ARG_VAL;	// 行数
int		left		= INVALID_ARG_VAL;	// 切り出し矩形の左上 x 座標
int		top		= INVALID_ARG_VAL;	// 同 y 座標
int		right		= INVALID_ARG_VAL;	// 同左下の x 座標
int		bottom		= INVALID_ARG_VAL;	// 同 y 座標

// コマンドライン引数をグローバル変数へ展開する
//	戻り値:
//		0    :正常
//		0以外:異常
int parse_args(int argc, char *argv[])
{
	if(argc < 7){
		return -1;
	}

	int			i;
	try{
		int		*arg_tbl[] = {NULL, &column_count, &row_count, &left, &top, &right, &bottom};
		for(i=1;i<=6;i++){		// 前から順番に整数として読み出す
			*arg_tbl[i] = boost::lexical_cast<int>(argv[i]);
		}
	}catch(const std::exception &e){	// 読み出せない形式ならここを通る
		std::cerr << str(boost::format("argv[%d]: %s is not int? -> %s") % i % argv[i] % e.what()) << std::endl;
		return -2;
	}

	// 引数の整合性チェック
	if((left < 0) || (left >= right)){
		std::cerr << str(boost::format("left must >= 0 and < right. but left:%d right:%d") % left % right) << std::endl;
		return -4;
	}
	if((top  < 0) || (top >= bottom)){
		std::cerr << str(boost::format("top must >= 0 and < bottom. but top:%d bottom:%d") % top % bottom) << std::endl;
		return -4;
	}

	// ファイル数のチェック
	int		file_count = argc - 7;
	if((file_count == 0) || (file_count % (column_count * row_count) != 0)){
		std::cerr << str(boost::format("file list count %d is not a multiple of %d (%d x %d)") % file_count % (column_count * row_count) % column_count % row_count) << std::endl;
		return -3;
	}

	return 0;
}
void usage(const char *argv0)
{
	std::cout << str(boost::format("Usage:\n\t>%s column_count row_count left top right bottom files...") % argv0) << std::endl;
}
}
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
//http://stackoverflow.com/questions/2442335/libpng-boostgil-png-infopp-null-not-found
//	ここによるとlibpng 1.4以降以下の定義がなくなってしまったとのこと
//	コンパイルエラーになるので自前で定義しておく
#define png_infopp_NULL (png_infopp)NULL
#define int_p_NULL (int*)NULL
#include <boost/gil/gil_all.hpp>
#include <boost/gil/extension/io/png_io.hpp>
namespace
{		// PNGファイル処理系

//タイル画像を生成する
//	参考URL: http://d.hatena.ne.jp/tsurushuu/20080723/1216783641
void generate_tiled_image(int number, char *files[])
{
	int		width	= right - left;
	int		height	= bottom - top;

	// 出力先を用意する (横幅×列数、縦幅×行数)
	boost::gil::rgba8_image_t	dst(width * column_count, height * row_count);

	for(int y=0;y<row_count;y++){			// 縦方向のループ(上から下に向けて画像を結合していく)
		for(int x=0;x<column_count;x++){	// 横方向のループ(左から右に向けて画像を結合していく)
			// 元画像をロードする
			boost::gil::rgba8_image_t src;
			boost::gil::png_read_image(files[y*column_count + x], src);

			// 元画像から指定範囲を切り出して出力先にコピーする
			boost::gil::copy_pixels(
				boost::gil::subimage_view(boost::gil::view(src), left,    top,      width, height),
				boost::gil::subimage_view(boost::gil::view(dst), x*width, y*height, width, height)
				);
		}
	}

	// 0.png, 1.png, 2.png, ... の名前で画像をファイルに保存する
	boost::gil::png_write_view(str(boost::format("%d.png") % number), boost::gil::view(dst));
}
}
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
int main(int argc, char *argv[])
{
	if(parse_args(argc, argv) != 0){	// コマンドライン引数をグローバル変数へ展開する
		usage(argv[0]);			// 引数がおかしい場合はUsage表示して終了する
		return -1;
	}

	int j = 0;
	for(int i=7;i<argc;i+=(column_count * row_count)){		// ファイル1セット分(列数×行数)ずつ
		try{
			generate_tiled_image(j++, argv + i);		// タイル画像に変換する
		}catch(const std::exception &e){
			std::cerr << e.what() << std::endl;
		}
	}

	return 0;
}
//-----------------------------------------------------------------------------

参考URL:
http://d.hatena.ne.jp/tsurushuu/20080723/1216783641
http://stackoverflow.com/questions/2442335/libpng-boostgil-png-infopp-null-not-found


今回はRGBAのpng画像を使うのでrgba8_image_tを使用したが、RGBのpngだとrgb8_image_tになる。
というか、最初rgb8_image_tを使っていたら以下のようなエラーになってしまった。

png_read_view: input view type is incompatible with the image type

boost::gil側をgrepしたら画像の種類が一致してないときに出るエラーだったので、ソースと画像側が一致してないとダメらしい。

ツール実行

で、このツールは以下のように呼び出す。

C:\maminus>tiled-png.exe 2 3 20 20 30 40 src1.png src2.png src3.png src4.png src5.png src6.png

動作イメージはこんな感じ。例だと6つの画像から矩形領域を切り取って2x3のタイル画像にしている。矩形領域がちょうど番号部分になっているので結果の画像は番号だけの画像になっている。

ツールのコマンドライン引数はこんな感じ。タイルの列数、行数、矩形領域のピクセル位置を指定する。

VBScript

で、毎回切り出し範囲などを指定するのは面倒なのでスクリプト化してみた。
ツールのexeとこのスクリプトは同じフォルダに入れる。
そして、このスクリプトファイルへのショートカットを「送る」に登録すれば画像ファイルを「送る」だけになるはず。
(毎回同じ範囲の画像を結合する前提になるけど)

' 引数は画像ファイルだけ指定してもらう
' (ショートカットを「送る」に入れてもらうような用途を想定)

Option Explicit

Dim objPrm
Set objPrm = Wscript.Arguments		' コマンドライン引数

' 引数チェック
If objPrm.Count Mod 6 <> 0 Then
	MsgBox "ファイルを6個ずつ指定してください。"
	WScript.Quit
End If


Dim objFs
Set objFs = CreateObject("Scripting.FileSystemObject")
Dim objShell
Set objShell = WScript.CreateObject("WScript.Shell")

' スクリプトのフォルダをカレントディレクトリに設定する
'	http://d.hatena.ne.jp/ku__ra__ge/20111110/p2
objShell.CurrentDirectory = objFs.GetFile(WScript.ScriptFullName).ParentFolder.Path


Const WindowHide = 0	' ウィンドウを非表示
Dim i
Dim target				' 出力ファイル名
Dim command				' コマンド実行用

' コマンドライン文字列を組み立てていく
' 固定パラメータでツールを実行する
' パラメータを頻繁に変更する場合は Const 変数とかに分離したほうが楽そう
command = "tiled-png.exe 2 3 330 95 790 465"
For i=0 to objPrm.Count / 6 - 1
	command = command&" "& objPrm(i*6+0)&" "& objPrm(i*6+1)&" "& objPrm(i*6+2)&" "& objPrm(i*6+3)&" "& objPrm(i*6+4)&" "& objPrm(i*6+5)
Next

' コマンド実行する
objShell.Run command, WindowHide

For i=0 to objPrm.Count / 6 - 1
	' 入力ウィンドウを表示してユーザさんに正式ファイル名を入力してもらう
	' デフォルト名にそれっぽい初期値を入れておく
	target = InputBox("ファイル名を入れてください", "正式ファイル名入力", "xxx_第"&(i+1)&"画像.png")

	' ファイル名を変更する
	objFs.MoveFile (i)&".png", target
Next

Set objShell = Nothing
Set objFs = Nothing
Set objPrm = Nothing

参考URL:WSHで、強制的にカレントディレクトリを設定する方法 - くらげのChangeLog


これで画像ファイルを作れば後はまとめて「送る」だけで結合ファイルができるようになる。
ただ、1点大問題がある。
「送る」でたくさんのファイルを指定すると、どうもファイルの順番がぐちゃぐちゃになるように見える。
VB側でソートするのは面倒なのでC++側でソートするくらいしかないかなと思う。
結合前のファイルは名前順になるように例えば連番とかにしておけば良いかと。

余談

と、ここまで書いて「でもこの程度ならスクリプトに対応したフォトレタッチソフト使えば同じことできるよね」と思った。
例えばGIMPってスクリプトに対応していたような気がする。
まぁ、「送る」で処理できるか、とか処理速度というかソフトの起動時間はどうか、とかいろいろあるかもしれないけど今回やったことも結局は車輪の再発明くさいねぇ。

とりあえず今回はC++(clang with Windows)から簡単にPNGを読み書きできる手段を手に入れたってことで。
今まではずっとBMPファイルでやってきたから個人的にうれしい進歩だね。
出力はサイズを気にしなければBMPファイルでも良いけど、特に入力ファイルに制約が出るのは困るしね。
いや、JPEGの導入方法調べてないじゃん、って言われると困るけど・・・
(boost::gil自体はJPEG対応してるっぽいけど、libjpegが必要っぽい。とはいえ、バイナリが配布されている気配*1がするのでzlib同様DLLとヘッダファイルを展開してmakefileでパス設定したら終わりのような気もする)