📝

Cloudflare WorkersとRust(WebAssembly)によるエッジでの画像リサイズ

2021/12/12に公開

CloudflareにはCloudflare Image Resizingがあるので、画像をオンザフライでリサイズするにはそれを使えますが、勉強の目的でCloudflare WorkersとRust(WebAssembly)を使ったエッジ上での画像リサイズに挑戦してみました。

環境の用意

Cloudflare WorkersでRustのWebAssembly開発の環境を整えるのには、下記の記事を参考にさせていただきました。Cloudflare Workersを開発するためのCLIツールであるwranglerがテンプレートを生成してくれるので簡単に環境を作成できます。

https://dev.classmethod.jp/articles/run-rust-programs-on-cloudflare-workers/

作ったもの

全コードはこちらのリポジトリからご確認ください。記事内では断片的にコードを紹介します。
https://github.com/dotneet/cf-image-resizing-rs

Cloudflare Workersの開発用CLIであるwranglerを使って動作確認できます。

# 下記を実行後、ブラウザで `http://127.0.0.1:8787/?w=100&h=150&src=画像のURL` を叩く
wrangler dev

コードの解説

Rustは素人なのでRustが分かる方には違和感があると思いますがご容赦ください。

リクエストの受付とルーティング

Cloudflare WorkersのRustではリクエストの受付はworkerクレートが用意してくれるRouterを使います。RouterにHTTPメソッドに対応する関数が定義されており、Futureを返したい場合は _async 付きのメソッドが別途用意されます。今回は非同期処理が含まれるので、get_asyncを使ってリクエストを受け付けます。

#[event(fetch)]
pub async fn main(req: Request, env: Env) -> Result<Response> {
	let router = Router::new();
	    router
		.get_async("/", |req, _| async move { ... }
		.run(req, env)
		.await
}

オリジンから画像を取得してバイト配列にする

Fetch::Requestのsend()関数で外部にHTTPリクエストを発行します。
結果はResponse型になり、bytes()関数でバイト配列を受け取ることができます。
これは非同期処理でFutureを返すため、awaitする必要があります。

async fn fetch_image(src: &str) -> Result<Vec<u8>> {
    let request = Request::new(src, Method::Get);
    let response = Fetch::Request(request?).send().await;
    response.unwrap().bytes().await
}

注意
2021/12月現在、bytes()関数にはバグが含まれており、バイト配列が壊れた状態で返ってきます。そこで、こちらの修正PRをマージした独自ビルド版のworkerクレートを使うことでこの問題を回避しました。

画像データのリサイズを行う

Rustで画像処理するにはimageクレートを使いました。
load_from_memory()を使って、DynamicImage型に変換したあと、resize_exact()でリサイズします。write_to()でバイト配列に戻すことができます。
画像フォーマットを変更することもでき、コードではPNGとJPEGに対応しています。

    pub fn modify_image(&self, bytes: &Vec<u8>) -> Result<Vec<u8>> {
        let img = load_from_memory(&bytes).expect("Failed to load an image from a byte slice.");
        let size = self.size.ok_or("no size")?;
        let modified_image = img.resize_exact(size.width, size.height, FilterType::Gaussian);
        let mut dst: Vec<u8> = Vec::new();
        let image_format: ImageOutputFormat = match self.format.as_ref() {
            "png" => ImageOutputFormat::Png,
            _ => ImageOutputFormat::Jpeg(80),
        };
        modified_image.write_to(&mut dst, image_format).unwrap();
        Ok(dst)
    }

注意
JPEGを扱うとエラーになってしまう問題がありました。これはimageクレートのjpeg_rayonという機能がWebAssemblyで使えないために起きる問題のようで、こちらのIssue を参考に Cargo.toml に下記を追加することで回避できました。

[dependencies.image]
version = "0.23.14"
default-features = false
features = ["gif", "jpeg", "ico", "png", "pnm", "tga", "tiff", "webp", "bmp", "hdr", "dxt", "dds", "farbfeld"]

画像のバイト配列をクライアントに返す

Response::from_bytes()でバイト配列のボディを持つResponseを作成した後、必要なヘッダーを付与して返します。

let response = Response::from_bytes(image_output)?;
let mut headers = Headers::new();
headers.set(
  "content-type",
  format!("image/{}", manipulation.format).as_str(),
)?;
Ok(response.with_headers(headers))

Cache APIについて

画像処理をするなら結果をキャッシュしたいですが、残念ながら現時点ではCache APIはRustのworkerクレートには未実装です。PRが出ているのでマージすれば使えそうですが、試してみたところなぜかキャッシュ結果の先頭数十バイトが壊れるというバグが発生し、これを解決できずキャッシュの実装は諦めました。

挑戦してみた所感

なんとか画像のリサイズはできたものの、独自ビルドのworkerクレートが必要になり、JPEGの対応も特殊な設定が必要でなかなかに苦難の道でした。

パフォーマンスについてですが、1000x600くらいの画像を300x300にリサイズするとレスポンスに500ms〜1000msほどかかります。キャッシュがないとさすがに実用には耐えません。workerクレートの改善が待たれます。

imageクレートはリサイズだけでなく様々な画像処理ができます。回転や白黒画像化、ピクセル単位の処理など、色々できるのでこれらがエッジ上でできるようになるのはおもしろいと思います。

試行錯誤のなかで色々なRustのクレートを入れたりしたのですが、ユーティリティ的なものが多かったからかWebAssemblyだからと言ってビルドできなかったり動かなかったりということは思ったほど多くありませんでした。OSSのライブラリを使って色々できそうなので今後の期待が大きくなりました。

それにしても、Cloudflare WorkersでRustを動かすサンプルコードは非常に少なく、細かいことでも調べるのに苦労しました。実は Fetch::Request() を見つけて動かし方を理解するだけでも一時間くらいかかりました。もっとコードサンプルがネット上に増えると嬉しいですね。

Discussion