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