Open Graph image maker

Create social media images locally in your browser. Share the link. Nothing is uploaded.

Open Graph image preview rendered with Inter Display
Inter Display
Open Graph image preview rendered with DejaVu Sans Mono
DejaVu Sans Mono

Download #

JavaScript #

This ES module uses static WebAssembly instance imports and includes SVG, PNG, and WebP output. Remove imports and output calls that your application does not use.

// Both font components accept application/x-www-form-urlencoded. The `title`
// field must be present but may be empty; `subtitle` is optional. Input is
// limited to 4 KiB, each field to 2 KiB, and each field to 16 wrapped lines.
//
// The renderers support Latin-1 from U+0020 through U+00FF, except soft hyphen
// U+00AD. They do not provide bidirectional layout, hyphenation, or font
// fallback. Inter is proportional and applies pair kerning. DejaVu is monospaced.
//
// Output is a self-contained 1200x630 SVG made from paths. The viewer does not
// need the font. Choose one font component:
import * as ogImageRenderer from "./text-to-og-image-svg-inter.wasm";
// import * as ogImageRenderer from "./text-to-og-image-svg-dejavu-sans-mono.wasm";

// The SVG rasterizer produces a BGRA BMP. The other two components encode that
// BMP as PNG or lossless WebP. Remove imports and calls for formats you do not use.
import * as svgRasterizer from "./svg-rasterize-to-bmp-b8g8r8a8-srgb.wasm";
import * as pngEncoder from "./bmp-to-png.wasm";
// This lossless WebP encoder reserves 1.5 GiB of Wasm memory when imported.
import * as webpEncoder from "./bmp-b8g8r8a8-srgb-to-webp-lossless.wasm";

const encoder = new TextEncoder();
const decoder = new TextDecoder("utf-8", { fatal: true });

export function renderOGImageToSVG({
  title = "",
  subtitle = "",
  textColor = 0xffffffff,
  backgroundColor = 0x4b2e83ff,
  fontMaxSize = 112,
  fontWeight = 700,
} = {}) {
  const form = new URLSearchParams({ title, subtitle }).toString();
  const input = new Uint8Array(
    ogImageRenderer.memory.buffer,
    ogImageRenderer.input_ptr(),
    ogImageRenderer.input_utf8_cap(),
  );
  const { read, written } = encoder.encodeInto(form, input);
  if (read !== form.length) {
    throw new RangeError("Open Graph image input is too large");
  }
  ogImageRenderer.uniform_set_text_color(textColor);
  ogImageRenderer.uniform_set_background_color(backgroundColor);
  ogImageRenderer.uniform_set_font_max_size(fontMaxSize);
  ogImageRenderer.uniform_set_font_weight(fontWeight);

  const outputSize = ogImageRenderer.render(written);
  if (outputSize > ogImageRenderer.output_utf8_cap()) {
    throw new RangeError("Open Graph image output is too large");
  }
  return decoder.decode(new Uint8Array(
    ogImageRenderer.memory.buffer,
    ogImageRenderer.output_ptr(),
    outputSize,
  ));
}

export function renderOGImageToPNG(options) {
  const svg = renderOGImageToSVG(options);
  const input = new Uint8Array(
    svgRasterizer.memory.buffer,
    svgRasterizer.input_ptr(),
    svgRasterizer.input_utf8_cap(),
  );
  const { read, written } = encoder.encodeInto(svg, input);
  if (read !== svg.length) {
    throw new RangeError("Open Graph SVG is too large to rasterize");
  }
  const bmpSize = svgRasterizer.render(written);
  if (bmpSize > svgRasterizer.output_bytes_cap()) {
    throw new RangeError("Open Graph BMP output is too large");
  }
  const bmp = new Uint8Array(
    svgRasterizer.memory.buffer,
    svgRasterizer.output_ptr(),
    bmpSize,
  );
  if (bmp.length > pngEncoder.input_bytes_cap()) {
    throw new RangeError("Open Graph BMP input is too large");
  }
  new Uint8Array(
    pngEncoder.memory.buffer,
    pngEncoder.input_ptr(),
    bmp.length,
  ).set(bmp);
  const pngSize = pngEncoder.render(bmp.length);
  if (pngSize > pngEncoder.output_bytes_cap()) {
    throw new RangeError("Open Graph PNG output is too large");
  }
  return new Uint8Array(
    pngEncoder.memory.buffer,
    pngEncoder.output_ptr(),
    pngSize,
  ).slice();
}

export function renderOGImageToWebp(options) {
  const svg = renderOGImageToSVG(options);
  const input = new Uint8Array(
    svgRasterizer.memory.buffer,
    svgRasterizer.input_ptr(),
    svgRasterizer.input_utf8_cap(),
  );
  const { read, written } = encoder.encodeInto(svg, input);
  if (read !== svg.length) {
    throw new RangeError("Open Graph SVG is too large to rasterize");
  }
  const bmpSize = svgRasterizer.render(written);
  if (bmpSize > svgRasterizer.output_bytes_cap()) {
    throw new RangeError("Open Graph BMP output is too large");
  }
  const bmp = new Uint8Array(
    svgRasterizer.memory.buffer,
    svgRasterizer.output_ptr(),
    bmpSize,
  );
  if (bmp.length > webpEncoder.input_bytes_cap()) {
    throw new RangeError("Open Graph BMP input is too large");
  }
  new Uint8Array(
    webpEncoder.memory.buffer,
    webpEncoder.input_ptr(),
    bmp.length,
  ).set(bmp);
  const webpSize = webpEncoder.render(bmp.length);
  if (webpSize > webpEncoder.output_bytes_cap()) {
    throw new RangeError("Open Graph WebP output is too large");
  }
  return new Uint8Array(
    webpEncoder.memory.buffer,
    webpEncoder.output_ptr(),
    webpSize,
  ).slice();
}

const options = {
  title: "Reusable components",
  subtitle: "Rendered locally with embedded Inter paths",
  textColor: 0xffffffff,
  backgroundColor: 0x4b2e83ff,
  fontMaxSize: 112,
  fontWeight: 700,
};

const svg = renderOGImageToSVG(options);
const png = renderOGImageToPNG(options);
const webp = renderOGImageToWebp(options);

console.log({ svg, png, webp });