📷

【EZOG】OGP画像をブラウザを使わずに生成できるライブラリを作りました

2023/01/03に公開

こんにちは、kosei28です。

OGP画像を簡単に動的に生成できるEZOG(イージーオージー)というライブラリを作ってみました。
https://github.com/kosei28/ezog

背景

OGP画像を動的に生成するWebサイトは増えており、アクセスしてもらうためにもOGP画像の動的生成は対応したいことが多いと思います。
しかし、そのようなOGP画像の生成でも、未だにその方法は確立されていないように思います。

ブラウザを使う方法は、自由度は高いですが、ファイルサイズも動作も重いため、なるべく避けたいですし、CloudinaryなどのSaaSを使う方法も、うまく改行されないといった問題があります。

そこで、ブラウザを使わずに、簡単にOGP画像を生成できるライブラリを作ってみました。
高度なレイアウト機能は作らず、OGP画像に必要な機能だけを実装することで、うまく改行されないといった問題は起こりにくいはずです。

もともとは、私が開発しているChatockというWebサービスでこの方法を採用しており、それをライブラリに切り出した形です。
https://chatock.com/
https://zenn.dev/kosei28/articles/d965f221a656fd

インストール

まずは、

npm install ezog

もしくは

yarn add ezog

でインストールしてください。

使い方

まずは、簡単なサンプルコードを紹介します。

import { generate } from "ezog";
import { readFile, writeFile } from "fs/promises";

const ogBaseBuffer = await readFile("./og-base.png");
const robotBoldBuffer = await readFile("./Roboto-Bold.ttf");

const png = await generate(
    [
        {
            type: "image",
            buffer: ogBaseBuffer,
            x: 0,
            y: 0,
            width: 1200,
            height: 630,
        },
        {
            type: "textBox",
            text: "Hello, World",
            x: 0,
            y: 275,
            width: 1200,
            fontFamily: ["Roboto Bold", "Noto Sans 700"],
            fontSize: 60,
            lineHeight: 80,
            lineClamp: 1,
            align: "center",
            color: "#000",
        },
    ],
    {
        width: 1200,
        height: 630,
        fonts: [
            {
                type: "normalFont",
                name: "Roboto Bold",
                data: robotBoldBuffer,
            },
            {
                type: "googleFont",
                name: "Noto Sans 700",
                googleFontName: "Noto+Sans",
                weight: 700,
            },
        ],
        background: "#fff",
    }
);

await writeFile("hello-world.png", png);

最初に、og-base.pngという画像と、Roboto-Bold.ttfというフォントファイルを用意します。
今回は次の画像をog-base.pngとして使うことにします。

プログラムを実行すると、次の画像がhello-world.pngとして保存されます。

大体の使い方は、コードを見てもらえば分かると思います。

generate関数は1つ目の引数に表示する画像とテキストの配列を入れ、2つ目の引数にはオプションを設定します。

画像やフォントはBufferかArrayBufferで設定することができ、fetchから読み込んだデータをそのまま使うことができます。
フォントはGoogle Fontsから読み込むこともできます。

絵文字は、Twemojiが表示されるようになっています。
そのうち、他の絵文字も表示できるようにするかもしれません。

また、defaultFontsという多言語のNoto SansフォントをGoogle Fontsから読み込む関数も用意しており、次のように使うことができます。

import { generate, defaultFonts } from "ezog";
import { readFile, writeFile } from "fs/promises";

const fonts = defaultFonts(700);

const png = await generate(
    [
        {
            type: "textBox",
            text: "Hello, World",
            x: 0,
            y: 275,
            width: 1200,
            fontFamily: [...fonts.map((font) => font.name)],
            fontSize: 60,
            lineHeight: 80,
            lineClamp: 1,
            align: "center",
            color: "#000",
        },
    ],
    {
        width: 1200,
        height: 630,
        fonts: [...fonts],
        background: "#fff",
    }
);

await writeFile("hello-world.png", png);

実際にOGP画像の生成に使うときはAPIとして作ると思いますが、そのときは生成したpngデータをResponseのBodyに入れて、Content-Type: image/pngとしてください。

実装方法

ここからは、実装方法について軽く説明します。
詳しく知りたい方はコードを読んでみてください(読みにくいかもしれませんが・・・)。
https://github.com/kosei28/ezog

基本的には、SVGを生成してそれをsharpで画像に変換しているだけです。
画像はimageタグで埋め込んでいるだけです。

テキストはopentype.jsを使ってSVGを生成しています。
複数のフォントに対応するため、1文字ずつフォントにグリフがあるか検証しています。

1文字ずつグリフの幅を測り、1行の幅がテキストボックスの幅を超えたら改行するようにしているのですが、単語の途中で改行されないように、Intl.Segmenterでテキストを単語に分割してから処理しています。
標準のオブジェクトでテキストセグメンテーションができるのは便利ですね。

CJK(中国語、日本語、韓国語)のテキストは単語に関係なく改行されるようにしたかったので、正規表現でUnicodeのScript_Extensionsを使ってCJKの文字か判定しています。
具体的には次のようなコードで判定できます。

text.matchAll(/\p{scx=Hani}|\p{scx=Hira}|\p{scx=Kana}|\p{scx=Hang}/gu);

これでoverflow-wrap: break-word;のような挙動を実現することができます。

また、連続する空白文字は半角スペースにし、先頭の空白文字を削除することでwhite-space: normal;のようにしています。

まとめ

今回作ったライブラリは、OGP画像の生成には十分な機能があるのではないかと思います。
OGP画像を動的に生成するときがあったら、ぜひ試してみてください。
OGP画像ではなくても、画像にテキストを埋め込みたいときがあったら使えるかもしれません。

自分のために作ったライブラリなので、高度な機能を作る予定はありませんが、コントリビュートは歓迎しているので、欲しい機能があったらPRしてくれると嬉しいです。
https://github.com/kosei28/ezog

Discussion