DEV Community

Cover image for Generate Image From HTML Using Satori and Resvg
anasrin
anasrin

Posted on • Updated on • Originally published at anasrar.github.io

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

  • Support JSX syntax.
  • Support image (URL and base64).
  • Automatically wrap text.
  • CSS Flexbox.

Cons

  • Only support JSX and React Node.
  • Explicit inline style.
  • Text baked to path.
  • CSS features not fully implemented.

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

Node.js

Tested on node v19.0.1.

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
Enter fullscreen mode Exit fullscreen mode
// 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);
Enter fullscreen mode Exit fullscreen mode
node main.mjs
Enter fullscreen mode Exit fullscreen mode

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.

mkdir deno-image
cd deno-image
touch main.tsx
Enter fullscreen mode Exit fullscreen mode
// 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);
Enter fullscreen mode Exit fullscreen mode
deno run --unstable --allow-env --allow-ffi --allow-net --allow-read --allow-write main.tsx
Enter fullscreen mode Exit fullscreen mode

Output

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

Edit History

  • [x] 2023-11-13: Replace placeimg with another image provider #50

Top comments (0)