hanzochang
hanzochang
はじめに
対象読者
前提知識
システム概要
技術スタック
使用ライブラリ
Satori
Sharp
Google Fonts API
実装の核心
1. WebFontの動的読み込み
2. ReactコンポーネントからSVGへの変換
3. Sharpによる多層画像合成
技術的なポイント
WebFontの動的読み込みの利点
Sharpのブレンドモードの活用
実際の使用例
シンプルなNext.js APIルート
フロントエンドでの呼び出し
まとめ
https://s3.ap-northeast-1.amazonaws.com/hanzochang.com/_v2/article-ogp-020.png

Next.js + Satori + Sharp で動的画像生成 - WebFontとレイヤー合成を使いこなす

Reactコンポーネントをベースにした動的画像生成システムの構築方法を解説します。Satori、Sharp、Google Fontsを組み合わせ、WebFontの動的読み込みと複数レイヤーの画像合成を実現する実践的な手法を紹介します。

公開日2025.07.20

更新日2025.07.20

はじめに

Webサービスを運営していると、ユーザーのデータに応じて画像を動的に生成したい場面が必ずと言っていいほど出てきます。例えば、ユーザーが投稿したコンテンツのOGP画像であったり、証明書やバッジの画像であったり、商品紹介カードの画像であったりと、その用途は多岐にわたります。



従来、こうした動的画像生成は非常に面倒な作業でした。ImageMagickのような画像処理ライブラリを使って複雑なスクリプトを書いたり、Canvasライブラリを駆使してピクセル単位で描画処理を記述したりと、どれも開発者にとって負担の大きい作業ばかりでした。特に複数の画像レイヤーを重ね合わせたり、WebFontを使った美しいテキストレンダリングを実現したりするとなると、その複雑さは一気に跳ね上がります。



そんな状況を一変させてくれるのが、SatoriとSharpの組み合わせです。Satoriを使えばReactコンポーネントで書いたJSXを直接SVGに変換でき、SharpならPhotoshop顔負けの高度なレイヤー合成処理を簡単に実装できます。この記事では、この2つのライブラリを使って、WebFontの動的読み込みと複数レイヤーの画像合成を組み合わせた、実用的な動的画像生成システムの構築方法を解説します。

対象読者

  • 動的画像生成を自動化したい開発者
  • Reactコンポーネントベースのデザインシステムを画像生成に活用したい方
  • WebFontや画像合成処理に興味がある方

前提知識

  • React/Next.jsの基本的な知識
  • TypeScriptの理解
  • Node.js APIの基本的な理解

システム概要

今回構築するシステムの核となる技術要素は以下の通りです:

  • Satori: ReactコンポーネントをSVGに変換、WebFontの動的読み込み対応
  • Sharp: 高性能画像処理ライブラリ、複数レイヤーの合成処理
  • Google Fonts API: WebFontの動的取得とバイナリデータ化

これらを組み合わせることで、従来は複雑だった「美しいフォントを使ったテキストレンダリング」と「複数画像の高度な合成処理」を、Web開発者にとって馴染みのあるReactとNext.jsの枠組みの中で実現できます。

技術スタック

使用ライブラリ

今回のシステムでは以下の技術を組み合わせます:

{
  "satori": "^0.10.9",
  "sharp": "^0.33.0",
  "@next/font": "^latest"
}

Satori

  • 役割: ReactコンポーネントをSVGに変換
  • 特徴: JSXでデザインを記述、Google Fonts対応、CSS-in-JS対応
  • Satori公式ドキュメント

Sharp

  • 役割: 高性能画像処理・合成
  • 特徴: 複数画像の合成、ブレンドモード、リサイズ、エフェクト
  • Sharp公式ドキュメント

Google Fonts API

  • 役割: 動的フォント読み込み
  • 特徴: WebFontの動的取得、フォント埋め込み
  • Google Fonts API

実装の核心

1. WebFontの動的読み込み

まず、Google Fonts APIを使ってWebFontをバイナリデータとして取得する仕組みを見てみましょう:

async function getFont(fontFamily: string, weight = 400): Promise<ArrayBuffer> {
  // Google Fonts CSSを取得
  const API = `https://fonts.googleapis.com/css2?family=${fontFamily.replace(/ /g, '+')}:wght@${weight}&display=swap`
  const css = await (await fetch(API)).text()
  
  // CSS内からフォントファイルURLを抽出
  const resource = css.match(/src: url\((.+)\) format\('(opentype|truetype)'\)/)
  if (!resource) throw new Error(`Failed to find font URL for: ${fontFamily}`)

  // フォントファイルをバイナリデータとして取得
  return await (await fetch(resource[1])).arrayBuffer()
}

この仕組みにより、任意のGoogle Fontsをサーバーサイドで動的に読み込み、Satoriで使用可能なバイナリ形式に変換できます。

2. ReactコンポーネントからSVGへの変換

次に、Satoriを使ってReactコンポーネントをSVGに変換する部分です:

import satori from 'satori'

// シンプルなテンプレート例
const CardTemplate = ({ title, description }: { title: string, description: string }) => {
  return (
    <div
      style={{
        display: 'flex',
        flexDirection: 'column',
        width: '800px',
        height: '400px',
        backgroundColor: '#1a1a1a',
        padding: '40px',
      }}
    >
      <h1
        style={{
          color: 'white',
          fontFamily: 'Inter',
          fontSize: '48px',
          fontWeight: 700,
        }}
      >
        {title}
      </h1>
      <p
        style={{
          color: '#cccccc',
          fontFamily: 'Inter',
          fontSize: '24px',
          marginTop: '20px',
        }}
      >
        {description}
      </p>
    </div>
  )
}

// SVG生成処理
async function generateSVG(title: string, description: string) {
  const fontData = await getFont('Inter', 400)
  
  const svg = await satori(
    CardTemplate({ title, description }), 
    {
      width: 800,
      height: 400,
      fonts: [
        {
          name: 'Inter',
          data: fontData,
          weight: 400,
          style: 'normal',
        },
      ],
    }
  )
  
  return svg
}

3. Sharpによる多層画像合成

最後に、Sharpを使って複数の画像レイヤーを合成する処理です:

import sharp from 'sharp'

async function compositeImages() {
  // 1. 背景レイヤーを作成
  const background = await sharp({
    create: {
      width: 800,
      height: 400,
      channels: 4,
      background: { r: 255, g: 255, b: 255, alpha: 1 },
    },
  }).png().toBuffer()

  // 2. テキストレイヤー(Satoriで生成したSVG)をPNGに変換
  const textLayer = await sharp(Buffer.from(svgString)).png().toBuffer()

  // 3. 装飾画像の読み込み
  const decorationLayer = fs.readFileSync('decoration.png')

  // 4. 全レイヤーを合成
  const finalImage = await sharp(background)
    .composite([
      {
        input: decorationLayer,
        top: 0,
        left: 0,
        blend: 'multiply', // ブレンドモードを指定
      },
      {
        input: textLayer,
        top: 0,
        left: 0,
        blend: 'over', // 通常合成
      },
    ])
    .jpeg({ quality: 90 })
    .toBuffer()

  return finalImage
}

技術的なポイント

WebFontの動的読み込みの利点

従来、サーバーサイドでWebFontを使用するには、事前にフォントファイルをダウンロードしてプロジェクトに含める必要がありました。しかし、今回の手法を使えば:

  • 任意のGoogle Fontsをランタイムで動的に取得可能
  • 複数のfont-weightを組み合わせた豊富な表現
  • フォント更新の自動追従(Google Fonts側の更新に自動で追従)

Sharpのブレンドモードの活用

Sharpには多様なブレンドモードが用意されており、Photoshopのレイヤー合成と同等の表現が可能です:

  • multiply: 乗算合成(暗い部分が強調される)
  • overlay: オーバーレイ合成(コントラストが強調される)
  • screen: スクリーン合成(明るい部分が強調される)
  • soft-light: ソフトライト合成(柔らかな光の効果)

これらを組み合わせることで、単純な画像の重ね合わせでは実現できない画像合成が可能になります。

実際の使用例

シンプルなNext.js APIルート

// app/api/generate-card/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
  const { title, description } = await request.json()

  // 1. フォントの動的読み込み
  const fontData = await getFont('Inter', 400)

  // 2. SVG生成
  const svg = await generateSVG(title, description)

  // 3. 画像合成
  const finalImage = await compositeImages(svg)

  // 4. Base64で返却
  const base64 = finalImage.toString('base64')
  return NextResponse.json({
    image: `data:image/jpeg;base64,${base64}`
  })
}

フロントエンドでの呼び出し

const [imageUrl, setImageUrl] = useState<string>('')

const generateCard = async () => {
  const response = await fetch('/api/generate-card', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      title: 'Hello World',
      description: 'This is a dynamically generated image.',
    }),
  })
  
  const { image } = await response.json()
  setImageUrl(image)
}

return (
  <div>
    <button onClick={generateCard}>カード生成</button>
    {imageUrl && <img src={imageUrl} alt="Generated Card" />}
  </div>
)

まとめ

この記事では、SatoriとSharpを組み合わせた動的画像生成システムの構築方法を解説しました。

従来の画像生成アプローチと比較して、今回紹介した手法には以下のような革新的な利点があります:

  • 開発者体験の向上: Web開発者が慣れ親しんだReactとCSSで画像デザインを記述可能
  • 動的フォント活用: Google Fontsの豊富なライブラリをサーバーサイドで活用
  • 複雑な合成処理: Photoshop並みのレイヤー合成をコードで実現
picture
hanzochang - 半澤勇大
慶應義塾大学卒業後、Webプランナーとして勤務。 ナショナルクライアントのキャンペーンサイトの企画・演出を担当。 その後開発会社に創業メンバーとして参加。 Fintech案件や大手企業のDXプロジェクトに関わり、その後個人事業主として独立し、 2023年にWeb3に特化した開発会社として法人化しました。 現在はWeb3アプリ開発を中心にAI開発フローの整備を行っています。
また、趣味で2017年ごろより匿名アカウントでCryptoの調査等を行い、 ブロックチェーンメディアやSNSでビットコイン論文等の図解等を発信していました。
X (Twitter)

お問い合わせはこちらから

ご希望に応じて職務経歴書や過去のポートフォリオを提出可能ですので、必要な方はお申し付けください。
また内容とによっては返信ができない場合や、お時間をいただく場合がございます。あらかじめご了承ください。