Table of Contents

<--   Back

Generate Image From HTML Using Satori and Resvg


TL;DR

Generate image from HTML using github.com/vercel/satori (to convert HTML to SVG) and github.com/yisibl/resvg-js (to convert SVG to PNG) on Node.js and Deno.

Example: github.com/anasrar/satori-resvg.

Background

When Vercel Introducing OG Image Generation: Fast, dynamic social card images at the Edge, and play around with @vercel/og and realize it only work with Edge Runtime and I want to create script that generate fully static image.

Vercel explain technical details behind @vercel/og, that it using github.com/vercel/satori and github.com/RazrFalcon/resvg.

Now let’s create from scratch using Node.js and Deno.

Dependencies

Satori

Satori convert HTML to SVG, automatically wrap text and using Yoga Layout under the hood.

Pros

Cons

Satori-html

Satori-html convert HTML to React Node, this is because Satori only support React node object at least on Node.js.

Resvg-js

Resvg-js convert SVG to PNG, Rust-Node binding for github.com/RazrFalcon/resvg.

Victor Mono

Victor mono is font that we will use because Satori need at least 1 font as default font.

Flow

FLow HTML HTML Satori Satori HTML->Satori Resvg Resvg Satori->Resvg PNG PNG Resvg->PNG

Node.js

Tested on node v19.0.1.

Commands
mkdir node-image
cd node-image
pnpm init
pnpm add satori@0.0.44 satori-html@0.3.2 @resvg/resvg-js@2.2.0
touch main.mjs
main.mjs
import { readFile, writeFile } from "node:fs/promises";
import { html } from "satori-html";
import satori from "satori";
import { Resvg } from "@resvg/resvg-js";
 
const template = html(`
<div style="display: flex; flex-flow: column nowrap; align-items: stretch; width: 600px; height: 400px; backgroundImage: linear-gradient(to right, #0f0c29, #302b63, #24243e); color: #000;">
  <div style="display: flex; flex: 1 0; flex-flow: row nowrap; justify-content: center; align-items: center;">
    <img style="border: 8px solid rgba(255, 255, 255, 0.2); border-radius: 50%;" src="https://placekitten.com/240/240" alt="animals" />
  </div>
  <div style="display: flex; justify-content: center; align-items: center; margin: 6px; padding: 12px; border-radius: 4px; background: rgba(255, 255, 255, 0.2); color: #fff; font-size: 22px;">
    The quick brown fox jumps over the lazy dog.
  </div>
</div>
`);
 
// convert html to svg
const svg = await satori(template, {
  width: 600,
  height: 400,
  fonts: [
    {
      name: "VictorMono",
      data: await readFile("./VictorMono-Bold.ttf"),
      weight: 700,
      style: "normal",
    },
  ],
});
 
// render png
const resvg = new Resvg(svg, {
  background: "rgba(238, 235, 230, .9)",
});
const pngData = resvg.render();
const pngBuffer = pngData.asPng();
 
await writeFile("./output.png", pngBuffer);
Commands
node main.mjs

Output

node output with cat the middle and caption the quick brown fox jumps over the lazy dog

Deno

Tested on deno 1.29.1.

Commands
mkdir deno-image
cd deno-image
touch main.tsx
main.tsx
import React from "https://esm.sh/react@18.2.0";
import satori, { init } from "npm:satori@0.0.44/wasm";
import initYoga from "npm:yoga-wasm-web@0.2.0";
import { Resvg } from "npm:@resvg/resvg-js@2.2.0";
import cacheDir from "https://deno.land/x/cache_dir@0.2.0/mod.ts";
 
const wasm = await Deno.readFile(
  `${cacheDir()}/deno/npm/registry.npmjs.org/yoga-wasm-web/0.2.0/dist/yoga.wasm`,
);
const yoga =
  await (initYoga as unknown as (wasm: Uint8Array) => Promise<unknown>)(wasm);
init(yoga);
 
const template = (
  <div
    style={{
      display: "flex",
      flexFlow: "column nowrap",
      alignItems: "stretch",
      width: "600px",
      height: "400px",
      backgroundImage: "linear-gradient(to top, #7028e4 0%, #e5b2ca 100%)",
      color: "#000",
    }}
  >
    <div
      style={{
        display: "flex",
        flex: "1 0",
        flexFlow: "row nowrap",
        justifyContent: "center",
        alignItems: "center",
      }}
    >
      <img
        style={{
          border: "8px solid rgba(255, 255, 255, 0.2)",
          borderRadius: "50%",
        }}
        src="https://placekitten.com/240/240"
        alt="animals"
      />
    </div>
    <div
      style={{
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        margin: "6px",
        padding: "12px",
        borderRadius: "4px",
        background: "rgba(255, 255, 255, 0.2)",
        color: "#fff",
        fontSize: "22px",
      }}
    >
      The quick brown fox jumps over the lazy dog.
    </div>
  </div>
);
 
// convert html to svg
const svg = await satori(
  template,
  {
    width: 600,
    height: 400,
    fonts: [
      {
        name: "VictorMono",
        data: await Deno.readFile("./VictorMono-Bold.ttf"),
        weight: 700,
        style: "normal",
      },
    ],
  },
);
 
// render png
const resvg = new Resvg(svg, {
  background: "rgba(238, 235, 230, .9)",
});
const pngData = resvg.render();
const pngBuffer = pngData.asPng();
 
await Deno.writeFile("./output.png", pngBuffer);
 
// ffi block, need to force exit
Deno.exit(0);
Commands
deno run --unstable --allow-env --allow-ffi --allow-net --allow-read --allow-write main.tsx

Output

deno output with cat the middle and caption the quick brown fox jumps over the lazy dog

Edit History


Open GitHub Discussions