| 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 | */ |
| 6 | import { Resvg, initWasm } from "@resvg/resvg-wasm"; |
| 7 | import satori, { init as initYoga, type Font } from "satori/standalone"; |
| 8 | |
| 9 | import { HEIGHT, WIDTH, cardTree } from "./card.ts"; |
| 10 | import type { Card } from "./resolve.ts"; |
| 11 | |
| 12 | export type Assets = { |
| 13 | yoga: WebAssembly.Module | ArrayBuffer | Uint8Array; |
| 14 | resvg: WebAssembly.Module | ArrayBuffer | Uint8Array; |
| 15 | fonts: Font[]; |
| 16 | }; |
| 17 | |
| 18 | let ready: Promise<void> | null = null; |
| 19 | |
| 20 | function 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. */ |
| 32 | export 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 | |
| 42 | export 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 | } |