OpenFresco.Renderer (open_fresco v0.2.0)

Copy Markdown View Source

The PNG facade: scene → SVG → raster → PNG bytes. Browser-free and deterministic.

render/3 never raises — every failure (a missing rasterizer, a crafted scene, a subprocess timeout, a blown budget) is an {:error, term}. It performs no network I/O; image inputs in the scene must be data: URLs (or resolved to bytes by the :resolver — see OpenFresco.render_svg/3).

Budgets

Canvas dimensions alone understate the work a crafted scene can demand, so total work is budgeted too (all overridable per call):

  • :max_dimension — canvas clamp (default 10 000 px per side)
  • :max_elements — element count (default 1 000)
  • :max_image_bytes — total decoded size of inlined images, estimated from base64 payloads (default 64 MiB)
  • :max_svg_bytes — the generated SVG document (default 96 MiB)

Exceeding a budget returns {:error, {:budget, which, actual, limit}}.

Telemetry

  • [:open_fresco, :render, :start | :stop | :exception] — a span per PNG render; :stop measurements carry duration (native units), metadata carries backend, measured?, width, height, and result (:ok / :error).

  • [:open_fresco, :element, :empty] — emitted by the SVG generator whenever an element paints nothing (unknown type, unresolved or policy-denied image), with the element id and reason.

Backend choice & fallback

The backend auto-detects in preference order (see OpenFresco.Rasterizer.which_backend/0). Different backends paint different pixels — for cache-honest or reproducible output pass backend: :resvg_nif (etc.) explicitly: a forced backend that isn't available hard-errors instead of silently falling back. The manifest (below) records what actually ran.

The render manifest

Success meta includes :manifest — the actual pixel-producing inputs, for cache keys that are honest about determinism:

%{
  generator: "ofsvg-1",            # SVG generator revision
  renderer: "rast-1",              # rasterization contract revision
  backend: :resvg_nif,             # what actually painted
  backend_version: "0.5.1",
  measured: true,                  # measurement-accurate wrap?
  fonts: "system" | digest,        # OpenFresco.Fonts.digest/1
  svg_sha256: "…"                  # digest of the exact SVG painted
}

svg_sha256 subsumes scene, values, globals, resolver output, and layout — two renders with equal manifests produce identical pixels on the same host.

Concurrency & crash safety

The resvg NIF (rustler) catches Rust panics and returns errors, but a hard native abort (allocator exhaustion on a huge decode) can still take the BEAM down — that's the failure envelope of any in-process NIF. The budgets above bound allocations; hosts that want full isolation on the crawler path should force backend: :resvg_cli (a separate OS process, killed on timeout) and accept the per-render subprocess cost. For crawler-facing endpoints, render through a bounded pool:

Task.Supervisor.async_nolink(MyApp.RenderSup, fn ->
  OpenFresco.render(scene, values)
end)

with max_children sized to your CPU budget.

Summary

Functions

Whether a rasterizer backend is reachable (else render/3 errors out).

Render a scene to PNG bytes.

A version string for cache keys — folds the SVG generator version and the rasterization revision. For caches keyed on the full determinism surface use the per-render :manifest instead (backend, fonts, and the exact SVG all participate there).

The rasterizer backend that auto-detection would pick right now.

Functions

available?()

@spec available?() :: boolean()

Whether a rasterizer backend is reachable (else render/3 errors out).

render(scene, values \\ %{}, opts \\ [])

@spec render(OpenFresco.Scene.t(), map(), keyword()) ::
  {:ok, binary(),
   %{width: pos_integer(), height: pos_integer(), manifest: map()}}
  | {:error, term()}

Render a scene to PNG bytes.

Returns {:ok, png_binary, %{width: w, height: h, manifest: manifest}} or {:error, term}.

values / opts are as OpenFresco.render_svg/3 (:globals, :max_dimension, :remote_images, :resolver, :font_dirs, …), plus:

  • :timeout — subprocess-backend wallclock ms (default 5000).
  • :backend — force a rasterizer backend; missing forced backends error instead of falling back.
  • the budget options documented above.

version()

@spec version() :: String.t()

A version string for cache keys — folds the SVG generator version and the rasterization revision. For caches keyed on the full determinism surface use the per-render :manifest instead (backend, fonts, and the exact SVG all participate there).

which_backend()

@spec which_backend() :: :resvg_nif | :resvg_cli | :rsvg | :magick | :none

The rasterizer backend that auto-detection would pick right now.