4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SatoriとExpressで動的な画像をJSXで作成する

Last updated at Posted at 2024-06-17

はじめに

こんにちは、いなたつ @inatatsu_csg です。
Symbolブロックチェーン上のトランザクションをWebアプリケーションから受け取り、署名を行いWebアプリケーションへと返却するブラウザ拡張機能SSS Extensionの開発や株式会社 OpeningLineでSymbolブロックチェーンを用いたWebアプリケーションの開発や、Symbolブロックチェーンを用いたアプリケーション開発に関する書籍 実践Symbolの執筆などを行っています。

今回は、動的な画像を生成するサーバの作成をExpressとsatoriを使って作る方法を解説します。

使用技術

  • React : JSX記法で画像を記述します
  • satori : JSXからsvgを生成します
  • sharp : pngにデータを変換する際に使用します
  • fs : フォントデータを読み込むために使用します
  • express : 画像を配信するためのサーバになります

デプロイ先がVercelやFirebaseFunctionsになる場合は使用するインフラに合わせてexpressの処理を書き換えるとその他のデプロイ環境にも対応可能です。

satori

https://www.npmjs.com/package/satori
satoriはNext.jsの開発元であるVercelがOG画像を生成するために開発・メンテナンスを行なっているライブラリです。
satoriを使うことでHTML/CSSやJSXからSVG画像を生成することができます。

sharp

https://www.npmjs.com/package/sharp
sharpは画像のリサイズや透過等の画像処理を行うことができるライブラリです。
sharpを使うことで、satoriで生成したSVG画像をPNGに変換することができます。

今回の目標

以下のようなURLでリクエストを投げると、入力に応じた画像が返ってきます。
今回は簡単に入力値を表示するだけになりますが、外部からデータ取得などを行うことにより、より高度な画像を生成することも可能です。
http://localhost:3000/image?firstName=inagaki&lastName=tatsuhiro&handleName=inatatsu&job=ApplicationEnginner
image.png

作るもの

名前とハンドルネームそして職業を送ると簡易なネームプレート画像を生成し返却するサーバ

クエリパラメタとして以下の項目を指定することでネームプレート画像が生成されます。

  • firstName
  • lastName
  • handleName
  • job

プログラムの解説

全体としては、シンプルにExpressでリクエストを受けて、画像を返却するだけのサーバとなっています。

プログラム全体
import satori from 'satori'
import sharp from 'sharp'
import fs from 'fs'
import React, { CSSProperties } from 'react'
import express, { Request } from 'express'

const roboto = fs.readFileSync('./fonts/Roboto.ttf')

const app = express()

app.get('/', (req, res) => {
  res.send('hello')
})

const width = 800
const height = 256

const option = {
  width,
  height,
  fonts: [
    {
      name: 'Roboto',
      data: roboto
    }
  ]
}

interface Style {
  [key: string]: CSSProperties
}

const s: Style = {
  root: {
    width: '100%',
    height: '100%',
    display: 'flex',
    justifyContent: 'center',
    alignItems: 'center',
    background:
      'linear-gradient(136deg, #2A74E2 13.85%, #2AA0E2 85.74%), linear-gradient(0deg, #2A74E2 0%, #2A74E2 100%)'
  },
  content: {
    width: `${width - 40}px`,
    height: `${height - 40}px`,
    padding: '32px',
    display: 'flex',
    flexDirection: 'column',
    background: 'white',
    borderRadius: '32px'
  },
  nameWrapper: {
    display: 'flex',
    marginBottom: '24px',
    flexDirection: 'column'
  },
  name: {
    fontSize: '48px'
  },
  handleName: {
    fontSize: '24px'
  },
  other: {
    display: 'flex',
    fontSize: '36px'
  }
}

interface ExReq extends Request {
  query: {
    firstName: string
    lastName: string
    handleName: string
    job: string
  }
}
app.get('/image', async (req: ExReq, res) => {
  const body = (
    <div style={s.root}>
      <div style={s.content}>
        <div style={s.nameWrapper}>
          <div style={s.name}>
            {`${req.query.firstName} ${req.query.lastName}`}
          </div>
          <div style={s.handleName}>{req.query.handleName}</div>
        </div>
        <div style={s.other}>{req.query.job}</div>
      </div>
    </div>
  )

  const svg = await satori(body, option)

  const pngBuffer = await sharp(Buffer.from(svg)).png().toBuffer()
  res.type('png')
  res.send(pngBuffer)
})

app.listen({ port: 3000 }, () => {
  console.log(`Server ready at http://localhost:3000`)
})

export default app

まずは、インポートなど、下準備です。

import satori from 'satori'
import sharp from 'sharp'
import fs from 'fs'
import React, { CSSProperties } from 'react'
import express, { Request } from 'express'

const roboto = fs.readFileSync('./fonts/Roboto.ttf')

とりあえず疎通確認、http://localhost:3000/ にアクセスして「hello」と表示されるとExpressが起動していることが確認できます。

const app = express()

app.get('/', (req, res) => {
  res.send('hello')
})

app.listen({ port: 3000 }, () => {
  console.log(`Server ready at http://localhost:3000`)
})

export default app

satoriで生成する画像のサイズやフォントなどのオプションを定義しています。

const width = 800
const height = 256

const option = {
  width,
  height,
  fonts: [
    {
      name: 'Roboto',
      data: roboto
    }
  ]
}

後ほど、こんな感じでJSXと生成オプションをsatoriに投げるとsvgが生成されます。

const svg = await satori(body, option)

ここはスタイル定義です。
Styleインターフェースを定義し、スタイル情報をobjectで管理できるようにします。flexにしろとよく怒られます。とりあえず困ったらflex突っ込んだらいいです。

Styleインターフェースは、インデックスシグネチャを利用し、すべてのプロパティに対してCSSProperties型を当てはめています。これにより、実際にスタイルを定義する際に、定義するスタイルの種類に関心を持つことなく、全てに型を反映させることができます。

interface Style {
  [key: string]: CSSProperties
}

const s: Style = {
  root: {
    width: '100%',
    height: '100%',
    display: 'flex',
    justifyContent: 'center',
    alignItems: 'center',
    background:
      'linear-gradient(136deg, #2A74E2 13.85%, #2AA0E2 85.74%), linear-gradient(0deg, #2A74E2 0%, #2A74E2 100%)'
  },
  content: {
    width: `${width - 40}px`,
    height: `${height - 40}px`,
    padding: '32px',
    display: 'flex',
    flexDirection: 'column',
    background: 'white',
    borderRadius: '32px'
  },
  nameWrapper: {
    display: 'flex',
    marginBottom: '24px',
    flexDirection: 'column'
  },
  name: {
    fontSize: '48px'
  },
  handleName: {
    fontSize: '24px'
  },
  other: {
    display: 'flex',
    fontSize: '36px'
  }
}

ExpressのRequest型を継承して、queryの型情報を追加し拡張したRequest型であるExReq型を定義しています。

interface ExReq extends Request {
  query: {
    firstName: string
    lastName: string
    handleName: string
    job: string
  }
}

Expressのそれ

app.get('/image', async (req: ExReq, res) => {
  // do something
})

描画する画像をJSXで作成しています。
今回は、queryで指定した、名前等の情報を素直に配置しているだけです。

  const body = (
    <div style={s.root}>
      <div style={s.content}>
        <div style={s.nameWrapper}>
          <div style={s.name}>
            {`${req.query.firstName} ${req.query.lastName}`}
          </div>
          <div style={s.handleName}>{req.query.handleName}</div>
        </div>
        <div style={s.other}>{req.query.job}</div>
      </div>
    </div>
  )

satoriでSVGを生成します。

const svg = await satori(body, option)

sharpを使って、生成したSVGをPNGに変換します。

const pngBuffer = await sharp(Buffer.from(svg)).png().toBuffer()

resに詰め込んでブラウザに返します。

  res.type('png')
  res.send(pngBuffer)

ユースケース

resにキャッシュする時間等の指定を追加し、入力された情報から外部のデータを取得するなどし、リアルタイムなデータを反映させた画像を生成することも可能です。

僕は、自分のブロックチェーンアドレスを入力し、残高を表示した画像をGitHubのプロフィールページに記載するとかやったりしました。

以下の画像は僕が一時期GitHubのプロフィールに設定していたブロックチェーンのアドレスを入れると所持している残高と、持っているその他のトークンの種類数をリアルタイムの情報で表示する画像です。
image.png

また、この技術は動的OGPの生成にもよく使われます。

おわりに

今回は、satoriとsharpで生成した画像をExpressで返却するサーバの作成を行いました。このようにして、OG画像の生成やGitHubのREADMEやプロフィールに動的な画像を埋め込むことができます。

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?