1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

LZ4I とは?

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

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

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

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

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

rdopngフォルダに移動してcmake

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ファイルが生成される。)

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

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

0.png

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

LZ4I のファイル構造

LZ4I ファイルの最初の 14 バイトはヘッダです。その後ろに LZ4 圧縮された画像データが続きます。とても単純ですね。ヘッダは以下のような定義になっています。

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)]でパディングを詰めることができます。

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

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

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 のソースコードを見るとどうやらLZ4_decompress_safeという関数で展開しているようです。この関数部分を静的ライブラリとしてコンパイルして Rust で作る exe に埋め込むという方針で行きます。

lz4.lib の作成

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

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

_2.png

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

_3.png

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

_4.png

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

_5.png

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

_6.png

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

_7.png

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

_8.png

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

_9.png

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

_10.png

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

_11.png

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

build.rs

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

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が気になりますが今回は無視して良さそうです。一つ目の引数がソースへのポインタ、二つ目が展開先のバッファへのポインタ、三つ目がソースのサイズ、四つ目がバッファのサイズということでしょう。

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

これを 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を返すラッパ関数を作るのも良いでしょう。

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クレートに渡すだけです。

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

まとめ

おそらく世界初の LZ4I ビューアを作成しました。ソースはここに置いてます。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?