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.
This v0.1 slice produces **SVG** (`render_svg/3`) — the shared
intermediate representation. A later, optional native dependency will
rasterize the same SVG to PNG (`render/3`, not yet available). Because
both 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.
"""
@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