Lambdaカクテル

京都在住Webエンジニアの日記です

Party Parrotを自作したくてゲーミングGIF生成ツールをRustで作った

様々な画像が明滅するので気を付けよう!

ここに僕がいます

https://raw.githubusercontent.com/windymelt/superify/master/yuyuko.2.small.png

これがこうなるツールを作成しました

% git clone https://github.com/windymelt/superify.git
% cd superify
% cargo build --release
% ./target/release/superify windymelt.png
% open windymelt.png.animated.gif

https://raw.githubusercontent.com/windymelt/superify/master/yuyuko.2.small.png.animated.gif

めでたいですね。

ゲーミングGIFを作りたい

自粛疲れなんて言葉がありますが僕も疲れています。せめて明るい話題が欲しいもの。そんな中僕はParty Parrotが好きで、何の説明も無しにゲーミングPCめいて発光するオウムを見るとなんか面白い気分になってしまいます。

https://cultofthepartyparrot.com/parrots/hd/parrot.gif

僕も画像を虹色に光らせたい。仮にゲーミングGIFと呼ぶことにしましょう。Slackの絵文字に設定すると爆笑必至、社内の人気者です。

GIMPでチマチマ作業することで作ることもできるのですが、やることは同じなので自動化したい。欲しいときに一瞬でキマったGIFを手に入れたいので、プログラムを作ることにしました。

どうすればいいか

どうすればゲーミングGIFを作れるのか考えてみます。

  • 画像を読み取る
  • 元画像の色相環(HSVのHue)を数°回転させた画像を生成する
  • 1周するまで繰り返す
  • 回転した画像を1つのアニメーションGIFにする

こういう流れでいけそうです。

実装

画像の読み込みができてGIFが出力できる言語、というかライブラリがあれば何でもよさそうですが、ひとまず最近勉強しているRustでやってみようと思いました。なんか速そうだし。

色相回転はimageライブラリで可能です。色相を15°回転させた画像を24回生成し一度ディスクに保存します。次に、Engiffenという画像をアニメーションGIFに変換するライブラリを使い、結合してもらいます。fpsは24に設定し、1秒で1回転するようになります。 一度保存したのは、Engiffenのインターフェイスの都合です。

f:id:Windymelt:20210612213708p:plain
15°ずつ色相を回転させた画像(24枚)

できたコードです。

use engiffen::*;
use image::imageops::*;
use std::fs::File;

fn main() {
    let args: Vec<String> = std::env::args().collect();
    if args.len() < 2 {
        exit_with_message("Please provide image file path");
    }
    let filename = &args[1];
    let outfilename = format!("{}.animated.gif", filename);

    let img = match image::open(filename) {
        Ok(img) => img,
        Err(_) => return exit_with_message("Failed to open file"),
    };

    for i in 0..24 {
        let img2 = colorops::huerotate(&img, 15 * i); // 24frameで360になる
        match img2.save(format!("out_{}.png", i)) {
            Ok(_) => print!("."),
            Err(_) => exit_with_message("Failed to save image"),
        }
    }
    println!();
    let paths = vec![
        "out_0.png",
        "out_1.png",
        "out_2.png",
        "out_3.png",
        "out_4.png",
        "out_5.png",
        "out_6.png",
        "out_7.png",
        "out_8.png",
        "out_9.png",
        "out_10.png",
        "out_11.png",
        "out_12.png",
        "out_13.png",
        "out_14.png",
        "out_15.png",
        "out_16.png",
        "out_17.png",
        "out_18.png",
        "out_19.png",
        "out_20.png",
        "out_21.png",
        "out_22.png",
        "out_23.png",
    ];
    let images = engiffen::load_images(&paths);
    let gif = match engiffen(&images, 24, Quantizer::NeuQuant(2)) {
        Ok(g) => g,
        Err(_) => return exit_with_message("making gif failed"),
    };
    let mut outfile = match File::create(&outfilename) {
        Ok(f) => f,
        Err(_) => return exit_with_message(&format!("failed to create {}", &outfilename)),
    };
    match gif.write(&mut outfile) {
        Ok(_) => (),
        Err(_) => return exit_with_message(&format!("failed to write {}", &outfilename)),
    };
    for p in paths {
        match std::fs::remove_file(p) {
            Ok(_) => (),
            Err(_) => return exit_with_message(&format!("failed to remove temporary file {}", p)),
        };
        print!(".");
    }
    println!("done!");
}

fn exit_with_message(message: &str) {
    eprintln!("*** {}", message);
    std::process::exit(1);
}

ファイル名をベタ書きしているところがダサいのですが、まあ動くので良いかと思いました。動作はかなり速く、いまのところストレスは感じません。

github.com

工夫したところ

Rustは成功するか失敗するかわからない関数の実行結果をResultという型で返してきます。プロトタイプでは適当に.unwrap()を呼んで動かしていました(scalaで言うとOption.getみたいな危険なメソッドです)が、最終的にちゃんとmatchで分岐して処理させるようにしました。このパターンで書くのがベストなのかはよくわかりません。ifで分岐するよりはちょっと綺麗かな?という感じです。モナドがあったらいいですね。

そういえばRustのborrowまわりはここが分かりやすかったと思います。

imoz.jp

C++のunique_ptrから辿って説明されているのですんなり理解できました。

よかったですね

cargo build --release した結果のバイナリサイズは5.6MiBでした。stripしたら2.2MiBにまで減りました。ご活用ください。

f:id:Windymelt:20210612212036g:plain

Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?