🖼️

WebPの画像ファイルビューアをZig言語で書いてみた

2023/05/16に公開

Webpの画像ビューア

libwebpとlibsdl2を使ってWebPの画像ファイルのビューアをZig言語で書いてみました。

https://github.com/tetsu-koba/zig-webp-viewer

WebPの画像のデコードはlibwebpに一任、画面に表示するところはlibsdl2の使い方の練習という感じです。
以下のような機能があります。

  • 引数で複数のwebpファイルを受け取り、最初の一枚を画面に表示する。
  • その場合に、画面サイズよりも大きい場合には画面に収まるようにリサイズする。
  • ウィンドウの枠をドラッグしたときには、ウインドウのサイズの変更に合わせて画像をリサイズする。このとき縦横のアスペクト比は維持する。
  • スペース、カーソルの右キー、下キー、リターンキーが押下されたら次の画像を表示する。
  • カーソルキーの左キー、上キーが押下されたら前の画像を表示する。
  • qのキーが押下されたら終了する。

Ubuntu 22.04 とMacOS 13.3 で動作確認しています。

RESIZEのイベントの挙動の違い

Ubuntuで動かしたときとMacで動かしたときで、ウィンドウの枠をドラッグしてリサイズするときにイベントの発生のしかたに差異がありました。
Ubuntuではドラッグしている間、多数のRESIZEのイベントが来ました。そのたびに描画をしていると過負荷になるので、100msecに一度だけ再描画をするようにしました。
Macでは同じ操作をしてもRESIZEのイベントが来るのはドラッグをやめたときだけです。ドラッグして引っ張っている間の再描画はMacのウインドウシステム側が勝手にやっています。なのでその間は画像のアスペクト比がくずれた状態で拡大縮小されます。
SDLのライブラリを使うことで、UbuntuとMacの違いを吸収できるのですが、このようなところに挙動の差が出ることを知りました。

Zig言語の観点でのポイント

C言語でのstructへのポインタ

SDL_CreateWindowはstructへのポインタSDL_Window *を返すのですが、
それはZigでは?*sdl.struct_SDL_Windowになります。先頭に?が付いているのはNULLである可能性があるポインタであることを示しています。

defer, errdeferが便利

リソースの解放に関するところはdefer, errdeferを使って、すっきりと漏れがなく書くことができます。

moveセマンティックスは無いので自分で管理する

新たな画像をロードして表示するときに、それまでの画像のためにリソースを解放する必要があります。Rustだったら所有権の移動ということで強制的に古いリソースが解放されるところだと思いますが、Zig言語ではコンパイラはそこまでチェックしないので、プログラマが自分で管理する必要があります。

        if (self.image.pixels.len != 0) webp.free(self.image.pixels);
        self.image = try decodeWebp(self.alc, self.files[self.file_index]);

Zigのstructのデフォルト値

Zigのstructを初期化するときに =.{}; と書けるものとそうでないものがあります。
その違いはstructの定義で各フィールドのデフォルト値が指定されているかどうかです。

webp.zig
pub const ImageInfo = struct {
    width: usize = 0,
    height: usize = 0,
};

このようにデフォルト値が指定されているときは =.{};と書くことができ、widthとheigtの初期値は0になります。
Cのstructをインポートしたときに、この書き方ができないのは、デフォルト値が指定されていないためです。

条件コンパイル

MacのHomeBrewではインクルードファイルやライブラリの位置がアーキテクチャによって変わるので、環境によってその指定を変更する必要があります。これはZigではbuild.zigの中で指定します。

build.zig
    const exe = b.addExecutable(.{
        .name = "webp-viewer",
        // In this case the main source file is merely a path, however, in more
        // complicated build scripts, this could be a generated file.
        .root_source_file = .{ .path = "src/main.zig" },
        .target = target,
        .optimize = optimize,
    });
    exe.linkLibC();
    exe.linkSystemLibrary("SDL2");
    exe.linkSystemLibrary("webp");
    switch (builtin.os.tag) {
        .macos => {
            switch (builtin.cpu.arch) {
                .aarch64 => {
                    exe.addIncludePath("/opt/homebrew/include");
                    exe.addLibraryPath("/opt/homebrew/lib");
                },
                .x86_64 => {
                    exe.addIncludePath("/usr/local/include");
                    exe.addLibraryPath("/usr/local/lib");
                },
                else => {},
            }
        },
        else => {},
    }

Discussion