📷

SvelteKitのサーバサイドの処理で画像を動的に生成して返す方法

2023/04/23に公開

SvelteKitでは、+server.jsを実装することでWeb APIを作成できます。
本記事では、動的に生成した画像データをこのAPIのレスポンスに含める方法について解説します。

+server.jsの基本的な実装方法については公式ドキュメントをご参照ください。

サンプルコード

ソースコードはGitHubに置きました。
あとVercelで公開しているので、試しに動かしたい方はこちら

解説

画像ファイルを読み込んでそのデータをレスポンスに含める

あらかじめstaticフォルダ内に画像を配置しておきましょう。

あとはfetchでその画像を読み込み、バイナリデータをレスポンスに含めてやれば良いです。
レスポンスヘッダのContent-Typeはimage/jpegにしておきましょう。
(これは画像の拡張子にあわせて、image/pngとかimage/gifなどに変える)

+server.ts
import { error } from "@sveltejs/kit";

/**
 * 画像ファイルを読み込んで返す
 * @returns png形式の画像データ
 */
export async function GET({ fetch }) {
  const image = await fetch("/dog.jpg", {
    method: "GET",
    headers: {
      "content-type": "image/jpeg"
    }
  }).then(async (response) => {
    if (!response.ok) {
      throw error(500, "Internal Server Error");
    }
    return await response.blob();
  });

  const response = new Response(image);
  response.headers.set("Content-Type", "image/jpeg");
  return response;
}

画像ファイルを動的に生成し、そのデータをレスポンスに含める

サーバサイドでcanvasを使う

JavaScriptで画像を動的に生成する手段はいくつかあると思いますが、今回はcanvas要素を使用します。
ただし、Node.jsにはcanvasを制御するようなAPIが存在しないため、@napi-rs/canvasというライブラリを使用します。これを使うと、canvasの制御がフロントエンドでやる時と同じ書き方でできるようになります。

+server.ts
import { createCanvas, GlobalFonts, loadImage } from "@napi-rs/canvas";
…
…
…

  const canvas = createCanvas(600, 315);
  const context = canvas.getContext("2d");

  const background = await loadImage(url.origin + "/dog.jpg");
  context.drawImage(background, 0, 0);

  context.font = "30px Noto";
  context.fillText(text, 30, 100);

補足

Node.jsでcanvasを制御するライブラリには、node-canvasというものもあります。
こちらは、Node.jsを実行する環境のOSに合わせていくつか追加のライブラリをインストールする必要があるため、導入に一手間かかります。
@napi-rs/canvasの場合は、そのあたりを良しなにやってくれるので、今回はこれを使用しました。

日本語が使えるフォントをインストールする

context.fillTextメソッドで文字列をcanvasに出力した際、実行環境で使えるフォントによっては日本語や記号が表示されません。ということで、Google FontsからNoto Sans Japaneseのttfファイルをダウンロードし、それを読み込むようにしましょう。リンク先の"Download family"からzipをダウンロードし、解凍するとttfファイルが手に入ります。

ttfファイルもstaticフォルダに置きます。

あとはfetchメソッドでこのttfファイルを読みこめば良いだけかと思いきや、読み込んだ時に生成されるArrayBufferオブジェクトをBufferオブジェクトに変換する必要があります。arraybuffer-to-bufferを使いましょう。

ということで、フォントの読み込みと登録はこのような実装になります。

+server.ts
import { createCanvas, GlobalFonts, loadImage } from "@napi-rs/canvas";
import { arrayBufferToBuffer } from "arraybuffer-to-buffer";
…
…
…
  if (!GlobalFonts.has("Noto")) {
    const font = await fetch("/NotoSansJP-Regular.ttf", {}).then(async (response: Response) => {
      if (!response.ok) {
        throw error(500, "Internal Server Error");
      }
      return await response.arrayBuffer();
    });
    GlobalFonts.register(arrayBufferToBuffer(font), "Noto");
  }

あとはcanvasのcontextでフォントを指定し、fillTextメソッドを呼んでやれば良いです。

+server.ts
  context.font = "30px Noto";
  context.fillText(text, 30, 100);

雑感

これでOGP画像の生成とかもできるはず。

参考文献

Discussion