D言語で作る画像ビューア

# はじめに

**D言語**では、標準ライブラリ[Phobos](https://dlang.org/phobos/index.html)の他に、[DUB](https://code.dlang.org/)パッケージで提供されるライブラリを利用可能です。
活用することで、開発できるアプリケーションの幅が広がります。

今回は、`DUB`パッケージで提供されている`SDL`、`FreeImage`を使った実装例を示したいと思います。

# SDLとは
[SDL](https://www.libsdl.org/)は、クロスプラットフォームのマルチメディアライブラリです。グラフィックの描画やサウンドの再生などのAPIを提供しています。Windows、macOS、Linux、iOS、Androidを公式にサポートします。 （[Wikipedia](https://ja.wikipedia.org/wiki/SDL)より）
インタフェース部はPerl、Python、Ruby、Javaなどのプログラミング言語にも移植されているそうですが、D言語でも[bindbc-sdl](https://github.com/BindBC/bindbc-sdl)として、利用可能です。

# FreeImageとは
[FreeImage](https://freeimage.sourceforge.io/)は、一般的なグラフィック画像形式をサポートしたい開発者向けのオープンソースライブラリプロジェクトです。
`PNG`、`JPEG`、`WebP`、`GIF`、`BMP`、`JPEG 2000`、`TIFF`など、様々な画像形式に対応しています。
>
 BMP files [reading, writing]
 Dr. Halo CUT files [reading] *
 DDS files [reading]
 EXR files [reading, writing]
 Raw Fax G3 files [reading]
 GIF files [reading, writing]
 HDR files [reading, writing]
 ICO files [reading, writing]
 IFF files [reading]
 JBIG files [reading, writing] **
 JNG files [reading, writing]
 JPEG/JIF files [reading, writing]
 JPEG-2000 File Format [reading, writing]
 JPEG-2000 codestream [reading, writing]
 JPEG-XR files [reading, writing]
 KOALA files [reading]
 Kodak PhotoCD files [reading]
 MNG files [reading]
 PCX files [reading]
 PBM/PGM/PPM files [reading, writing]
 PFM files [reading, writing]
 PNG files [reading, writing]
 Macintosh PICT files [reading]
 Photoshop PSD files [reading]
 RAW camera files [reading]
 Sun RAS files [reading]
 SGI files [reading]
 TARGA files [reading, writing]
 TIFF files [reading, writing]
 WBMP files [reading, writing]
 WebP files [reading, writing]
 XBM files [reading]
 XPM files [reading, writing]
* only grayscale
** only via external plugin, might require a commercial license

D言語では、[bindbc-freeimage](https://github.com/BindBC/bindbc-freeimage)として、利用可能です。

# ソースコード

私の場合、開発用フォルダ`D:\Dev`に`FIViewer`というフォルダ、さらにその下に`source`フォルダを作成します。
`FIViewer`フォルダに`dub.json`、`FIViewer\source`フォルダに`app.d`を配置します。
`app.d`は、空行、コメント行を除くと、98行で実装できました。

```d:app.d
import std.conv;
import std.stdio;
import std.string;
import std.windows.charset;

import bindbc.freeimage;
import bindbc.sdl;

int main(string[] args)
{
	// Argument check
	if ( args.length < 2 ){
		writeln("Argument required : filename");
		return ( 1 );
	}
	string filename = to!string(toMBSz(args[1]));
	
	// Load SDL
	SDLSupport ret = loadSDL();
	if ( ret != sdlSupport ){
		if ( ret == SDLSupport.noLibrary ){
			writeln("SDLSupport.noLibrary");
		} else if ( SDLSupport.badLibrary ){
			writeln("SDLSupport.badLibrary");
		}
		return ( 1 );
	}
	scope(exit) unloadSDL();
	
	// Initialize SDL
	if ( SDL_Init(SDL_INIT_VIDEO) < 0 ){
		writefln("SDL could not initialize! SDL_Error: %s", SDL_GetError());
		return ( 1 );
	}
	scope(exit) SDL_Quit();
	
	// Load FreeImage
	FISupport fis = loadFreeImage();
	if ( fis != fiSupport ){
		if ( fis == FISupport.noLibrary ){
			writeln("FISupport.noLibrary");
		} else if ( FISupport.badLibrary ){
			writeln("FISupport.badLibrary");
		}
		return ( 1 );
	}
	
	// Load Image
	FREE_IMAGE_FORMAT fif = FreeImage_GetFileType(filename.toStringz, 0);
	FIBITMAP *dib = FreeImage_Load(fif, filename.toStringz, 0);
	scope(exit) FreeImage_Unload(dib);
	
	FIMEMORY* hmem = FreeImage_OpenMemory();
	FreeImage_SaveToMemory(FIF_BMP, dib, hmem, 0);
	scope(exit) FreeImage_CloseMemory(hmem);
	ubyte* mem;
	uint bufsize;
	FreeImage_AcquireMemory(hmem, &mem, &bufsize);
	
	int orgw = FreeImage_GetWidth(dib);
	int orgh = FreeImage_GetHeight(dib);
	
	// Create window
	SDL_DisplayMode dm;
	SDL_GetCurrentDisplayMode(0, &dm);
	int dispw = (orgw < dm.w - 50) ? orgw : dm.w - 50;
	int disph = (orgh < dm.h - 50) ? orgh : dm.h - 50;
	if ( (cast(double)dispw) / orgw > (cast(double)disph) / orgh ){
		dispw = orgw * disph / orgh;
	} else {
		disph = orgh * dispw / orgw;
	}
	
	SDL_Window* window = SDL_CreateWindow("FreeImage Viewer",
		SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, dispw, disph,
		SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE);
	if ( window == null ){
		writefln("Window could not be created! SDL_Error: %s", SDL_GetError());
		return ( 1 );
	}
	scope(exit) SDL_DestroyWindow(window);
	
	SDL_Surface* image0 = SDL_LoadBMP_RW(SDL_RWFromMem(mem, bufsize), 1);
	if ( !image0 ){
		writefln("SDL_LoadBMP_RW: %s", IMG_GetError());
		return ( 1 );
	}
	SDL_Rect imageRect = SDL_Rect(0, 0, orgw, orgh);
	SDL_Rect drawRect  = SDL_Rect(0, 0, dispw, disph);
	SDL_Surface* image = SDL_CreateRGBSurface(0, orgw, orgh, 32, 0x000000ff,  0x0000ff00,  0x00ff0000, 0xff000000);
	scope(exit) SDL_FreeSurface(image);
	SDL_BlitSurface(image0, &imageRect, image, &imageRect);
	SDL_FreeSurface(image0);
	// Show Image
	eventloop : while ( true ){
		SDL_Event e;
		while ( SDL_PollEvent(&e) ){
			if ( e.type == SDL_QUIT ){
				break eventloop;
			}
			SDL_GetWindowSize(window, &dispw, &disph);
			if ( (cast(double)dispw) / orgw > (cast(double)disph) / orgh ){
				drawRect = SDL_Rect(0, 0, orgw * disph / orgh, disph);
			} else {
				drawRect = SDL_Rect(0, 0, dispw, orgh * dispw / orgw);
			}
			SDL_Surface* screen = SDL_GetWindowSurface(window);
			SDL_FillRect(screen, null, SDL_MapRGB(screen.format, 200, 200, 200));
			SDL_LowerBlitScaled(image, &imageRect, screen, &drawRect);
			SDL_UpdateWindowSurface(window);
		}
		SDL_Delay(100);
	}
	return ( 0 );
}
```
```json:dub.json
{
	"name": "fiviewer",
	"description": "FreeImage Viewer",
	"license": "BSL-1.0",
	"dependencies": {
		"bindbc-freeimage": ">=0.3.0",
		"bindbc-sdl": ">=0.11.0",
	},
	"versions": ["FI_318","BindSDL_Image"]
}
```

# ソースコード補足説明
`SDL_`から始まる関数は`SDL(SDL2.dll)`が提供する関数です。詳細は、[SDL 2.0日本語リファレンスマニュアル](http://sdl2referencejp.osdn.jp/index.html)を参照するとよいかと思います。
`FreeImage_`から始まる関数は`FreeImage(FreeImage.dll)`が提供する関数です。詳細は、[Documentation](https://freeimage.sourceforge.io/documentation.html)を参照するとよいかと思います。

大まかな処理の流れとして、

* 引数があるかをチェック。引数が表示したい画像ファイル名となります。
* `SDL(SDL2.dll)`を使うために`loadSDL`、`FreeImage(FreeImage.dll)`を使うために`loadFreeImage`を先に呼び出す必要があります。
* `FreeImage_`の関数を使って、ファイルの画像情報をメモリ上に保存。
* 画像ファイルと画面サイズを比較。画像が大きい場合、画面に収まるように表示サイズを計算します。（`dispw`、`disph`）
* `SDL_`の関数でウィンドウを作成し、画像を画面に表示。
* ウィンドウを閉じるまで、`eventloop`でループします。ウィンドウサイズを変更すると、画像表示サイズを再計算します。

# コンパイル、実行
```text:
D:\Dev> cd D:\Dev\FIViewer

D:\Dev\FIViewer> dmd --version
DMD32 D Compiler v2.094.0-dirty
Copyright (C) 1999-2020 by The D Language Foundation, All Rights Reserved written by Walter Bright

D:\Dev\FIViewer> dub build --build=release --arch=x86_64
Performing "release" build using D:\dmd2\windows\bin\dmd.exe for x86_64.
bindbc-loader 0.3.2: target for configuration "noBC" is up to date.
bindbc-freeimage 0.5.0: target for configuration "dynamic" is up to date.
bindbc-sdl 0.19.1: target for configuration "dynamic" is up to date.
fiviewer ~master: building configuration "application"...
Linking...
To force a rebuild of up-to-date targets, run again with --force.

D:\Dev\FIViewer> tree
フォルダー パスの一覧:  ボリューム ****
ボリューム シリアル番号は ****-**** です
D:.
├─.dub
│  └─build
│      └─application-release-windows-x86_64-dmd_2094-6074A52ED719B4D88AA30E8597869525
└─source
```

コンパイルの結果、`FIViewer`フォルダに `.dub`フォルダと`dub.selections.json`、`fiviewer.exe`ファイルが生成されます。

`FIViewer`フォルダに、`SDL2.dll`、`FreeImage.dll`を配置します。
`SDL2.dll`は、[ダウンロードページ](https://www.libsdl.org/download-2.0.php)にある`SDL2-2.0.12-win32-x64.zip`から入手します。
`FreeImage.dll`は、[ダウンロードページ](https://freeimage.sourceforge.io/download.html)にある`Download FreeImage 3.18.0 [WIN32/WIN64]`から入手します。


```text:
D:\Dev\FIViewer> dir
 ドライブ D のボリューム ラベルは **** です
 ボリューム シリアル番号は ****-**** です

 D:\Dev\FIViewer のディレクトリ

2020/10/24  17:49    <DIR>          .
2020/10/24  17:49    <DIR>          ..
2020/10/24  16:51    <DIR>          .dub
2020/10/24  16:51               208 dub.json
2020/10/24  16:51               125 dub.selections.json
2020/10/24  17:49           662,528 fiviewer.exe
2020/10/24  17:18         6,942,208 FreeImage.dll
2020/10/24  17:11         1,471,488 SDL2.dll
2020/10/24  13:58    <DIR>          source

D:\Dev\FIViewer> fiviewer.exe D:\Dev\dlangman.webp
```

[こちら](https://qiita.com/__pandaman64__/items/da67cfbb809a141d91e2)から、D言語くんの[webp形式の画像](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.amazonaws.com%2F0%2F11714%2F4cce563c-c3ed-e3ea-36da-240bfe313158.png?ixlib=rb-1.2.2&auto=format&gif-q=60&q=75&s=cc132cf8495852341b2ab8c8c75fe42a)を入手させていただきました。
`fiviewer.exe "ファイル名"`で、指定したファイルを画像表示できます。

![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/675901/908a71d4-3b95-9cfd-0dea-065794ea22df.png)

# 参考情報
[SDL](https://www.libsdl.org/)
[FreeImage](https://freeimage.sourceforge.io/)
[bindbc-sdl](https://github.com/BindBC/bindbc-sdl)
[bindbc-freeimage](https://github.com/BindBC/bindbc-freeimage)
[D言語でビルドツールDUBを用いて便利なライブラリをより簡単に利用する](https://qiita.com/yasei_no_otoko/items/2724eebab10f5cd0a02f)

# 更新履歴

* 2020.10.24 初回投稿
* 2021.3.8 `app.d` 修正
 * `SDL_LowerBlitScaled`が8ビットカラーで動作しないことへの対応（`SDL_CreateRGBSurface`処理を追加）