🖼️

HTML/CSS を SVG に変換する Vercel 製のパッケージ「satori」を試してみる

2023/01/02に公開

Next.js では Vercel's Edge Functions を使用して動的に OGP 画像を生成できる @vercel/og という Vercel 製のパッケージが公開されています。

https://www.npmjs.com/package/@vercel/og

この @vercel/og では内部的に satori という HTML/CSS を SVG に変換するパッケージが使用されています。

https://www.npmjs.com/package/satori

面白そうだったので簡単に試してみたメモです。
こういう SVG も簡単に作れます。


バッジっぽいやつ

検証環境

  • Node.js v18.12.1
  • satori v0.0.44

インストール

$ npm i satori
# or
$ yarn add satori

使い方

基本

satori() に SVG に変換したい要素とオプションを渡すだけです。
SVG に変換したい要素は JSX 記法を使うことができるので、 React アプリケーションであれば導入しやすいのではないかと思います。

example.tsx
import fs from "fs";
import satori from "satori";

(async () => {
  // フォントを読み込む
  // ファイルから読み込む例 ( Node.js のみ )
  const fontData = fs.readFileSync("/path/to/Roboto-Regular.ttf");
  // URL から読み込みたいときは `fetch` 等を使う
  const fontData = await fetch("https://example.com/Roboto-Regular.ttf").then((resp) => resp.arrayBuffer());

  // `satori()` で SVG を生成する
  const svg = await satori(
    // 第一引数に SVG に変換したい要素を渡す
    <div style={{ color: "red" }}>hello, world</div>,
    // 第二引数に幅、高さ、フォントなどのオプションを指定する
    {
      width: 600, // 幅
      height: 400, // 高さ
      fonts: [ // フォント
        {
          name: "Roboto",
          data: fontData,
          weight: 400,
          style: "normal",
        },
      ],
    }
  );

  console.log(svg);
})();
出力
<svg width="600" height="400" viewBox="0 0 600 400" xmlns="http://www.w3.org/2000/svg"><path fill="red" d="M2.5 3.5L2.5 8.1Q3.5 6.9 5.0 6.9L5.0 6.9Q7.7 6.9 7.7 9.9L7.7 9.9L7.7 15.5L6.3 15.5L6.3 9.9Q6.3 9.0 5.9 8.5Q5.5 8.1 4.6 8.1L4.6 8.1Q3.9 8.1 3.4 8.5Q2.8 8.9 2.5 9.5L2.5 9.5L2.5 15.5L1.1 15.5L1.1 3.5L2.5 3.5ZM13.4 15.6L13.4 15.6Q11.7 15.6 10.6 14.5Q9.5 13.4 9.5 11.5L9.5 11.5L9.5 11.2Q9.5 10.0 10.0 9.0Q10.5 8.0 11.4 7.4Q12.2 6.9 13.2 6.9L13.2 6.9Q14.9 6.9 15.8 8.0Q16.7 9.0 16.7 11.1L16.7 11.1L16.7 11.7L11.0 11.7Q11.0 12.9 11.7 13.7Q12.4 14.5 13.5 14.5L13.5 14.5Q14.3 14.5 14.8 14.1Q15.3 13.8 15.7 13.3L15.7 13.3L16.6 14.0Q15.5 15.6 13.4 15.6ZM13.2 8.1L13.2 8.1Q12.4 8.1 11.8 8.7Q11.2 9.3 11.0 10.5L11.0 10.5L15.3 10.5L15.3 10.4Q15.2 9.3 14.7 8.7Q14.1 8.1 13.2 8.1ZM20.0 3.5L20.0 15.5L18.5 15.5L18.5 3.5L20.0 3.5ZM23.8 3.5L23.8 15.5L22.4 15.5L22.4 3.5L23.8 3.5ZM25.8 11.3L25.8 11.2Q25.8 9.9 26.3 8.9Q26.7 8.0 27.6 7.4Q28.5 6.9 29.6 6.9L29.6 6.9Q31.3 6.9 32.4 8.1Q33.5 9.3 33.5 11.2L33.5 11.2L33.5 11.4Q33.5 12.6 33.0 13.6Q32.5 14.5 31.6 15.1Q30.8 15.6 29.6 15.6L29.6 15.6Q27.9 15.6 26.8 14.4Q25.8 13.2 25.8 11.3L25.8 11.3ZM27.2 11.4L27.2 11.4Q27.2 12.8 27.9 13.6Q28.5 14.5 29.6 14.5L29.6 14.5Q30.7 14.5 31.4 13.6Q32.0 12.7 32.0 11.2L32.0 11.2Q32.0 9.8 31.4 8.9Q30.7 8.1 29.6 8.1L29.6 8.1Q28.5 8.1 27.9 8.9Q27.2 9.8 27.2 11.4ZM35.2 17.7L35.2 17.7L34.4 17.2Q35.1 16.2 35.2 15.1L35.2 15.1L35.2 13.8L36.6 13.8L36.6 14.9Q36.6 15.7 36.2 16.5Q35.8 17.3 35.2 17.7Z M47.9 7.0L49.9 13.5L51.5 7.0L52.9 7.0L50.5 15.5L49.3 15.5L47.2 9.1L45.2 15.5L44.1 15.5L41.6 7.0L43.1 7.0L44.7 13.4L46.7 7.0L47.9 7.0ZM54.0 11.3L54.0 11.2Q54.0 9.9 54.5 8.9Q55.0 8.0 55.9 7.4Q56.7 6.9 57.9 6.9L57.9 6.9Q59.6 6.9 60.6 8.1Q61.7 9.3 61.7 11.2L61.7 11.2L61.7 11.4Q61.7 12.6 61.2 13.6Q60.8 14.5 59.9 15.1Q59.0 15.6 57.9 15.6L57.9 15.6Q56.1 15.6 55.1 14.4Q54.0 13.2 54.0 11.3L54.0 11.3ZM55.5 11.4L55.5 11.4Q55.5 12.8 56.1 13.6Q56.8 14.5 57.9 14.5L57.9 14.5Q59.0 14.5 59.6 13.6Q60.3 12.7 60.3 11.2L60.3 11.2Q60.3 9.8 59.6 8.9Q58.9 8.1 57.9 8.1L57.9 8.1Q56.8 8.1 56.1 8.9Q55.5 9.8 55.5 11.4ZM67.6 7.0L67.6 8.3Q67.3 8.3 66.9 8.3L66.9 8.3Q65.5 8.3 65.0 9.5L65.0 9.5L65.0 15.5L63.5 15.5L63.5 7.0L64.9 7.0L65.0 8.0Q65.7 6.9 67.0 6.9L67.0 6.9Q67.4 6.9 67.6 7.0L67.6 7.0ZM70.5 3.5L70.5 15.5L69.1 15.5L69.1 3.5L70.5 3.5ZM72.5 11.3L72.5 11.2Q72.5 9.2 73.4 8.1Q74.3 6.9 75.8 6.9L75.8 6.9Q77.3 6.9 78.2 7.9L78.2 7.9L78.2 3.5L79.6 3.5L79.6 15.5L78.3 15.5L78.2 14.6Q77.3 15.6 75.8 15.6L75.8 15.6Q74.3 15.6 73.4 14.4Q72.5 13.2 72.5 11.3L72.5 11.3ZM73.9 11.4L73.9 11.4Q73.9 12.8 74.5 13.6Q75.1 14.4 76.1 14.4L76.1 14.4Q77.5 14.4 78.2 13.2L78.2 13.2L78.2 9.3Q77.5 8.1 76.2 8.1L76.2 8.1Q75.1 8.1 74.5 8.9Q73.9 9.7 73.9 11.4Z "/></svg>


出力された SVG 画像

JSX 記法を使えない場合は代わりに typeprops を持つオブジェクトを渡すこともできます。

example.ts
 import fs from "fs";
 import satori from "satori";

 (async () => {
   // フォントを読み込む
   // ファイルから読み込む例 ( Node.js のみ )
   const fontData = fs.readFileSync("/path/to/Roboto-Regular.ttf");
   // URL から読み込みたいときは `fetch` 等を使う
   const fontData = await fetch("https://example.com/Roboto-Regular.ttf").then((resp) => resp.arrayBuffer());

   // `satori()` で SVG を生成する
   const svg = await satori(
     // 第一引数に SVG に変換したい要素を渡す
-    <div style={{ color: "red" }}>hello, world</div>,
+    {
+      type: "div",
+      props: {
+        children: "hello, world",
+        style: { color: "red" },
+      },
+    },
     // 第二引数に幅、高さ、フォントなどのオプションを指定する
     {
       width: 600, // 幅
       height: 400, // 高さ
       fonts: [ // フォント
         {
           name: "Roboto",
           data: fontData,
           weight: 400,
           style: "normal",
         },
       ],
     }
   );

   console.log(svg);
 })();

複数のフォントを使用する

オプションの fonts にはフォントを複数渡すことができます。
フォントを複数渡すことで文字の太さを使い分けたり複数のフォントを使用することができます。
フォントのフォーマットは現時点で TTF, OTF, WOFF の 3 種類に対応しており、 WOFF2 には対応していません。

example.tsx
import fs from "fs";
import satori from "satori";

(async () => {
  // フォントを複数読み込む
  const fontRegularData = fs.readFileSync("/path/to/Roboto-Regular.ttf");
  const fontBoldData = fs.readFileSync("/path/to/Roboto-Bold.ttf");

  const svg = await satori(
    <div style={{ color: "red", display: "flex", flexDirection: "column" }}>
      <div>Regular</div>
      <div style={{ fontWeight: "bold" }}>Bold</div>
    </div>,
    {
      width: 600,
      height: 400,
      // フォントを複数指定する
      fonts: [
        {
          name: "Roboto",
          data: fontRegularData,
          weight: 400, // font-weight が 400 (normal) のときに使用される
          style: "normal",
        },
        {
          name: "Roboto",
          data: fontBoldData,
          weight: 700, // font-weight が 700 (bold) のときに使用される
          style: "normal",
        },
      ],
    }
  );

  console.log(svg);
})();
出力
<svg width="600" height="400" viewBox="0 0 600 400" xmlns="http://www.w3.org/2000/svg"><path fill="red" d="M8.0 15.5L5.5 10.9L2.8 10.9L2.8 15.5L1.3 15.5L1.3 4.1L5.1 4.1Q7 4.1 8.0 5.0Q9.1 5.9 9.1 7.5L9.1 7.5Q9.1 8.6 8.5 9.4Q7.9 10.2 6.9 10.6L6.9 10.6L9.6 15.4L9.6 15.5L8.0 15.5ZM2.8 5.3L2.8 9.7L5.1 9.7Q6.2 9.7 6.9 9.1Q7.6 8.5 7.6 7.5L7.6 7.5Q7.6 6.5 6.9 5.9Q6.3 5.4 5.1 5.3L5.1 5.3L2.8 5.3ZM14.5 15.6L14.5 15.6Q12.7 15.6 11.7 14.5Q10.6 13.4 10.6 11.5L10.6 11.5L10.6 11.2Q10.6 10.0 11.1 9.0Q11.5 8.0 12.4 7.4Q13.3 6.9 14.3 6.9L14.3 6.9Q15.9 6.9 16.8 8.0Q17.8 9.0 17.8 11.1L17.8 11.1L17.8 11.7L12.0 11.7Q12.1 12.9 12.8 13.7Q13.5 14.5 14.5 14.5L14.5 14.5Q15.3 14.5 15.8 14.1Q16.4 13.8 16.8 13.3L16.8 13.3L17.6 14.0Q16.6 15.6 14.5 15.6ZM14.3 8.1L14.3 8.1Q13.4 8.1 12.8 8.7Q12.2 9.3 12.1 10.5L12.1 10.5L16.3 10.5L16.3 10.4Q16.2 9.3 15.7 8.7Q15.2 8.1 14.3 8.1ZM19.1 11.2L19.1 11.2Q19.1 9.2 20.0 8.0Q20.9 6.9 22.4 6.9L22.4 6.9Q24.0 6.9 24.8 8.0L24.8 8.0L24.9 7.0L26.2 7.0L26.2 15.3Q26.2 16.9 25.2 17.9Q24.3 18.8 22.6 18.8L22.6 18.8Q21.7 18.8 20.8 18.4Q20.0 18.0 19.5 17.4L19.5 17.4L20.3 16.5Q21.2 17.6 22.5 17.6L22.5 17.6Q23.6 17.6 24.2 17.0Q24.8 16.4 24.8 15.4L24.8 15.4L24.8 14.6Q23.9 15.6 22.4 15.6L22.4 15.6Q20.9 15.6 20.0 14.4Q19.1 13.2 19.1 11.2ZM20.5 11.4L20.5 11.4Q20.5 12.8 21.1 13.6Q21.7 14.4 22.8 14.4L22.8 14.4Q24.1 14.4 24.8 13.2L24.8 13.2L24.8 9.3Q24.1 8.1 22.8 8.1L22.8 8.1Q21.7 8.1 21.1 8.9Q20.5 9.7 20.5 11.4ZM33.6 15.5L33.6 14.6Q32.8 15.6 31.1 15.6L31.1 15.6Q29.8 15.6 29.1 14.9Q28.4 14.1 28.4 12.5L28.4 12.5L28.4 7.0L29.8 7.0L29.8 12.5Q29.8 14.4 31.4 14.4L31.4 14.4Q33.0 14.4 33.6 13.2L33.6 13.2L33.6 7.0L35.0 7.0L35.0 15.5L33.6 15.5ZM38.8 3.5L38.8 15.5L37.3 15.5L37.3 3.5L38.8 3.5ZM47.8 15.5L46.3 15.5Q46.2 15.2 46.1 14.6L46.1 14.6Q45.1 15.6 43.7 15.6L43.7 15.6Q42.5 15.6 41.7 14.9Q40.9 14.2 40.9 13.1L40.9 13.1Q40.9 11.8 41.9 11.1Q42.9 10.4 44.7 10.4L44.7 10.4L46.1 10.4L46.1 9.7Q46.1 8.9 45.6 8.5Q45.2 8.0 44.3 8.0L44.3 8.0Q43.5 8.0 43.0 8.4Q42.5 8.8 42.5 9.4L42.5 9.4L41.0 9.4Q41.0 8.7 41.5 8.2Q41.9 7.6 42.7 7.2Q43.5 6.9 44.4 6.9L44.4 6.9Q45.9 6.9 46.7 7.6Q47.5 8.3 47.5 9.6L47.5 9.6L47.5 13.5Q47.5 14.7 47.8 15.4L47.8 15.4L47.8 15.5ZM43.9 14.4L43.9 14.4Q44.6 14.4 45.2 14.0Q45.8 13.7 46.1 13.1L46.1 13.1L46.1 11.4L45.0 11.4Q42.3 11.4 42.3 12.9L42.3 12.9Q42.3 13.6 42.8 14.0Q43.2 14.4 43.9 14.4ZM53.9 7.0L53.9 8.3Q53.6 8.3 53.2 8.3L53.2 8.3Q51.8 8.3 51.3 9.5L51.3 9.5L51.3 15.5L49.8 15.5L49.8 7.0L51.2 7.0L51.2 8.0Q51.9 6.9 53.3 6.9L53.3 6.9Q53.7 6.9 53.9 7.0L53.9 7.0Z "/><path fill="red" d="M5.4 34.5L1.0 34.5L1.0 23.1L5 23.1Q7.1 23.1 8.1 23.9Q9.2 24.7 9.2 26.2L9.2 26.2Q9.2 27.1 8.8 27.7Q8.4 28.3 7.6 28.6L7.6 28.6Q8.5 28.9 9.0 29.5Q9.5 30.2 9.5 31.1L9.5 31.1Q9.5 32.8 8.4 33.6Q7.4 34.5 5.4 34.5L5.4 34.5ZM5.5 29.5L3.4 29.5L3.4 32.6L5.4 32.6Q6.2 32.6 6.7 32.2Q7.1 31.8 7.1 31.1L7.1 31.1Q7.1 29.6 5.5 29.5L5.5 29.5ZM3.4 25.0L3.4 27.9L5.1 27.9Q6.9 27.8 6.9 26.5L6.9 26.5Q6.9 25.7 6.4 25.3Q6.0 25.0 5 25.0L5 25.0L3.4 25.0ZM10.7 30.3L10.7 30.2Q10.7 28.9 11.2 27.9Q11.7 27.0 12.6 26.4Q13.5 25.9 14.7 25.9L14.7 25.9Q16.4 25.9 17.5 26.9Q18.6 28.0 18.7 29.8L18.7 29.8L18.7 30.3Q18.7 32.3 17.6 33.5Q16.6 34.6 14.7 34.6Q12.9 34.6 11.8 33.5Q10.7 32.3 10.7 30.3L10.7 30.3ZM13.0 30.3L13.0 30.3Q13.0 31.5 13.4 32.2Q13.9 32.8 14.7 32.8L14.7 32.8Q15.6 32.8 16.0 32.2Q16.5 31.6 16.5 30.2L16.5 30.2Q16.5 29.0 16.0 28.4Q15.6 27.7 14.7 27.7L14.7 27.7Q13.9 27.7 13.4 28.3Q13.0 29.0 13.0 30.3ZM22.5 22.5L22.5 34.5L20.2 34.5L20.2 22.5L22.5 22.5ZM24.0 30.2L24.0 30.2Q24.0 28.2 24.9 27.0Q25.8 25.9 27.3 25.9L27.3 25.9Q28.6 25.9 29.4 26.8L29.4 26.8L29.4 22.5L31.6 22.5L31.6 34.5L29.6 34.5L29.5 33.6Q28.6 34.6 27.3 34.6L27.3 34.6Q25.8 34.6 24.9 33.5Q24.0 32.3 24.0 30.2ZM26.3 30.4L26.3 30.4Q26.3 31.5 26.7 32.2Q27.1 32.8 27.9 32.8L27.9 32.8Q28.9 32.8 29.4 31.9L29.4 31.9L29.4 28.6Q28.9 27.7 27.9 27.7L27.9 27.7Q26.3 27.7 26.3 30.4Z "/></svg>


出力された SVG 画像

画像の埋め込み

<img /> 要素を使って画像を埋め込むこともできます。
src が URL の場合、画像は SVG 生成時に取得されて DataURL に変換されます。
そのため、もしも使用する画像が決まっているのであれば src に直接 DataURL を設定した方が余計な I/O が発生しません。
また、 widthheight は必須ではありませんが設定することが推奨されています。

example.tsx
const svg = await satori(
  <img src="https://picsum.photos/200/300" width={200} height={300} />,
  // ...省略
);

対応している HTML/CSS

当たり前ですが、全ての HTML/CSS に完全に対応しているわけではありません ( とはいえかなり広い範囲をカバーしていますが ) 。
対応している HTML/CSS につきましては下記をご参照ください。

バッジっぽいの作ってみた

よくあるやつ雑に作ってみました。

badge.tsx
import fs from "fs";
import satori from "satori";

(async () => {
  const fontData = fs.readFileSync("/path/to/Roboto-Regular.ttf");

  const svg = await satori(
    <div
      style={{
        borderRadius: "4px",
        color: "#fff",
        display: "flex",
        overflow: "hidden",
      }}
    >
      <div style={{ backgroundColor: "#000", padding: "4px 8px" }}>label</div>
      <div style={{ backgroundColor: "#6fc93f", padding: "4px 8px" }}>message</div>
    </div>,
    {
      height: 27,
      fonts: [
        {
          name: "Roboto",
          data: fontData,
          weight: 400,
          style: "normal",
        },
      ],
    }
  );

  console.log(svg);
})();
出力
<svg width="131" height="27" viewBox="0 0 131 27" xmlns="http://www.w3.org/2000/svg"><clipPath id="satori_cp-id"><path x="0" y="0" width="131" height="27" d="M4,0 h123 a4,4 0 0 1 4,4 v19 a4,4 0 0 1 -4,4 h-123 a4,4 0 0 1 -4,-4 v-19 a4,4 0 0 1 4,-4"/></clipPath><mask id="satori_om-id"><rect x="0" y="0" width="131" height="27" fill="#fff"/></mask><rect x="0" y="0" width="50" height="27" fill="#000" clip-path="url(#satori_cp-id)" mask="url(#satori_om-id)"/><path fill="#fff" d="M10.7 7.5L10.7 19.5L9.2 19.5L9.2 7.5L10.7 7.5ZM19.7 19.5L18.2 19.5Q18.1 19.2 18.0 18.6L18.0 18.6Q17.0 19.6 15.6 19.6L15.6 19.6Q14.3 19.6 13.5 18.9Q12.7 18.2 12.7 17.1L12.7 17.1Q12.7 15.8 13.7 15.1Q14.7 14.4 16.6 14.4L16.6 14.4L18.0 14.4L18.0 13.7Q18.0 12.9 17.5 12.5Q17.1 12.0 16.2 12.0L16.2 12.0Q15.4 12.0 14.9 12.4Q14.4 12.8 14.4 13.4L14.4 13.4L12.9 13.4Q12.9 12.7 13.4 12.2Q13.8 11.6 14.6 11.2Q15.3 10.9 16.3 10.9L16.3 10.9Q17.7 10.9 18.6 11.6Q19.4 12.3 19.4 13.6L19.4 13.6L19.4 17.5Q19.4 18.7 19.7 19.4L19.7 19.4L19.7 19.5ZM15.8 18.4L15.8 18.4Q16.5 18.4 17.1 18.0Q17.7 17.7 18.0 17.1L18.0 17.1L18.0 15.4L16.8 15.4Q14.2 15.4 14.2 16.9L14.2 16.9Q14.2 17.6 14.6 18.0Q15.1 18.4 15.8 18.4ZM28.8 15.2L28.8 15.4Q28.8 17.3 27.9 18.5Q27.1 19.6 25.6 19.6L25.6 19.6Q24.0 19.6 23.1 18.5L23.1 18.5L23.0 19.5L21.7 19.5L21.7 7.5L23.1 7.5L23.1 12.0Q24 10.9 25.5 10.9Q27.1 10.9 28.0 12.0Q28.8 13.2 28.8 15.2L28.8 15.2ZM27.4 15.2L27.4 15.2Q27.4 13.7 26.8 12.9Q26.3 12.1 25.2 12.1L25.2 12.1Q23.8 12.1 23.1 13.4L23.1 13.4L23.1 17.1Q23.8 18.4 25.2 18.4L25.2 18.4Q26.2 18.4 26.8 17.6Q27.4 16.8 27.4 15.2ZM34.2 19.6L34.2 19.6Q32.4 19.6 31.4 18.5Q30.3 17.4 30.3 15.5L30.3 15.5L30.3 15.2Q30.3 14.0 30.8 13.0Q31.3 12.0 32.1 11.4Q33.0 10.9 34.0 10.9L34.0 10.9Q35.6 10.9 36.5 12.0Q37.5 13.0 37.5 15.1L37.5 15.1L37.5 15.7L31.7 15.7Q31.8 16.9 32.5 17.7Q33.2 18.5 34.2 18.5L34.2 18.5Q35.0 18.5 35.5 18.1Q36.1 17.8 36.5 17.3L36.5 17.3L37.4 18.0Q36.3 19.6 34.2 19.6ZM34.0 12.1L34.0 12.1Q33.1 12.1 32.5 12.7Q31.9 13.3 31.8 14.5L31.8 14.5L36.0 14.5L36.0 14.4Q36.0 13.3 35.4 12.7Q34.9 12.1 34.0 12.1ZM40.7 7.5L40.7 19.5L39.3 19.5L39.3 7.5L40.7 7.5Z " clip-path="url(#satori_cp-id)" mask="url(#satori_om-id)"/><rect x="50" y="0" width="81" height="27" fill="#6fc93f" clip-path="url(#satori_cp-id)" mask="url(#satori_om-id)"/><path fill="#fff" d="M59.1 11.0L60.5 11.0L60.5 12.0Q61.4 10.9 63 10.9L63 10.9Q64.8 10.9 65.4 12.2L65.4 12.2Q65.8 11.6 66.5 11.2Q67.2 10.9 68.1 10.9L68.1 10.9Q70.9 10.9 70.9 13.8L70.9 13.8L70.9 19.5L69.5 19.5L69.5 13.9Q69.5 13.0 69.1 12.5Q68.7 12.1 67.7 12.1L67.7 12.1Q66.9 12.1 66.4 12.6Q65.8 13.1 65.7 13.9L65.7 13.9L65.7 19.5L64.3 19.5L64.3 13.9Q64.3 12.1 62.5 12.1L62.5 12.1Q61.1 12.1 60.5 13.3L60.5 13.3L60.5 19.5L59.1 19.5L59.1 11.0ZM76.6 19.6L76.6 19.6Q74.9 19.6 73.8 18.5Q72.8 17.4 72.8 15.5L72.8 15.5L72.8 15.2Q72.8 14.0 73.2 13.0Q73.7 12.0 74.6 11.4Q75.4 10.9 76.4 10.9L76.4 10.9Q78.1 10.9 79.0 12.0Q79.9 13.0 79.9 15.1L79.9 15.1L79.9 15.7L74.2 15.7Q74.2 16.9 74.9 17.7Q75.6 18.5 76.7 18.5L76.7 18.5Q77.5 18.5 78 18.1Q78.5 17.8 78.9 17.3L78.9 17.3L79.8 18.0Q78.8 19.6 76.6 19.6ZM76.4 12.1L76.4 12.1Q75.6 12.1 75.0 12.7Q74.4 13.3 74.2 14.5L74.2 14.5L78.5 14.5L78.5 14.4Q78.4 13.3 77.9 12.7Q77.4 12.1 76.4 12.1ZM86.5 17.2L86.5 17.2Q86.5 16.7 86.1 16.3Q85.6 16.0 84.5 15.8Q83.4 15.5 82.8 15.2Q82.1 14.9 81.8 14.4Q81.5 14.0 81.5 13.4L81.5 13.4Q81.5 12.3 82.4 11.6Q83.3 10.9 84.7 10.9L84.7 10.9Q86.1 10.9 87.0 11.6Q87.9 12.4 87.9 13.5L87.9 13.5L86.4 13.5Q86.4 12.9 85.9 12.5Q85.4 12.1 84.7 12.1L84.7 12.1Q83.9 12.1 83.4 12.4Q83.0 12.7 83.0 13.3L83.0 13.3Q83.0 13.8 83.4 14.1Q83.8 14.4 84.9 14.6Q86.0 14.8 86.6 15.2Q87.3 15.5 87.6 16.0Q88.0 16.5 88.0 17.1L88.0 17.1Q88.0 18.3 87.1 19.0Q86.1 19.6 84.7 19.6L84.7 19.6Q83.7 19.6 82.9 19.3Q82.1 18.9 81.7 18.3Q81.2 17.6 81.2 16.9L81.2 16.9L82.7 16.9Q82.7 17.6 83.3 18.0Q83.8 18.5 84.7 18.5L84.7 18.5Q85.5 18.5 86.0 18.1Q86.5 17.8 86.5 17.2ZM94.8 17.2L94.8 17.2Q94.8 16.7 94.3 16.3Q93.9 16.0 92.8 15.8Q91.7 15.5 91.0 15.2Q90.4 14.9 90.1 14.4Q89.8 14.0 89.8 13.4L89.8 13.4Q89.8 12.3 90.7 11.6Q91.5 10.9 92.9 10.9L92.9 10.9Q94.3 10.9 95.2 11.6Q96.1 12.4 96.1 13.5L96.1 13.5L94.7 13.5Q94.7 12.9 94.2 12.5Q93.7 12.1 92.9 12.1L92.9 12.1Q92.1 12.1 91.7 12.4Q91.2 12.7 91.2 13.3L91.2 13.3Q91.2 13.8 91.6 14.1Q92.1 14.4 93.1 14.6Q94.2 14.8 94.9 15.2Q95.6 15.5 95.9 16.0Q96.2 16.5 96.2 17.1L96.2 17.1Q96.2 18.3 95.3 19.0Q94.4 19.6 93.0 19.6L93.0 19.6Q91.9 19.6 91.2 19.3Q90.4 18.9 89.9 18.3Q89.5 17.6 89.5 16.9L89.5 16.9L90.9 16.9Q91.0 17.6 91.5 18.0Q92.1 18.5 93.0 18.5L93.0 18.5Q93.8 18.5 94.3 18.1Q94.8 17.8 94.8 17.2ZM104.8 19.5L103.3 19.5Q103.2 19.2 103.1 18.6L103.1 18.6Q102.1 19.6 100.7 19.6L100.7 19.6Q99.5 19.6 98.7 18.9Q97.9 18.2 97.9 17.1L97.9 17.1Q97.9 15.8 98.9 15.1Q99.9 14.4 101.7 14.4L101.7 14.4L103.1 14.4L103.1 13.7Q103.1 12.9 102.6 12.5Q102.2 12.0 101.3 12.0L101.3 12.0Q100.5 12.0 100 12.4Q99.5 12.8 99.5 13.4L99.5 13.4L98.0 13.4Q98.0 12.7 98.5 12.2Q98.9 11.6 99.7 11.2Q100.5 10.9 101.4 10.9L101.4 10.9Q102.8 10.9 103.7 11.6Q104.5 12.3 104.5 13.6L104.5 13.6L104.5 17.5Q104.5 18.7 104.8 19.4L104.8 19.4L104.8 19.5ZM100.9 18.4L100.9 18.4Q101.6 18.4 102.2 18.0Q102.8 17.7 103.1 17.1L103.1 17.1L103.1 15.4L102.0 15.4Q99.3 15.4 99.3 16.9L99.3 16.9Q99.3 17.6 99.8 18.0Q100.2 18.4 100.9 18.4ZM106.5 15.2L106.5 15.2Q106.5 13.2 107.4 12.0Q108.3 10.9 109.8 10.9L109.8 10.9Q111.3 10.9 112.2 12.0L112.2 12.0L112.3 11.0L113.6 11.0L113.6 19.3Q113.6 20.9 112.6 21.9Q111.6 22.8 110.0 22.8L110.0 22.8Q109.1 22.8 108.2 22.4Q107.3 22.0 106.9 21.4L106.9 21.4L107.6 20.5Q108.6 21.6 109.9 21.6L109.9 21.6Q111.0 21.6 111.6 21.0Q112.1 20.4 112.1 19.4L112.1 19.4L112.1 18.6Q111.3 19.6 109.8 19.6L109.8 19.6Q108.3 19.6 107.4 18.4Q106.5 17.2 106.5 15.2ZM107.9 15.4L107.9 15.4Q107.9 16.8 108.5 17.6Q109.1 18.4 110.1 18.4L110.1 18.4Q111.5 18.4 112.1 17.2L112.1 17.2L112.1 13.3Q111.5 12.1 110.1 12.1L110.1 12.1Q109.1 12.1 108.5 12.9Q107.9 13.7 107.9 15.4ZM119.3 19.6L119.3 19.6Q117.6 19.6 116.5 18.5Q115.4 17.4 115.4 15.5L115.4 15.5L115.4 15.2Q115.4 14.0 115.9 13.0Q116.4 12.0 117.2 11.4Q118.1 10.9 119.1 10.9L119.1 10.9Q120.8 10.9 121.7 12.0Q122.6 13.0 122.6 15.1L122.6 15.1L122.6 15.7L116.9 15.7Q116.9 16.9 117.6 17.7Q118.3 18.5 119.4 18.5L119.4 18.5Q120.1 18.5 120.7 18.1Q121.2 17.8 121.6 17.3L121.6 17.3L122.5 18.0Q121.4 19.6 119.3 19.6ZM119.1 12.1L119.1 12.1Q118.2 12.1 117.6 12.7Q117.0 13.3 116.9 14.5L116.9 14.5L121.1 14.5L121.1 14.4Q121.1 13.3 120.5 12.7Q120.0 12.1 119.1 12.1Z " clip-path="url(#satori_cp-id)" mask="url(#satori_om-id)"/></svg>


出力された SVG 画像

その他

satori は他にも様々なことができます。

詳しくは公式の README をご参照ください。

まとめ

Shields.ioGitHub Readme Stats のような動的に SVG 生成する機能を簡単に実装できそうですね。

Discussion