Rustでwebp形式の画像を生成するメモ。アニメーションでも生成可能。
すること
- 静止画のwebpファイルを作成してみる
- アニメーションするwebpファイルを作成してみる
画像は正弦波を使うことにした。動いたときに分かりやすいかなと思った。
結果としてはどちらもうまくいった。ここではその記録を残す。
webpの仕様はこちら:
![](https://www.gstatic.com/devrel-devsite/prod/vc7080045e84cd2ce1faf7f7a3876037748d52d088e5100df2e949d051a784791/developers/images/opengraph/google-green.png)
使うもの
webpクレートを使って、webpアニメーションを作るのが目標。webpクレートについてはこちらを参照:
それぞれのフレーム・静止画は、imageクレートを使って用意することにした:
まずは静止画
解像度依存をなくすため座標を-1から+1の範囲のf32に変換する関数を使う。また、変換した座標に対するRGB値を関数で決めている。それぞれの関数の中身は後述。
webp生成はEncoder
の関数名にencode
を含む関数を呼び出せばいい。encode_simple()
を使うとロスレスか否かと品質(0-100)の2つを指定できる。
動画の場合もそうだが、最終的なバイナリはWebPMemory
という構造体に保持されているので、それを使ってfs
を通してファイルに書き込む。
// 静止画描画の例。useは省略
fn main() {
println!("hello");
let (width, height) = (800, 800);
let mut buf = image::ImageBuffer::new(width, height);
for (x, y, p) in buf.enumerate_pixels_mut() {
// これらの関数は後述
let (uvx, uvy) = xy_to_uv(x, y, width, height);
*p = uv_to_rgba(uvx, uvy, 1.57);
}
// imageクレートだけ使ってpngファイルを作成するならこれで十分
// buf.save("z://sin.png").unwrap();
let binding = buf.into_vec();
let encoder = Encoder::from_rgba(&binding, width, height);
let res = encoder.encode_lossless();
// この関数も後述
write_webp_file(res, "z://sin.webp").unwrap();
}
上のコードでは正弦波の位相を1.57rad
だけずらしているが、残っていた静止画は位相をずらしていないものだったので、これをwebp静止画の出力例として載せておく:
次に動画
アニメーションにはwebpクレートのAnimEncoder
とAnimFrame
が必要だ。
フレームの画像を保持しつつ、タイムスタンプを適切に指定して、エンコードする。
タイムスタンプについては、webpの仕様を見る限りはDurationを指定することになっているが、webpエンコードの実装のlibwebp
ではタイムスタンプを利用してDurationを算出している。
そして、webpクレートは内部でlibwebp
をFFIで使っているようなので、タイムスタンプで指定するようになっている。libwebp
の該当実装箇所はこのあたり:
よって、あるフレームのタイムスタンプはその手前のフレームのタイムスタンプとの差の分だけの間表示されているということになる。1つ前のフレームのタイムスタンプとの差だけでDurationを決定するので、1つ目のフレームのタイムスタンプは0でなくても問題はないと思う(未実験)。
その辺だけ注意して実装すれば問題はないと思う。間違えてすべてのフレームのタイムスタンプを同じ値にしても、表示ソフトは最小Durationを設定している(webpの仕様書に書いてあった)ので狙ったタイミングではなくなるが、アニメーションは行われる。
webp動画エンコードのコード例は以下:
use std::{f32::consts::PI, fs::File, io::Write, path::Path};
use image::Rgba;
use webp::{AnimEncoder, AnimFrame, WebPConfig, WebPMemory};
type IOResult = std::io::Result<()>;
/// 1ループの合計描画枚数
const NUMBER_OF_FRAMES: usize = 120;
/// 1フレームあたりの表示時間(ミリ秒)
const DURATION_OF_A_FRAME: i32 = 16;
// 上2つの積がアニメーションの1ループの時間になる
fn main() {
example_anim();
}
fn xy_to_uv(x: u32, y: u32, w: u32, h: u32) -> (f32, f32) {
let uvx = (x as f32 * 2.0 - w as f32) / w as f32;
let uvy = (y as f32 * 2.0 - h as f32) / h as f32;
(uvx, uvy)
}
fn uv_to_rgba(uvx: f32, uvy: f32, t: f32) -> Rgba<u8> {
if (uvy - (uvx + t).sin()).abs() < 0.0061 {
let color_v = ((uvx + t).sin() + 1.0) / 2.0;
let color_v = (color_v * 255.0) as u8;
return Rgba([color_v, color_v / 2, color_v / 3, 255]);
} else {
return Rgba([0, 0, 0, 255]);
}
}
fn write_webp_file<S: AsRef<Path>>(webpm: WebPMemory, filepath: S) -> IOResult {
let mut file = File::create(filepath)?;
file.write_all(&webpm)?;
Ok(())
}
fn example_anim() {
let (width, height) = (800, 800);
let mut config = WebPConfig::new().unwrap();
// 各フレームはこのconfigを変更して品質やロスレスフラグなどを変えられる
config.quality = 90.0;
let mut animator = Animator::new(width, height, &config);
let mut imgs = vec![];
// 1秒分のフレームを作る
let length = NUMBER_OF_FRAMES;
for i in 0..length {
let t = 2.0 * PI * i as f32 / NUMBER_OF_FRAMES as f32;
let mut buf = image::ImageBuffer::new(width, height);
// 位相をtだけずらしたsinカーブの画像を生成
for (x, y, p) in buf.enumerate_pixels_mut() {
let (uvx, uvy) = xy_to_uv(x, y, width, height);
*p = uv_to_rgba(uvx, uvy, t);
}
let v = buf.into_vec();
imgs.push(v);
}
let mut elapsed_time_ms = 0;
// animatorに追加
for i in 0..length {
animator.add_frame(&imgs[i as usize], elapsed_time_ms, width, height);
// 次に使うタイムスタンプを設定。描画時間分だけ増加させるのがいい
elapsed_time_ms += DURATION_OF_A_FRAME;
}
// animatorからwebpを生成し、保存する
let webpm = animator.webp();
write_webp_file(webpm, "z://example_sin_anim.webp").unwrap();
}
// エンコーダのラッパ
struct Animator<'a> {
encoder: AnimEncoder<'a>,
}
impl<'a> Animator<'a> {
fn new(width: u32, height: u32, config: &'a WebPConfig) -> Self {
let encoder = AnimEncoder::new(width, height, config);
Self { encoder }
}
fn add_frame(&mut self, img: &'a Vec<u8>, timestamp: i32, width: u32, height: u32) {
let frame = AnimFrame::from_rgba(&img, width, height, timestamp);
self.encoder.add_frame(frame);
}
fn webp(&self) -> WebPMemory {
self.encoder.encode()
}
}
2秒間で60fpsのwebp動画を作成する。
出力されるwebp動画はこちらのようになる:
おわり
今回はwebpに格納するためのメモとして書いた。誰かの役に立つかもしくは自分があとから見てわかることを願う。
以上です。