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 よりファイルの容量は増えてますね。
おそらく世の中にこの画像ファイルを表示できるビューアはないでしょう。では今から作ります
LZ4I のファイル構造
LZ4I ファイルの最初の 14 バイトはヘッダです。その後ろに LZ4 圧縮された画像データが続きます。とても単純ですね。ヘッダは以下のような定義になっています。
#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 ファイルの先頭部分を上記のヘッダと解釈することで画像の幅・高さなどの情報が得られます。width
、height
はビッグエンディアンであることに注意しましょう。
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 での操作例を示します。
まず「ソリューションエクスプローラー」の「ソリューション」上で右クリックして新しいプロジェクトを追加します。
「空のプロジェクト」を選択します。
「プロジェクト名」と「場所」を設定します。下の例では保存場所はrdopng
ソリューションフォルダ内にしていますが、どこでもいいです。
「ソリューションエクスプローラー」内に「lz4
プロジェクト」が追加されました。
「ソースファイル」上で右クリックして「既存の項目を追加」を選択します。
「ソースファイル」にrdopng
フォルダ直下にあるlz4.c
を追加して、「ヘッダーファイル」にlz4.h
を追加します。
「lz4
プロジェクト」上で右クリックして「プロパティ」を選択します。
「全般」のページで「構成の種類」を「スタティック ライブラリ (.lib)」に変更します。
「詳細」のページで「ターゲット ファイルの拡張子」を「.lib」にします。
「lz4
プロジェクト」上で右クリックして「ビルド」を選択します。
visual_studio_project/rdopng/x64/Release
フォルダ内にlz4.lib
が生成されておれば成功です。次はこの静的ライブラリを Rust に埋め込みます。
build.rs
Cargo.toml
があるフォルダと同じ場所にbuild.rs
を以下の内容で作成します。そうするとビルド時に自動的にライブラリを探しにいってくれてリンクするようになります。便利!(rustc-link-search のパスは適宜書き換えてください)
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
が気になりますが今回は無視して良さそうです。一つ目の引数がソースへのポインタ、二つ目が展開先のバッファへのポインタ、三つ目がソースのサイズ、四つ目がバッファのサイズということでしょう。
LZ4LIB_API int LZ4_decompress_safe (const char* src, char* dst, int compressedSize, int dstCapacity);
これを Rust でも定義します。
extern "C" {
fn LZ4_decompress_safe(src: *const u8, dst: *mut u8, compressed_size: i32, dst_capacity: i32) -> i32;
}
これで呼び出す準備ができました。C の関数は危険なのでunsafe
で囲みましょう。Rust っぽくResult
を返すラッパ関数を作るのも良いでしょう。
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
クレートに渡すだけです。
// 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);
成果物
やったぜ!
まとめ
おそらく世界初の LZ4I ビューアを作成しました。ソースはここに置いてます。