Rust で Windows プログラミング - 簡易画像ビューア編

おはこんばんちは。この記事は [Rust 2 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/rust2) の3日目の記事です。Rust よりも Win32API の内容がメインです。すみません。

## 環境
toolchain: stable-x86_64-pc-windows-msvc
rustc 1.48.0 (7eac88abb 2020-11-16)
[winapi 0.3.9](https://crates.io/crates/winapi)

## 画像ビューアを作ろう
### こんなものが出来ます
「Open」ボタンを押すとファイルピッカーダイアログが表示されます。画像ファイルを選択すると 640px x 480px 以下にリサイズされた画像が表示されるというもの。簡単のためにウィンドウのリサイズはできない仕様になっています。（写真は若桜鉄道の[隼駅](https://ja.wikipedia.org/wiki/%E9%9A%BC%E9%A7%85)です）
![2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353186/f7c3d523-7285-61a3-994d-32e183cebbaf.jpeg)


### CreateWindow
まずウィンドウを作ります。これは以前[投稿した記事]()で説明しているので詳細は割愛します。

```rust
static mut BUF: Vec<u8> = Vec::new();

fn main() -> Result<()> {
    unsafe {
        let class_name = l("pinion_window_class");
        let wnd_class = WNDCLASSW {
            style: 0,
            lpfnWndProc: Some(window_proc),
            cbClsExtra: 0,
            cbWndExtra: 0,
            hInstance: ptr::null_mut(),
            hIcon: LoadIconW(ptr::null_mut(), IDI_APPLICATION),
            hCursor: LoadCursorW(ptr::null_mut(), IDI_APPLICATION),
            hbrBackground: GetSysColorBrush(COLOR_MENUBAR),
            lpszMenuName: ptr::null_mut(),
            lpszClassName: class_name.as_ptr(),
        };
        RegisterClassW(&wnd_class);

        let title = format!("{} v{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
        H_WINDOW = CreateWindowExW(
            0,
            class_name.as_ptr(),
            l(&title).as_ptr(),
            WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_VISIBLE,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            656,
            551,
            ptr::null_mut(),
            ptr::null_mut(),
            ptr::null_mut(),
            ptr::null_mut(),
        );
        ensure!(!H_WINDOW.is_null(), "CreateWindowExW failed.");

        // 無圧縮 Windows ビットマップデータを保存する領域を確保しておく
        // 640px x 480px x (RGB の 3 bytes) = 921,600 bytes
        BUF.reserve(640 * 480 * 3);

        ShowWindow(H_WINDOW, SW_SHOW);
        UpdateWindow(H_WINDOW);
        let mut msg = init::<MSG>();
        loop {
            if GetMessageW(&mut msg, ptr::null_mut(), 0, 0) == 0 {
                break;
            }
            TranslateMessage(&msg);
            DispatchMessageW(&msg);
        }
    }
    Ok(())
}

// NULL 終端文字を追加した UTF-16 文字列を返す
fn l(source: &str) -> Vec<u16> {
    source.encode_utf16().chain(Some(0)).collect()
}
```

### ウィンドウプロシージャ
```rust
// ビットマップデータの長さを保持しておくグローバル変数
static mut DATA_LEN: usize = 0;

unsafe extern "system" fn window_proc(
    h_wnd: HWND,
    msg: UINT,
    w_param: WPARAM,
    l_param: LPARAM,
) -> LRESULT {
    match msg {
        // CreateWindow 関数が呼ばれたときに送られるメッセージ
        // ここで「Open」ボタンを作る
        WM_CREATE => create(h_wnd),

        // メニューとかボタンが押されたときに送られるメッセージ
        // 「Open」ボタンが押されたらファイルピッカーを開いて
        // 選択された画像をビットマップデータとしてメモリ上に保持する
        WM_COMMAND => command(h_wnd, w_param),

        // ウィンドウの描画が要求されたときに送られるメッセージ
        // ここでメモリ上のビットマップデータをウィンドウに表示する
        WM_PAINT => {
            if DATA_LEN > 0 {
                paint(h_wnd)
            } else {
                return DefWindowProcW(h_wnd, msg, w_param, l_param);
            }
        }

        // ウィンドウが破棄されるときに送られるメッセージ
        // 諸々の終了処理
        WM_DESTROY => {
            DeleteObject(H_FONT as *mut c_void);
            PostQuitMessage(0);
            Ok(())
        }
        _ => return DefWindowProcW(h_wnd, msg, w_param, l_param),
    }
    .map_err(msg_box)
    .ok();
    0
}
```

### ファイルピッカーダイアログ
ファイル選択ダイアログを表示するには[`GetOpenFileName`](https://docs.microsoft.com/en-us/windows/win32/api/commdlg/nf-commdlg-getopenfilenamew)関数を使用します。引数には[`OPENFILENAME`](https://docs.microsoft.com/en-us/windows/win32/api/commdlg/ns-commdlg-openfilenamew)構造体へのポインタを渡します。

```rust
unsafe fn open_dialog(h_wnd: HWND) -> Result<String> {
    const MAX_PATH: u32 = 260;
    let mut buf = [0u16; MAX_PATH as usize];

    // 最後の \0 が必要なことに注意
    let filter = l("Image file\0*.jpg;*.png;*.gif;*.bmp\0");
    let title = l("Choose a image file");

    // zeroed 関数で構造体を 0 で初期化
    // 必要最低限のメンバだけ設定する
    let mut ofn = zeroed::<OPENFILENAMEW>();
    ofn.lStructSize = mem::size_of::<OPENFILENAMEW>() as u32;
    ofn.lpstrFilter = filter.as_ptr();
    ofn.lpstrTitle = title.as_ptr();
    ofn.lpstrFile = buf.as_mut_ptr();
    ofn.nMaxFile = MAX_PATH;
    ofn.Flags = OFN_FILEMUSTEXIST;
    ofn.hwndOwner = h_wnd;

    ensure!(GetOpenFileNameW(&mut ofn) != 0, "Cannot get file path.");

    // Windows の世界の UTF-16 を Rust の世界の UTF-8 に変換
    let slice = slice::from_raw_parts(ofn.lpstrFile, MAX_PATH as usize);
    Ok(decode(slice))
}

unsafe fn zeroed<T>() -> T {
    mem::MaybeUninit::<T>::zeroed().assume_init()
}

fn decode(source: &[u16]) -> String {
    decode_utf16(source.iter().take_while(|&n| n != &0).cloned())
        .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))
        .collect()
}
```

### 画像ファイルの読み込み
[image](https://crates.io/crates/image) crate で画像ファイルを読み込んで 640px x 480px 以下にリサイズします。メモリ上のバッファに無圧縮 Windows ビットマップデータとして保存します。各ピクセルの情報を BGR の順番で表すことに注意です（ RGB ではない）。

あとビットマップデータは水平方向のバイト数が 4 の倍数である必要があります。4 の倍数になるように`0x00`のパディングを挿入します。例えば画像の横幅が 619px だった場合、水平方向のバイト数は 619px x BGR の 3bytes = 1857bytes となりますが、これは 4 の倍数ではありません。後ろに`0x00`を 3bytes 追加して 1860bytes にします。

```rust
static mut WIDTH: i32 = 0;
static mut HEIGHT: i32 = 0;

unsafe fn read_image(file_path: &str) -> Result<()> {
    let img = image::open(file_path)?;

    // 640px x 480px より大きければリサイズする
    let img = if img.width() > 640 || img.height() > 480 {
        let new_size = if img.width() as f32 / img.height() as f32 > 1.333 {
            640
        } else {
            if img.width() > img.height() {
                (480.0 / img.height() as f32 * img.width() as f32) as u32
            } else {
                480
            }
        };
        img.resize(new_size, new_size, imageops::Lanczos3)
    } else {
        img
    };
    // リサイズ後の幅、高さをグローバル変数に保存
    WIDTH = img.width() as i32;
    HEIGHT = img.height() as i32;

    // BGR としてデータ取り出す
    let bgr = img.into_bgr();
    ensure!(bgr.len() <= 640 * 480 * 3, "Invalid data length.");

    // 水平方向のバイト数が 4 の倍数でなければパディングを挿入し
    // BUF にビットマップデータをコピーする
    let remain = (3 * WIDTH as usize) % 4;
    if remain > 0 {
        let chunk_size = 3 * WIDTH as usize;
        let line_bytes_len = chunk_size + 4 - remain;
        DATA_LEN = line_bytes_len * HEIGHT as usize;
        let mut p = BUF.as_mut_ptr();
        bgr.chunks(chunk_size).for_each(|c| {
            ptr::copy_nonoverlapping(c.as_ptr(), p, chunk_size);
            p = p.add(line_bytes_len);
        });
    } else {
        DATA_LEN = (WIDTH * HEIGHT * 3) as usize;
        ptr::copy_nonoverlapping(bgr.as_ptr(), BUF.as_mut_ptr(), DATA_LEN);
    };

    // ウィンドウを再描画する
    // ウィンドウプロシージャに WM_PAINT メッセージが送られる
    let rc = RECT {
        top: 32,
        left: 0,
        right: 640,
        bottom: 512,
    };
    InvalidateRect(H_WINDOW, &rc, TRUE);

    // タイトルバーにファイルパスを表示する
    SetWindowTextW(H_WINDOW, l(file_path).as_ptr());
    Ok(())
}
```
### ウィンドウに描画
最後にウィンドウへの描画処理です。Win32API 関数がいっぱい出てきますがよくわかってません（おい）。ここでの注意点は`BITMAPINFOHEADER`構造体の`biHeight`メンバに負の値を渡しているところです。ビットマップデータはボトムアップ、つまり画像の左下が起点になっています。それに対して image crate はトップダウン、つまり画像の左上が起点になったデータを出力します。なので`biHeight`に正の値を渡すと上下反転した画像が表示されることになります。

```rust
unsafe fn paint(h_wnd: HWND) -> Result<()> {
    let mut ps = init::<PAINTSTRUCT>();
    let hdc = BeginPaint(h_wnd, &mut ps);

    // ビットマップ情報設定
    let mut bi = zeroed::<BITMAPINFO>();
    bi.bmiHeader = zeroed::<BITMAPINFOHEADER>();
    bi.bmiHeader.biSize = mem::size_of::<BITMAPINFOHEADER>() as u32;
    bi.bmiHeader.biWidth = WIDTH;
    bi.bmiHeader.biHeight = -HEIGHT; // ここ！
    bi.bmiHeader.biPlanes = 1;
    bi.bmiHeader.biBitCount = 24;
    bi.bmiHeader.biSizeImage = DATA_LEN as u32;
    bi.bmiHeader.biCompression = BI_RGB;

    // 指定したデバイスコンテキストと互換性のあるビットマップを作成？？？
    let h_bmp = CreateCompatibleBitmap(hdc, WIDTH, HEIGHT);

    // BUF のビットマップデータを h_bmp にセットする？？？
    SetDIBits(
        hdc,
        h_bmp,
        0,
        HEIGHT as u32,
        BUF.as_ptr() as *const c_void,
        &bi,
        DIB_RGB_COLORS,
    );

    // HBITMAP を HDC に変換？？？
    let h_mdc = CreateCompatibleDC(hdc);
    SelectObject(h_mdc, h_bmp as *mut c_void);

    // センタリングのためのパッディング
    let padding_left = (640 - WIDTH) / 2;
    let padding_top = (480 - HEIGHT) / 2;

    // デバイスコンテキストにビットマップデータを転送？？？
    BitBlt(
        hdc,
        padding_left,
        padding_top + 32,
        WIDTH,
        HEIGHT,
        h_mdc,
        0,
        0,
        SRCCOPY,
    );

    // 後始末
    DeleteDC(h_mdc);
    DeleteObject(h_bmp as *mut c_void);
    EndPaint(h_wnd, &ps);
    Ok(())
}
```

## まとめ
- Rust で Windows Native な簡易画像ビューアを作ったよ
- Windows ビットマップの仕様を完全に理解したよ
- なにかしら画像処理した結果をリアルタイムに表示するのに使えるかも
- 写真だけじゃなくて、Rust で計算して生成した画像を表示するのもおもしろそう
- unsafe なコードばっかりで Rust の安全性はどこへやら…
- unsafe のスコープはできるだけ小さくした方がいいのかな？生ポインタは`Option`で包んだ方がいいのかな？[(参考)](https://dev.to/pjam/select-syscall-in-rust-mm)
- 全体のソースコードは[ここ](https://github.com/zxrs/pinion)