Cloud FunctionとSVGでOGP画像生成を試行錯誤したまとめ

Nuxt+Firebaseで開発してるサービスのOGP画像を改善しようと、
いろいろ試してみたときの備忘録。

OGP画像の生成はクライアント側とサーバ側かがあるが、
Firestoreの変更に合わせて生成したいので、
Cloud Function上で利用できる方法を考えてた。

結果的に、

  1. sharpで画像を合成してベースをつくり
  2. ImageMagickで文字を追記していく

という構成になった。その試行錯誤の備忘録です。

やりたかったこと

背景画像+本の画像+文字みたいなOGP画像にしたい。

tsundoku_book_start_4873115655.png

あと、座標の指定はめんどくさいので、楽な方法を探してみた。
SVGだとCSSも使えて、ブラウザ上で書くにできるので良さそう。
(と思ったけど、結果、だめだった...)

ためした方法とまとめ

試したのは以下の4パターン。
SVG+sharpで試した感じ。
node-canvasはおまけな感じ程度。

  1. sharp+SVG(style+foreginObject)
    • SVGですべて構成する方法。
    • フォントとか位置、サイズはstyleで設定
    • 外部URLの画像が取得できずにNG
  2. sharp+SVG+画像は別で合成
    • 1.の画像部分を別で合成するパターン
    • カスタムフォントが設定できずNG
  3. sharp+SVG+ImageMagick
    • 画像の合成部分のみsharpをつかい
    • 枠線をSVG、文字はImageMagickで書き出し
    • これを採用
  4. node-canvas+SVG
    • node-canvasもSVGを入力にできるようなので試した
    • 1.や2.と同様の理由でNG

内部で利用しているSVGライブラリ自体で未対応っぽいので、
SVGのstyleや外部URLの画像、カスタムフォントなどは、
ブラウザ上以外の画像生成ではまだ未対応っぽい。

ほかにも、CloudFunction上でpuppeteerを使えるようだけど、
メモリをすごい使うっぽい記事を見てしまい、まだためしてない。。


以下試したコード。

SVG(style+foreginObject)

  • sharpが、foreignObject+sytleに未対応...のためNG
  • imageに外部URLを使っているとダメっぽい...
import sharp from "sharp";

await sharp("./input.svg")
  .png()
  .toFile("./output.png");
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" 
  xmlns:xlink="http://www.w3.org/1999/xlink" viewbox="0 0 1200 630" width="1200" height="630">
  <style>
        @import url("https://fonts.googleapis.com/css?family=Noto+Sans+JP:500&amp;display=swap&amp;subset=japanese");

        .contents {
          display: flex;
          justify-content: center;
          align-items: flex-end;
          width: 1200px;
          height: 630px;

          position: relative;
        }

        .book {
          height: calc(100% - 100px);
        }
        .text-contents {
          position: absolute;
          left: 0;
          right: 0;
          bottom: 58px;
          width: 1200px;

          background-color: rgba(0, 0, 0, 0.6);
          font-family: "Noto Sans JP", sans-serif;
          color: white;
          padding-bottom: 8px;
          text-align: center;
        }
        .text-label {
          font-size: 60px;
          font-weight: 500;
          padding-left: 0.5em;
        }
        .title-label {
          font-size: 28px;
          font-weight: 500;
          padding-top: 0.2em;
        }
  </style>
  <image xlink:href="https://.../background.png" width="1200" height="630" />
  <foreignObject requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" width="1200" height="630">
    <div class="contents">
      <img class="book" src="https://.../thumbnail.png" />
      <div class="text-contents">
        <div class="text-label">
          <span>読みます!!</span>
        </div>
        <div class="title-label">
          <span>リーダブルコード</span>
        </div>
        <div class="title-label">
          <span>ダスティン・ボズウェル/トレバー・フォシェ</span>
        </div>
      </div>
    </div>
  </foreignObject>
</svg>

SVG+外部URLの画像

  • SVGには文字とかだけにして、画像はファイル読み込みで対応
  • カスタムフォントが使えない...のNG
  • GAEかlamdaだと、fontconfigに関する環境変数を設定できるが、Cloud Functionでは無理そう
  • fonts.confを設定してもダメだった...
import axios from "axios";
import sharp from "sharp";

// 埋め込む画像のURL
const bookURL = "https://.../thumbnail.png";
const bookBuffer = await axios.get(bookURL, { responseType: "arraybuffer" });

// URLから取得した画像を加工(リサイズ)
const bookImage = await sharp(bookBuffer.data)
  .resize(520, 454, { position: "top" })
  .toBuffer();

// sharpで結合
await sharp("./background.png") // 背景画像を読み込み
  .composite([
    { input: bookImage, gravity: "south" }, 本の画像を書き出し
    { input: "./input.svg" } // SVGの書き出し
  ])
  .png()
  .toFile(tempFile);
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" 
  xmlns:xlink="http://www.w3.org/1999/xlink" viewbox="0 0 1200 630" width="1200" height="630">
  <defs>
    <!-- Fontを外部URLで指定 -->
    <font-face font-family="Noto Sans JP">
      <font-face-src>
        <font-face-uri xlink:href="https://fonts.googleapis.com/css?family=Noto+Sans+JP:400,700|Roboto:400,700&amp;display=swap&amp;subset=japanese" />
      </font-face-src>
    </font-face>

    <!-- フォントをローカルファイルで指定 -->
    <font-face font-family="Noto Sans JP" font-weight="500">
      <font-face-src>
        <font-face-uri xlink:href="./font/NotoSansJP-Medium.otf">
          <font-face-format string="opentype"/>
        </font-face-uri>
      </font-face-src>
    </font-face>
    <font-face font-family="Noto Sans JP" font-weight="700">
      <font-face-src>
        <font-face-uri xlink:href="./font/NotoSansJP-Bold.otf">
          <font-face-format string="opentype"/>
        </font-face-uri>
      </font-face-src>
    </font-face>

    <!-- ローカルファイルを@font-faceで指定 -->
    <style type="text/css">
        @font-face {
          font-family: 'Noto Sans JP';
          font-style: normal;
          font-weight: 500;
          src: url('./font/NotoSansJP-Medium.otf') format("opentype");
        }
        @font-face {
          font-family: 'Noto Sans JP';
          font-style: normal;
          font-weight: 700;
          src: url('./font/NotoSansJP-Bold.otf') format("opentype");
        }
    </style>
  </defs>
  <rect x="0" y="530" width="1200" height="100" fill="#000000" fill-opacity="0.6" />
  <text x="610" y="156" font-size="60" fill="#FFFFFF" text-anchor="middle">
    <tspan font-weight="700">読みます!!</tspan>
  </text>
  <text x="600" y="574" font-size="28" fill="#FFFFFF" text-anchor="middle">
    <tspan font-weight="700">リーダブルコード</tspan>
  </text>
  <text x="600" y="610" font-size="24" fill="#FFFFFF" text-anchor="middle">
    <tspan font-weight="500">ダスティン・ボズウェル/トレバー・フォシェ</tspan>
  </text>
</svg>

SVG+ImageMagick

node-canvas+SVG

こんなのつくってます!!

積読用の読書管理アプリ 『積読ハウマッチ』をリリースしました!
積読ハウマッチは、Nuxt.js+Firebaseで開発してます!

もしよかったら、遊んでみてくださいヽ(=´▽`=)ノ

要望・感想・アドバイスなどあれば、
公式アカウント(@MemoryLoverz)や開発者(@kira_puka)まで♪

参考にしたサイト様

Originally published at www.memory-lovers.blog
ツイッターでシェア
みんなに共有、忘れないようにメモ

きらぷか@i18n補助ツール『トランスノート』開発者

フリーエンジニア/今はNuxt.js/いつかFlutter 受託&アプリ/Webサービス/ゲームを #個人開発 CS修士→SIer/R&D→フリー #paiza はAランクで満足/AtCoderしたい 仕事依頼やご相談はDMまで Kotlin/Python/Swift/Unity/Java/Haskell/DDD

Crieitは個人で開発中です。 興味がある方は是非記事の投稿をお願いします! どんな軽い内容でも嬉しいです。
なぜCrieitを作ろうと思ったか

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

有料記事を販売できるようになりました!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください!
ボードとは?

関連記事

コメント