cairoのrustバインディングとffmpegで2Dアニメ作成ツールを作ってみた

cairoのrust バインディングがあっさり使えたので、それをffmpegでエンコードして動画にするツールを作ってみました。
ソースコードは gist に貼りました。[ここ](https://gist.github.com/tetsu-koba/14083c6705b69017bbc7fb97602f610a#file-main-rs)
実用ではなくて私のrustの練習用です。

# cairo のrustバインディング

cairoは2Dグラフィックスのライブラリです。
Rustからcairoを使う方法はこのページに丁寧に書いてあります。
[Intro to Cairo Graphics in Rust](https://medium.com/@bit101/intro-to-cairo-graphics-in-rust-35470a6aed86)
Ubuntu 18.04上で、このページの通りにやってcairoのライブラリのインストールからRustでのサンプルプログラムの実行まで問題なくできました。

# プログラムの動作

とても単純です。
1フレームごとにcairoのグラフィックスライブラリで描画して、そのビットマップデータを子プロセスとして起動しておいたffmpegに食わせてエンコードしてmp4の動画ファイルを作ります。

# プログラムの実行方法

全てソースコード内にハードコードしています。引数なしで実行させると、1280x720 30fps で10秒間の動画を作成します。これを変更するにはmain関数の定数を変更してください。
描画内容を変えるには関数draw()を編集してください。

作成できたmp4の動画ファイルをYouTubeに貼りました。
https://www.youtube.com/watch?v=JVZBuGC_HNU

描画する関数draw()は以下の通り。引数f に毎回インクリメントされるフレーム番号が入ります。
全面を白で塗りつぶし、扇形の描画、文字列の描画を行っています。

```rust
fn draw(surface: &ImageSurface, f: i32) {
    let cr = Context::new(surface);
    let width = surface.get_width() as f64;
    let height = surface.get_height() as f64;
    let dx = f as f64 * 1.0;

    cr.set_source_rgb(1.0, 1.0, 1.0);
    cr.paint();

    let cx = width / 2.0;
    let cy = height / 2.0;
    let r = cy;
    let cstart = -0.5 * PI;
    let cend = cstart + 2.0 * PI * ((f + 1) as f64) / 300.0;
    cr.move_to(cx, cy);
    cr.line_to(cx, 0.0);
    cr.arc(cx, cy, r, cstart, cend);
    cr.line_to(cx, cy);
    cr.set_source_rgba(0.0, 0.5, 0.0, 0.2);
    cr.fill();

    cr.select_font_face(
        "sans-serif",
        cairo::FontSlant::Normal,
        cairo::FontWeight::Normal,
    );
    cr.set_font_size(70.0);
    cr.move_to(600.0 - dx, 100.0);
    cr.set_source_rgb(0.0, 0.0, 1.0);
    cr.show_text("Hello, world! 1234567890");
    cr.fill();
}
```

子プロセスとしてffmpegを起動して、そこにピクセルデータを送り込むところは以下の通り。

```rust
fn make_cmdline(width: i32, height: i32, framerate: i32) -> String {
    format!(
        "ffmpeg -f rawvideo -pix_fmt bgra -s {width}x{height} -i - -pix_fmt yuv420p -r {framerate} -y out.mp4",
        width = width,
        height = height,
        framerate = framerate
    )
}

fn make_movie(width: i32, height: i32, framerate: i32, frames: i32) {
    let mut surface =
        ImageSurface::create(Format::ARgb32, width, height).expect("Couldn't create surface");
    let mut child = Command::new("/bin/sh")
        .args(&["-c", &make_cmdline(width, height, framerate)])
        .stdin(Stdio::piped())
        .spawn()
        .expect("failed to execute child");
    {
        // limited borrow of stdin
        let child_stdin = child.stdin.as_mut().expect("failed to get stdin");

        (0..frames).for_each(|f| {
            draw(&surface, f);
            let d = surface.get_data().expect("Failed to get_data");
            child_stdin.write_all(&d).expect("Failed to write to file");
        });
    }
    child.wait().expect("child process wasn't running");
}

fn main() {
    make_movie(1280, 720, 30, 300);
}
```

# つまずいたところ

## get_data()を使用可能にする条件

`cairo::ImageSurface::get_data()` がリファレンスカウントをチェックしていて、それが1より大きいとNoExclusive だという理由でエラーになります。描画のためにこのsurfaceの`cairo::Context`を作るとそれでリファレンスカウンタが増えます。なので、get_data()を成功させるには、それまでに`cairo::Context`をドロップさせなければなりません。
この関係がなかなかわからずに苦労しました。

なお、`cairo::Context`は内部に可変の状態を持っているので意味的にはmutable のはずです。しかし、状態を変更する部分が全て `unsafe` に囲まれているのでRustコンパイラとしてはチェックができないためimmutableの扱いです。

## ピクセルデータの順番

`cairo::ImageSurface`　ではARGBの32bitでピクセルを扱います。`get_data()` ではこれを`[u8]`として取り出せます。これをこのまま`write_all()`で書くのが一番簡単なのですが、そうすると32bit単位のデータを8bit単位で扱うことになるので、順番がひっくり返ります。ですが、ffmpeg ではピクセルデータのフォーマットの対応の幅が広くてBGRAの32bitも扱えるので、単にそのように指定するだけで済んでいます。
もしも順番を変えずに転送したい場合は`[u8]`で取り出したデータのraw pointerをunsafeな方法で`[u32]`にキャストして扱うことになると思います。

# Future work

このツールは、時系列のデータの可視化に使えると思っています。GPSから取得した位置情報のログを地図データ上にプロットするとか。

8コアのマシンで実行してCPUの負荷状態を見たのですが、まだCPUを使い切っていません。マルチスレッド化して複数のCPUに描画を割り振るなど工夫の余地がたくさんあります。それは今後の楽しみに。