Rust で LZ4I (LZ4 Image) ビューアを作ろう

## LZ4I とは？

JPEG や PNG といった画像フォーマットは高い圧縮率を実現しますが、インターネットの転送速度やストレージ容量が大きくなった現在では、高い圧縮率よりもより速く（計算量が少なく）展開できる画像フォーマットが求められているという考えのもとで作られた画像フォーマットです。

http://richg42.blogspot.com/2023/04/a-dead-simple-lossless-or-lossy-lz4.html

以前 Rust で Windows の GUI 画像ビューアを作ったので、これを LZ4I に対応させたいと思います。

https://qiita.com/benki/items/a01556b3391b264941fa

## まずは LZ4I を生成してみよう

`rdopng`リポジトリを任意のフォルダにクローンします。

```terminal
~> cd /path/to/visual_studio_project 
visual_studio_project> git clone https://github.com/richgel999/rdopng.git
```

`rdopng`フォルダに移動して`cmake`。

```terminal
visual_studio_project> cd rdopng
visual_studio_project/rdopng> cmake .
```

そうすると、`rdopng.sln`という Visual Stdio 用のソリューションファイルが生成されるので、ダブルクリックして Visual Stdio を起動します。x64 の Release 構成でソリューションをビルドすると`visual_studio_project/rdopng/bin/Release`フォルダ下に`rdopng.exe`が生成されました。

Release フォルダ下に移動して`rdopng.exe`に LZ4I を生成させるオプションと入力ファイルへのパスを渡してやれば`rdopng.exe`があるフォルダに LZ4I ファイルが生成されます。（下記の場合、`visual_studio_project/rdopng/bin/Release`フォルダに`image_rdo.lz4i`ファイルが生成される。）

```terminal
visual_studio_project/rdopng> cd bin/Release
visual_studio_project/rdopng/bin/Release> rdopng.exe -lz4i /path/to/image.png
```

オリジナルの PNG よりファイルの容量は増えてますね。

![0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353186/f328eb60-52b1-3410-1bc0-1b05659f0f09.png)

おそらく世の中にこの画像ファイルを表示できるビューアはないでしょう。では今から作ります:grin:

## LZ4I のファイル構造

LZ4I ファイルの最初の 14 バイトはヘッダです。その後ろに LZ4 圧縮された画像データが続きます。とても単純ですね。ヘッダは以下のような[定義](https://github.com/richgel999/rdopng/blob/main/rdopng.cpp#L3278)になっています。

```C:rdopng.cpp
#pragma pack(push, 1)
struct lz4i_header
{
	char sig[4]; // signature bytes "lz4i"
	uint32_t width; // image width in pixels (BE)
	uint32_t height; // image height in pixels (BE)
	uint8_t channels; // 3 = RGB, 4 = RGBA
	uint8_t colorspace; // 0 = sRGB with linear alpha 1 = all channels linear
};
#pragma pack(pop)
```

このヘッダを Rust に写経しましょう。`#[repr(packed)]`でパディングを詰めることができます。

```Rust
#[repr(packed)]
struct Lz4iHeader {
    sig: [u8; 4],
    width: u32,
    height: u32,
    channels: u8,
    colorspace: u8,
}
```

メモリに読み込んだ LZ4I ファイルの先頭部分を上記のヘッダと解釈することで画像の幅・高さなどの情報が得られます。`width`、`height`はビッグエンディアンであることに注意しましょう。

```Rust
let raw_lz4i = fs::read(file_path)?;
let header = unsafe { &*(raw_lz4i.as_ptr() as *const Lz4iHeader) };
ensure!(header.sig.eq(b"lz4i"), "Invalid LZ4I format.");

let width = header.width.to_be();
let height = header.height.to_be();
```

## デコード

続いて LZ4 圧縮された画像部分のデータをデコード（RGB(A) のピクセルデータに展開）しましょう。rdopng の[ソースコード](https://github.com/richgel999/rdopng/blob/main/rdopng.cpp#L3932)を見るとどうやら`LZ4_decompress_safe`という関数で展開しているようです。この関数部分を静的ライブラリとしてコンパイルして Rust で作る exe に埋め込むという方針で行きます。

### lz4.lib の作成

Visual Studio のソリューションがすでに用意されてるので、Visual Stduio で静的ライブラリを作ります。以下 Visual Stdio 2019 Comunity Edition での操作例を示します。

まず「ソリューションエクスプローラー」の「ソリューション」上で右クリックして新しいプロジェクトを追加します。

![_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353186/7550f2de-3a41-813d-c26c-a62b0bbcd655.png)

「空のプロジェクト」を選択します。

![_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353186/653f6c8e-3506-cf62-f85f-7db50444b79c.png)

「プロジェクト名」と「場所」を設定します。下の例では保存場所は`rdopng`ソリューションフォルダ内にしていますが、どこでもいいです。

![_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353186/a6c2561d-dcec-7233-726b-0af5439ff9df.png)

「ソリューションエクスプローラー」内に`「lz4`プロジェクト」が追加されました。

![_5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353186/772fe661-6c65-6ebb-2395-ab67e5aac8a1.png)

「ソースファイル」上で右クリックして「既存の項目を追加」を選択します。

![_6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353186/b4a41e78-2a88-5ce9-de16-e6e68fd5f735.png)

「ソースファイル」に`rdopng`フォルダ直下にある`lz4.c`を追加して、「ヘッダーファイル」に`lz4.h`を追加します。

![_7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353186/18af8bb3-319c-e68e-ef5f-855c6dcb5e60.png)

「`lz4`プロジェクト」上で右クリックして「プロパティ」を選択します。

![_8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353186/7ed6f024-f83e-b6f3-38cf-59611e654bae.png)

「全般」のページで「構成の種類」を「スタティック ライブラリ (.lib)」に変更します。

![_9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353186/cfb388ca-f466-f948-0af6-75a826cfb85b.png)

「詳細」のページで「ターゲット ファイルの拡張子」を「.lib」にします。

![_10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353186/14d29839-e4d4-4a50-8f50-ee25c2b34595.png)

「`lz4`プロジェクト」上で右クリックして「ビルド」を選択します。

![_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353186/7f72d266-f45f-da06-4dec-70557a439d5c.png)

`visual_studio_project/rdopng/x64/Release`フォルダ内に`lz4.lib`が生成されておれば成功です。次はこの静的ライブラリを Rust に埋め込みます。

## build.rs

`Cargo.toml`があるフォルダと同じ場所に`build.rs`を以下の内容で作成します。そうするとビルド時に自動的にライブラリを探しにいってくれてリンクするようになります。便利！（rustc-link-search のパスは適宜書き換えてください）

```rust:build.rs
fn main() {
    println!(
        r"cargo:rustc-link-search=C:\path\to\visual_studio_project\rdopng\x64\Release"
    );
    println!("cargo:rustc-link-lib=lz4");
}

```

## Rust から C の関数を呼ぶ

Rust から呼び出したい`LZ4_decompress_safe`関数は`lz4.h`で以下のように定義されています。先頭の`LZ4LIB_API`が気になりますが今回は無視して良さそうです。一つ目の引数がソースへのポインタ、二つ目が展開先のバッファへのポインタ、三つ目がソースのサイズ、四つ目がバッファのサイズということでしょう。

```C:lz4.h
LZ4LIB_API int LZ4_decompress_safe (const char* src, char* dst, int compressedSize, int dstCapacity);
```

これを Rust でも定義します。

```rust:main.rs
extern "C" {
    fn LZ4_decompress_safe(src: *const u8, dst: *mut u8, compressed_size: i32, dst_capacity: i32) -> i32;
}
```

これで呼び出す準備ができました。C の関数は危険なので`unsafe`で囲みましょう。Rust っぽく`Result`を返すラッパ関数を作るのも良いでしょう。

```rust:main.rs
fn lz4_decomp(header: &Lz4iHeader, src: &[u8]) -> Result<Vec<u8>> {
    let width = header.width.to_be();
    let height = header.height.to_be();
    let dst_capacity = width
        .checked_mul(height)
        .context("u32 overflow")?
        .checked_mul(header.channels as u32)
        .context("u32 overflow")? as usize;
    let mut dst = vec![0; dst_capacity];
    unsafe {
        LZ4_decompress_safe(
            src.as_ptr(),
            dst.as_mut_ptr(),
            src.len() as i32,
            dst_capacity as i32,
        )
    };
    Ok(dst)
}
```

RGB のピクセルデータが取得できたので、あとは`image`クレートに渡すだけです。

```rust:main.rs
// LZ4I を展開して
let decomped = lz4_decomp(header, &raw_lz4i[header_size..])?;
// image クレートのバッファとして扱う
let buf = ImageBuffer::<Rgb<_>, _>::from_raw(width, height, decomped).context("buf overflow.")?;
// DynamicImage にすると
let img = DynamicImage::ImageRgb8(buf);
// リサイズとかもできる
let img = img.resize(640, 640, imageops::Lanczos3);
```

## 成果物

やったぜ！:relaxed:

![クリップボード一時ファイル01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353186/b739086f-07f1-4c74-fb23-75a1148e2f75.png)

## まとめ

おそらく世界初の LZ4I ビューアを作成しました。ソースは[ここ](https://github.com/zxrs/pinion)に置いてます。