Rustは実行効率や安全性を重視した人気のプログラミング言語ですが、難しいと言われることもあります。本連載ではいろいろな有名アルゴリズムを解くことでRustに慣れていきます。今回は、画像ファイルの生成方法について見てみましょう。
画像ファイルとは何か
そもそも、画像ファイルとは何でしょうか。コンピューターのモニターは小さな点(ピクセル)の集まりです。それで、画像ファイルを表現する場合は、ピクセルの並びをそのままデータとして表現した「ビットマップ画像(ラスタ画像)」と、図形をどのように描画するのか座標情報で表現する「ベクタ画像」の二種類が代表的です。今回は、Rustで単純にピクセル情報をファイルに保存すれば良いビットマップ画像を作成してみましょう。
ビットマップ画像ファイルの構造
ビットマップ画像について簡単に分かったところで、実際のファイル構造について見ていきましょう。基本的には、画像の各ピクセルの色番号を左上から右下に並べたデータに加えて、画像情報を表すヘッダ情報のデータをくっつけたものです。ヘッダ情報はビットマップファイルであることを示すファイルヘッダと、画像に関する情報をまとめた情報ヘッダから構成されます。次の図のような構造となっています。
画像の各ピクセルは、光の三原色である赤緑青(RGB)の色の割合を指定したものとなっています。例えば、24ビットTrue Colorでは赤緑青をそれぞれ8ビット(0-255の範囲)で指定して色を表現します。なお、この時、青・緑・赤の順番で指定します。
なお、Webでよく見かける、PNGやJPEG画像は画像データを圧縮したものですが、基本的にビットマップ画像は圧縮が不要な単純な画像データです。
ビットマップの具体的な構造
それでは、より具体的にファイル構造を確認してみましょう。ビットマップファイルのヘッダ情報は、次のような構造となっています。
実際の画像データに加えて、上記の表の通りのバイト数でファイルに書き込めばビットマップ画像ファイルができあがります。
バイトオーダーについて
なお、ビットマップのヘッダを書き込む際、数値はすべて「リトルエンディアン」にて書き込む必要があります。これは数値データをメモリに書き込む際に、どのような順番で書き込むかの方式です。「ビッグエンディアン」と「リトルエンディアン」があり、「バイトオーダー(エンディアン)」と呼びます。
例えば、32ビット整数の0x12345678を書き込む際に、ビッグエンディアンであれば、[0x12, 0x34, 0x56, 0x78]のようにしますが、リトルエンディアンでは、[0x78, 0x56, 0x34, 0x12]のように書き込みます。
画像ファイルを作成してみよう
以上、ここまでの知識があれば、ビットマップ画像をファイルに保存することができるでしょう。それでは、Rustでビットマップ画像を書き出すプログラムを作ってみましょう。
なお、今回からRustのプロジェクト管理ツールである、Cargoを使ってみましょう。プロジェクトを作成するには、コマンドラインで次のようなコマンドを実行します。
cargo init
そして、src/main.rsを次のように書き換えましょう。こちらからもコピーできます。
use std::io::Write;
fn main() { // メイン処理 --- (*1)
let data = make_bitmap(8, 8); // 画像を作成
let mut f = std::fs::File::create("test.bmp").unwrap(); // ファイル生成
f.write(&data).unwrap(); // ファイルに書き出す
println!("画像を書き出しました。");
}
// ビットマップ画像データを作成する --- (*2)
fn make_bitmap(width: u32, height: u32) -> Vec<u8> {
let mut data = vec![];
let image_size = width * height * 3; // サイズ計算 --- (*3)
let filesize = 14 + 40 + image_size; //
// ファイルヘッダを書き込む --- (*4)
data.push('B' as u8); data.push('M' as u8);
write_u32(&mut data, filesize); // ファイルサイズ
write_u32(&mut data, 0); // 0
write_u32(&mut data, 0x36); // 先頭から画像データまでのオフセット
// 情報ヘッダを書き込む --- (*5)
write_u32(&mut data, 40); // ヘッダサイズ
write_u32(&mut data, width); // 幅
write_u32(&mut data, height); // 高さ
write_u16(&mut data, 1); // 1
write_u16(&mut data, 24); // 色ビット数
write_u32(&mut data, 0); // 圧縮形式
write_u32(&mut data, image_size); // 画像データサイズ
write_u32(&mut data, 0); // 水平解像度(0も可/96dpi=3780)
write_u32(&mut data, 0); // 垂直解像度(0も可/96dpi=3780)
write_u32(&mut data, 0); // パレット数
write_u32(&mut data, 0); // 重要色数(0でも良い)
// 画像データを書き込む --- (*6)
for y in 0..height {
for x in 0..width {
let color = if (x+y) % 2 == 0 { 0xFF0000 } else { 0xFFFFFF };
write_rgb(&mut data, color);
}
}
data
}
fn write_u16(data: &mut Vec<u8>, v: u16) { // u16を書き込む --- (*7)
for i in v.to_le_bytes() { data.push(i); }
}
fn write_u32(data: &mut Vec<u8>, v: u32) { // u32を書き込む
for i in v.to_le_bytes() { data.push(i); }
}
fn write_rgb(data: &mut Vec<u8>, rgb: u32) { // RGBを書き込む --- (*8)
data.push((rgb >> 0 & 0xFF) as u8); // Blue
data.push((rgb >> 8 & 0xFF) as u8); // Green
data.push((rgb >> 16 & 0xFF) as u8); // Red
}
書き換えたら、次のコマンドを実行します。これにより、コンパイルが行われてプログラムが実行されます。
cargo run
すると、8x8のサイズの「test.bmp」という画像ファイル(ビットマップ画像)が生成されます。せっかくなので、市松模様を生成するようにしてみました。
それでは、プログラムを確認してみましょう。プログラムは大きく分けて、二つの部分に分けられます。メモリ内に画像を生成する部分(関数make_bitmap)と、メモリ内のデータをファイルに保存する部分(関数main)です。