Packages

Server-side OG-image / social-card rendering, built on Fresco. A scene model (text, image, shape, button) with fills/gradients, {{slot}} templating, measured word-wrap, anchoring, and responsive sizing — rendered to SVG or PNG (via the optional resvg dependency).

Current section

Files

Jump to
open_fresco lib open_fresco.ex
Raw

lib/open_fresco.ex

defmodule OpenFresco do
@moduledoc """
OG-image / social-card scenes rendered server-side, built to run on
[Fresco](https://hex.pm/packages/fresco) canvases.
An **OG template is a design with holes** — the post title, hero image,
and CTA label are substituted per post (and per locale) at render time,
not baked into the design. `OpenFresco` models that as a `Scene` of
positioned elements whose text/image values may be *placeholders*, then
resolves those values and emits the result.
It produces **SVG** (`render_svg/3`) — the shared intermediate
representation — and **PNG** (`render/3`) via the optional `:resvg`
native dependency (or a CLI rasterizer on PATH). Because the editor
preview and the final raster come from one SVG generator, "what you edit
is what renders" holds by construction.
## Quick start
scene =
OpenFresco.Scene.new(width: 1200, height: 630, background: OpenFresco.Scene.solid("#0b1220"))
|> OpenFresco.Scene.add(
OpenFresco.Scene.text("title",
box: %{x: 64, y: 420, w: 1072, h: 150},
value: OpenFresco.Scene.placeholder("title"),
size: 68, weight: 700, fill: OpenFresco.Scene.solid("#ffffff")
)
)
svg = OpenFresco.render_svg(scene, %{"title" => "Hello, world"})
## Determinism & caching
`render_svg/3` is a pure function of `(scene, values)`. `version/0`
returns the generator version string — fold it into cache keys so cached
output invalidates when the generator changes.
## Not fresco-coupled at render time
A `Scene` is standalone. It is *carried inside* a Fresco canvas's
`extensions["open_fresco"]` blob for editing and storage (non-breaking
for etcher/tessera, which own their own extension keys), but rendering
needs no fresco dependency — this package depends only on `:jason`.
"""
alias OpenFresco.{Scene, Svg}
# Bump when the SVG generator's output changes in a way that must
# invalidate cached renders. Independent of the package @version.
@generator_version "ofsvg-1"
@doc """
Render a scene to SVG (a binary), resolving `values`.
`values` maps `{{slot}}` (and `[[global]]`) names to resolved values.
Inline tokens are substituted inside text and image strings; an unwired
`{{slot}}` passes through visibly (og's "needs wiring" signal), and a
missing image slot draws the neutral "unresolved image" stand-in. Locale
is a render input — pass locale-resolved values (or a `[[page_locale]]`
global).
Options:
* `:globals` — a map of `[[global]]` values (site host, page URL,
locale…), merged with `values` before substitution.
* `:max_dimension` — clamp canvas width/height to this many pixels
(default `10_000`) as an OOM guard against crafted scenes.
* `:remote_images` — external-resource policy. By default remote
(`http(s)://`, protocol-relative) and `file:` image hrefs are
**denied** and draw the unresolved stand-in: scenes are potentially
attacker-influenced DB content, and browsers/CLI rasterizers would
otherwise fetch at render time (SSRF) or read local files. Pass
`:allow` to permit `http(s)` for hosts that accept that surface.
`data:` URLs always pass.
* `:id_prefix` — namespace for generated SVG defs ids (gradients,
masks, clips, filters). Defaults to a scene-derived hash; pass a
per-instance id when inlining multiple *different* renders into one
page so their `url(#id)` references can't cross-wire (the editor
component passes its component id automatically).
"""
@spec render_svg(Scene.t(), map(), keyword()) :: binary()
def render_svg(%Scene{} = scene, values \\ %{}, opts \\ []) do
Svg.render(scene, values, opts)
end
@doc """
The SVG-generator version string, for folding into cache keys.
"""
@spec version() :: String.t()
def version, do: @generator_version
@doc """
Render a scene to **PNG bytes**`{:ok, png, %{width, height}}` or
`{:error, term}`. Browser-free, deterministic, never raises. Requires a
rasterizer backend (the optional `:resvg` dep, or a CLI on PATH); returns
`{:error, :rasterizer_missing}` when none is reachable. See
`OpenFresco.Renderer`.
"""
@spec render(Scene.t(), map(), keyword()) ::
{:ok, binary(), %{width: pos_integer(), height: pos_integer()}} | {:error, term()}
defdelegate render(scene, values \\ %{}, opts \\ []), to: OpenFresco.Renderer
@doc "Whether a PNG rasterizer backend is available (see `render/3`)."
@spec rasterizer_available?() :: boolean()
defdelegate rasterizer_available?(), to: OpenFresco.Renderer, as: :available?
end