This guide describes the EMRG v3 image asset pipeline.
image/2 and Background.image/2 support raster formats plus self-contained SVGs.
SVG text uses system font matching; relative subresources and external SVG fonts are not loaded in v1.
Design Goals
- Keep UI APIs source-based (
~m"...", logical paths, runtime paths). - Keep runtime loading and decoding off the render-critical path.
- Resolve and cache assets in Rust asynchronously.
- Never fail fast on missing runtime media: show loading/failed placeholders.
Source Types
image/2 and Background.image/2 support:
%Emerge.Assets.Ref{}from~m"..."- logical path string (example:
"images/logo.png") - runtime path tuple (example:
{:path, "/data/photos/a.jpg"}) - preloaded image ID tuple (example:
{:id, "img_<sha256>"})
In EMRG v3 these are encoded as typed image sources:
0->{:id, id}1-> logical path2->{:path, path}
Runtime Flow
- Elixir uploads/patches tree sources as-is (no Elixir-side file IO).
- Rust tree actor requests missing sources from
AssetManageractor. AssetManagerresolves logical paths from the configured OTP appprivroot (or validates runtime paths) and reads files asynchronously.- On success, raster bytes are decoded or SVGs are parsed into a cached vector tree.
AssetManagernotifies tree actor, which triggers relayout/rerender.
Startup/config flow:
EmergeSkia.start/1requiresotp_appand callsconfigure_assets_nifwith<otp_app>/privas the source root plus runtime-path policy.EmergeSkia.start/1preloads configured font assets (assets.fonts) from<otp_app>/privand registers them in the native font cache.- Rust stores normalized config in
AssetManagerstate. - Reconfiguration clears source-status cache so paths are revalidated under new policy.
Render behavior while waiting:
- pending source -> loading placeholder
- failed source -> failed placeholder
- ready source -> normal image draw
Source status state machine:
- missing ->
pending(request queued) pending->ready(decoded/parsed + cached)pending->failed(blocked, unreadable, decode error, or missing)
There is no strict/lenient runtime mode and no fail-fast path for image load errors. Runtime failures always render the failed placeholder.
Source Root
Logical sources are resolved directly from the priv root of the otp_app passed to EmergeSkia.start/1.
Path safety rules for logical sources:
- paths must be relative (leading
/is normalized away) ..traversal is rejected- missing files resolve to the failed placeholder path
~m Verified Media Sigil
~m"images/logo.png" returns %Emerge.Assets.Ref{path: ..., verified?: true}.
Behavior:
- compile-time validation that the file exists under
<otp_app>/priv - marks source file as external resource for recompilation tracking
- only accepts literal string paths (no modifiers)
Import with:
use Emerge.Assets.Path, otp_app: :my_appRuntime Paths (Security)
Runtime filesystem ingestion is controlled by runtime_paths config.
Defaults are restrictive:
enabled: false- empty allowlist
- symlink following disabled
- extension allowlist enforced
- max file size enforced
Validation sequence for runtime paths:
- file stat
- extension check
- file size check
- symlink/canonical path policy
- allowlist root check
Font Assets
Font assets are configured at startup under assets.fonts and loaded synchronously.
Each entry supports:
family(required)source(required logical path under<otp_app>/priv, or%Emerge.Assets.Ref{})weight(optional, default400)italic(optional, defaultfalse)
Duplicate variants ({family, weight, italic}) are rejected at startup.
Start Options
EmergeSkia.start(
otp_app: :my_app,
assets: [
fonts: [
[family: "my-font", source: "fonts/MyFont-Regular.ttf", weight: 400],
[family: "my-font", source: "fonts/MyFont-Bold.ttf", weight: 700],
[family: "my-font", source: "fonts/MyFont-Italic.ttf", weight: 400, italic: true]
],
runtime_paths: [
enabled: false,
allowlist: [],
follow_symlinks: false,
max_file_size: 25_000_000,
extensions: [".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".svg"]
]
]
)