pr_01m47d24b0e6n91zwymwxg0vpx/services/og/src/render.ts

50 lines1,587 bytesCodeBlame
1/**
2 * Turns a card into a PNG: satori lays it out as SVG, with the text as
3 * outlines, and resvg rasterises that. Both run as WebAssembly, which the
4 * caller hands in: as modules in the Worker, as bytes in Node.
5 */
6import { Resvg, initWasm } from "@resvg/resvg-wasm";
7import satori, { init as initYoga, type Font } from "satori/standalone";
8
9import { HEIGHT, WIDTH, cardTree } from "./card.ts";
10import type { Card } from "./resolve.ts";
11
12export type Assets = {
13 yoga: WebAssembly.Module | ArrayBuffer | Uint8Array;
14 resvg: WebAssembly.Module | ArrayBuffer | Uint8Array;
15 fonts: Font[];
16};
17
18let ready: Promise<void> | null = null;
19
20function start(assets: Assets): Promise<void> {
21 ready ??= Promise.all([initYoga(assets.yoga), initWasm(assets.resvg)]).then(
22 () => undefined,
23 (error) => {
24 ready = null;
25 throw error;
26 },
27 );
28 return ready;
29}
30
31/** The card as SVG, for tests and for looking at without rasterising. */
32export async function cardSvg(card: Card, assets: Assets): Promise<string> {
33 await start(assets);
34 // Satori takes React elements; the tree is the same shape without React.
35 return satori(cardTree(card) as unknown as Parameters<typeof satori>[0], {
36 width: WIDTH,
37 height: HEIGHT,
38 fonts: assets.fonts,
39 });
40}
41
42export async function cardPng(card: Card, assets: Assets): Promise<Uint8Array> {
43 const svg = await cardSvg(card, assets);
44 const resvg = new Resvg(svg, { fitTo: { mode: "width", value: WIDTH }, font: { loadSystemFonts: false } });
45 try {
46 return resvg.render().asPng();
47 } finally {
48 resvg.free();
49 }
50}