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
Current section
Files
open_fresco
CHANGELOG.md
CHANGELOG.md
# Changelog
All notable changes to **OpenFresco** are documented here. The format
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this
project adheres to [Semantic Versioning](https://semver.org/).
## [0.2.0] — 2026-07-28
Consolidated response to the phoenix_kit_og feedback list — the editor
and renderer are now browser-verified and hardened for production.
### Fixed
- **Renderer-only hosts compile again** — the `Phoenix.LiveComponent`
optional-dep guard now wraps `defmodule` from the outside, so a host
without `:phoenix_live_view` builds cleanly (the editor module simply
isn't compiled).
- **Editor gesture race** — the JS hook hit-tests locally at pointerdown,
captures the pointer, previews with a client transform (re-applied
across mid-gesture server patches), commits one revision on pointerup,
and rolls back on pointercancel. Select-then-drag is one gesture.
- **Hit-testing uses resolved layout + paint order** — clicks land on
anchored / auto-width / placed elements where they actually paint;
`z`-ties resolve as painted (last wins).
- **Coordinate math is transform-safe** — pointer coords convert through
`getScreenCTM().inverse()`, correct under CSS scale, zoom, and DPR.
### Added
- **Resize** — corner handles on the selection chrome + `editor:resize`
emitter (the advertised `Ops.resize/5` is now wired end-to-end).
- **Stacking API** — `Ops.reorder/3`, `send_to_back/2`, `bring_forward/2`,
`send_backward/2`, with dense-`z` renumbering for deterministic order.
- **Command / inverse ops** — `Ops.command/2` returns each edit's inverse
(undo/redo for hosts); batch `move_many` / `delete_many` / `ids_in_rect`
for multi-select group gestures.
- **Multi-select** — shift-click sets, marquee (`editor:select_box`), and
group drag/delete as single transactional commits; keyboard Tab-cycle,
arrow-nudge, Escape, `]`/`[` (Alt = step).
- **`Scene.from_map/2` / `from_json/2`** — non-raising decode returning
`{:error, %{path, reason}}` locating the offending value, plus
strict/lenient unknown-field modes.
- **Lossless fill-variant switching** — `Scene.switch_fill/2` retains
inactive variants in the schema (round-trips through JSON); README claim
is now true.
- **Per-instance SVG id namespacing** (`:id_prefix`) — two stages on one
page no longer cross-reference each other's `<defs>`.
- **`Svg.render_parts/3` + `split_images`** — defs / body / selection as
separate dynamics and `<image>` payloads hoisted into `<defs>`, so
LiveView patches don't retransmit data-URL images.
- **Editor options** — `:globals` (one substitution path with the PNG),
`:resolver`, `:notify` (pid or `{module, id}`), `:selected`
(host-driven), `:fit` (container scaling). Echo-suppressed ownership
contract so parent re-renders can't clobber a gesture.
- **Resource-resolver contract** — one `:resolver` fun shared by stage and
rasterizer; returns inline bytes+mime (or a data URL), with the
deny-by-default policy still applied to anything it doesn't inline.
- **Custom fonts** — `config :open_fresco, font_dirs:/font_files:` (and
per-call opts) wired through a warm cached resvg fontdb (measurement +
paint) and the resvg CLI; `OpenFresco.Fonts.digest/1` feeds the manifest.
- **International text** — optional `:unicode_string` enables UAX #14
line-breaking (CJK etc.); `:direction` forwarded for RTL; a documented
supported-scripts matrix in `OpenFresco.Layout`.
- **Telemetry, budgets, and a render manifest** — `[:open_fresco, :render,
:start|:stop|:exception]` spans and `[:open_fresco, :element, :empty]`;
`:max_elements` / `:max_image_bytes` / `:max_svg_bytes` budgets; a
per-render `:manifest` (backend + versions, measurement mode, font
digest, SVG sha256) for cache-honest keys; forced backends hard-error
instead of silently falling back.
- **`OpenFresco.Conformance`** — the SVG security contract as an
enforceable allowlist (`check_svg/1`, `check_scene/3`), a hostile-input
fuzz corpus (`hostile_scenes/0`), and host-usable assertions.
### Security
- **SSRF deny-by-default is an allowlist** — image hrefs pass only as
`data:image/` URLs, opted-in `http(s)`, or conservative relative paths;
`javascript:`, `file:`, `data:text/html`, and stray markup draw the
unresolved stand-in.
- **CLI rasterizer hardening** — subprocesses are SIGKILLed by OS pid on
timeout (a closed port doesn't kill them) with a bounded output cap.
### Notes
- resvg drops valid 1×1 raster images (paints transparent) — reproduced
and drafted for upstream in `UPSTREAM_RESVG_1x1.md`.
## [0.1.0] — 2026-07-22
Initial release. Server-side OG-image / social-card rendering built to run on
[Fresco](https://hex.pm/packages/fresco) canvases — a scene model that renders
to SVG or PNG, browser-free.
### Scene model & templating
- **`OpenFresco.Scene`** — a versioned, fixed-size canvas plus z-ordered
elements: `text`, `image`, `shape`, `button` (solid/outline/soft presets),
and `stamp` (preset text). Serializes to/from JSON (`to_json!` /
`from_json!`) and migrates forward (`migrate/1`). Standalone at render time
(only runtime dep is `:jason`) but designed to be carried inside a Fresco
canvas's `extensions["open_fresco"]` blob.
- **Fills** — `solid/1`, `gradient/2` (angle + multi-stop, per-stop alpha),
`image_fill/3` (cover/contain/stretch, optional overlay tint). Gradient
alpha **masks** on images.
- **Templating** — `{{slot}}` / `[[global]]` inline substitution
(`OpenFresco.Substitute`), byte-compatible with `phoenix_kit_og`. Unknown
tokens pass through visibly; `Substitute.slots/1` lists a scene's slots with
inferred types. Whole-value `%{placeholder: k}` shorthand supported. Locale
is a render input.
### Rendering
- **SVG** — `OpenFresco.render_svg/3`. Pure, deterministic, XML-escaped,
dimension-clamped (OOM guard). A missing image slot draws a neutral
stand-in (never a `data:svg` black square).
- **PNG** — `OpenFresco.render/3` (→ `OpenFresco.Renderer`). Browser-free,
never raises, no network. Backed by the **optional `:resvg`** dependency
with a runtime fallback chain (`resvg` NIF → `resvg` CLI → `rsvg-convert` →
ImageMagick → `{:error, :rasterizer_missing}`). Returns
`{:ok, png, %{width, height}}`; `version/0` folds a cache-key token.
### Layout
- **Measurement-accurate text** (`OpenFresco.Measure`) — widths come from
resvg's own engine when available, so wrap points match the render;
character-estimate fallback otherwise.
- **`OpenFresco.Layout`** — word wrap, `valign`, **element-to-element
anchoring + reflow** (chains resolve, cycles rejected), **button
auto-width**, and legibility **underlays** (glow behind text, box behind
shapes/images).
- **Responsive sizing** — `Scene.place/1` (canvas-relative inset placement)
+ `Scene.put_size/3` + **`OpenFresco.Sizes`** presets
(`:og` / `:twitter` / `:square` / `:pinterest` / `:story`). One template
adapts to any canvas size.
### Editor & migration
- **`OpenFresco.Editor.Ops`** — pure, server-authoritative editing core
(hit-test / move / resize / reorder / delete).
- **`OpenFresco.Editor`** — a `Phoenix.LiveComponent` (optional
`:phoenix_live_view`) rendering a server-authoritative SVG preview, with a
JS hook in `priv/static/open_fresco.js`. _(Browser-verified and
substantially expanded in 0.2.0.)_
- **`OpenFresco.OgImport.scene_from_canvas/1`** — converts a `phoenix_kit_og`
canvas map into a scene; tokens are preserved verbatim, so imported
templates render the same output.