Satoriによる快適&ハイパフォーマンスな画像生成!
こんにちは!テラーノベルでiOS/Android/Webとフロントエンド周りを担当している @kazutoyoです!
先日テラーノベルの新しい機能として、作品のカバー画像を生成する機能をリリースしました! 🚀
こちらの機能はVercelが公開しているvercel/satoriを利用し、動的に画像を生成しています。
Satoriは、HTML/CSSを使用してSVGを出力することができるライブラリです。
Vercelは以前、PuppeteerのようなHeadless Browserを使ってOG Imageを生成するvercel/og-imageを開発していましたが、パフォーマンスやその他の問題点があったため、今回Satoriを開発しました。
テラーノベルでは、以前からカバー画像生成機能の提供を検討し始めました。
当初のプロトタイプでは、opentype.jsを利用してフォントをSVGに変換し、SVGでレイアウトして画像を生成していました。
しかし、複雑な表示の構成が難しく、レイアウトもやりづらいと感じていました。
また、og-imageのようにHTML+CSSで構成されたページからPuppeteerを使ってスクリーンショットを取得する方法も検討しましたが、パフォーマンス面で問題があると考えていました。
Satoriはこれらの問題点を解決できると判断し、今回利用することにしました。
Satoriで出来ること
Satoriで行えることを確認するには、Vercelが公開しているPlaygroundを試してみてください。
左側のペインにはHTMLとCSSで構成されたレイアウトが表示され、右側のペインでプレビューが可能です。Tailwindを使ったCSS、EmojiのProviderの種類選択、多言語対応などの例が紹介されています。
JSX Support
SatoriはJSXをサポートしているため、Reactに慣れている方にとっては普段のWebページを作成するのと同じようにレイアウトをすることができます。
(またJSXトランスパイラを有効にしないときは、React-elements-like objectによるレイアウトも可能です。)
<img>
のサポート
<img>
要素も対応しています。
しかし src
で指定されているURLはSatoriのレンダリング時に動的に取得されるため、静的な画像であればBase64でインラインで埋め込んでしまう方が良いでしょう。
CSSの対応
Satoriは内部的にReact Nativeなどで利用されているFlexbox実装のYoga LayoutのWASM版を利用しています。
CSSのサブセットとなるため、すべてのCSSの機能をサポートしているわけではありません。
詳細なAPIの対応状況はこちらをご確認ください。
フォント
Satoriは内部的にOpenType.jsを利用しており、OpenType.jsがサポートしているTTF/OTF/WOFFを利用できます。
satoriの fonts
オプションでフォントデータを設定し、 fontFamily
で指定したフォントを利用することが出来ます。
await satori(
<div style={{ fontFamily: 'Inter' }}>Hello</div>,
{
width: 600,
height: 400,
fonts: [
{
name: 'Inter',
data: inter,
weight: 400,
style: 'normal',
},
{
name: 'Inter',
data: interBold,
weight: 700,
style: 'normal',
},
],
}
)
Emojiの対応
SatoriでEmojiに対応する方法として graphemeImages
で対応する方法と loadAdditionalAsset
で対応する方法のがあります。
graphemeImages
こちらはシンプルな方法で、対応する文字のときに指定した画像のURLを返すといった対応です。
await satori(
<div>Next.js is 🤯!</div>,
{
...,
graphemeImages: {
'🤯': 'https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/1f92f.svg',
},
}
)
ただし、絵文字に対して1つ1つにこちらのように対応させるのは難しいため、次の loadAdditionalAsset
の方法で対応したほうが良いでしょう。
loadAdditionalAsset
loadAdditionalAsset
では、文字ごとにその文字の言語を判定し動的なアセットを読み込む処理が可能です。
Satoriのドキュメントにある以下の例では、 👋
の文字は code="emoji"
と判定され、そのときに data:image/svg+xml;base64,...
といったSVGの画像データを返します。
await satori(
<div>👋 你好</div>,
{
// `code` will be the detected language code, `emoji` if it's an Emoji, or `unknown` if not able to tell.
// `segment` will be the content to render.
loadAdditionalAsset: async (code: string, segment: string) => {
if (code === 'emoji') {
// if segment is an emoji
return `data:image/svg+xml;base64,...`
}
// if segment is normal text
return loadFontFromSystem(code)
}
}
)
上記の例では実際の処理が省略されているため、実際にEmojiを表示するにはSatoriのリポジトリにあるPlaygroundが参考になります。
コードとしては長いので抜粋しますが index.tsx
の loadDynamicAsset
でemojiのときに loadEmoji
関数を呼び出します。
utils/twemoji.ts
の loadEmoji
関数では文字のコードをTwemojiやFluent EmojiなどjsdelivrにホストされているSVG画像のURLを読み込んで返します。
テラーノベルではUnicode 15.0のEmojiまで対応しているNoto Color EmojiのSVGファイルをアプリケーション内に配置し、そちらを読み込むようにしています。
顔文字の対応 😃
顔文字は特殊な文字を使っているケースがあり、 fonts
オプションで指定しているような一般的なフォントでは対応していないケースがあります。
例えば次のような顔文字の目の部分など、Noto Sans CJKでも対応できません。
このようなときEmojiの対応と同様ですが、 loadDynamicAsset
で対応している言語のフォントを動的に読み込みます。
こちらもPlaygroundのコードが参考になります。
主な処理は以下の通りで、文字の言語がSatori内で対応しているもの[1]に関してはGoogle Fontsから動的にフォントを読み込みフォントデータを返します。
これにより、フォントのフォールバックが行われて特殊な文字にも対応することが出来ます。
OpenType.jsのフォントがパースできないとき
loadAdditionalAssetでGoogle Fontからフォントを動的に読み込んだとき、一部のフォントがOpenType.jsでパースが出来ない文字が発生しました。
こちらはフォントをGoogle Font APIで読み込んだが、何らかのエラー(自分の環境では400などが発生していました)が起きており、PlaygroundのコードではそのエラーページのArrayBufferとして返しているため、OpenType.jsがFontDataではない不正なデータとしてエラーとなっていました。
コードを以下のように修正します。
const res = await fetch(resource[1])
+ if (!res.ok) return null;
return res.arrayBuffer()
このように修正することでTofu表示になってしまいましたが、レンダリングエラーで生成ができないことは回避することが出来ました。
まとめ
以上、Satoriの機能と実際の使用感についてのお話でした。
絵文字や顔文字の対応は少々手間がかかりましたが、レイアウトのしやすさやパフォーマンスの向上など、画像生成でSatoriを用いたことは良かったと感じています。
テラーノベルのカバー画像生成機能はユーザーから高い評価を受けており、今後はさらにリッチな画像生成が可能になるよう改善を進めていきたいと思っています!
-
現在サポートされているのは以下に指定されているcode
https://github.com/vercel/satori/blob/main/src/language.ts#L25-L46 ↩︎
Discussion