# 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.
