Skip to content
Edgar Mesquita edited this page Oct 10, 2026 · 10 revisions

Image

🌐 This page in: English · Português

A bitmap in an explicitly sized slot. The size is a constructor argument and not a hint, because layout can never infer extent from a source it has not decoded — a slot that waits for the network is a page that reflows in front of the reader.

Image("/images/hero.jpg", 640, 360, label: "The team at the 2026 offsite")

Written with the factory, like the rest of the vocabulary. The same call realizes on the web and on Photon; nothing in it names a target.

The slot

Parameter Type Meaning
source string Where the artwork is. A path the target can fetch.
width float The slot's width, in density-independent pixels.
height float The slot's height.
fit ImageFit How the artwork fills the slot. Cover by default.
label string What a screen reader says. Empty means decorative, which is the HTML rule and holds on both targets.

Three more are set as properties, because they are the exception rather than the shape:

Property Type Meaning
DarkSource string? The artwork for DARK mode, when one image cannot serve both.
CornerRadius CornerRadii Per-corner radius; the artwork is clipped to it.
Fit ImageFit Also settable here, for a tree built without the factory.
new Image("/brand/wordmark.svg", 180, 40)
{
    DarkSource = "/brand/wordmark-white.svg",
    CornerRadius = new CornerRadii(8),
    Label = "",           // decorative: the name beside it already says it
}

Fit

ImageFit What it does
Cover Center-crop fill. The default, and what a photograph wants.
Contain Letterboxed inside the slot, whole source visible.
Stretch Distorting fill. Reserved for art that tolerates it.

Dark artwork is a PAIR, not a branch

A wordmark drawn in ink for light surfaces and in white for dark ones cannot be one file. Colors solve this with a ColorToken pair; artwork cannot, so the pair lives on the node and the realizer resolves it the same way — no branch in app code, and no mode question asked at paint time.

On the web both sources ship and CSS shows one, following the OS preference and the app's own forced mode, which the theme controller stamps as data-theme.

What each target draws

Web: an <img> with the slot's width and height, object-fit from Fit, border-radius from CornerRadius, src from Source and alt from Label.

Photon: the engine draws a SurfaceSubtle placeholder box under the same radius until texture upload lands — the documented degradation pattern, stated here so the shape of the page is the same on both targets even while the pixels are not.

v1 fences, shared by both: nine-slice, the loading and error states, and the decode crossfade join with the asset and animation systems rather than arriving on this node.


Server-side image optimization

eQuantic.UI.Images adds an endpoint: a standalone service that resizes, re-encodes and caches images on demand.

Read this first, because the shape of the feature is not what it looks like. The endpoint is not wired to the Image node. Nothing in the vocabulary asks it for a resized variant, and the node has no quality, srcset or opt-in property — the page you are reading used to say otherwise, and it described a web-only element that preceded the vocabulary. What ships is the service; pointing something at it is the caller's move today.

Enabling it

builder.Services.AddUI(options =>
{
    options.UseImageOptimization(opts =>
    {
        opts.DefaultQuality = 80;              // 1-100, default 75
        opts.Formats = ["image/webp"];         // preferred output formats
        opts.CacheTtlSeconds = 14400;          // default 4 hours
        opts.MaxSourceSize = 10 * 1024 * 1024; // default 10 MB
    });
});

app.UseStaticFiles();
app.MapUI();   // maps /_equantic/image

The contract

GET /_equantic/image?url=<path>&w=<width>&q=<quality>
Parameter Rule
url Required. Must start with / and must not contain .., so it cannot leave the content root.
w Required. Must be one of AllowedWidths, or the answer is 400 naming the allowed values.
q Optional, 1–100. Falls back to DefaultQuality.

The response format is negotiated from the request's Accept header against Formats, with a fallback the browser can always read. A miss resizes, converts and writes the result to CacheDirectory; a hit serves the cached bytes with Cache-Control: public, max-age=<CacheTtlSeconds>. A source larger than MaxSourceSize is refused rather than decoded, whoever hands it over: the optimizer and the placeholder generator stop reading at the budget, so a stream a caller opens is held to it as a file under wwwroot is.

What it reads, and what comes back

A source is read when its bytes are a JPEG, PNG, GIF, WebP or BMP, whatever its name says. A .tiff is refused before it is opened, and a file the optimizer cannot read, or one whose header asks for more than 16,383 × 16,383 pixels, is answered 400 with no pixel decoded. The optimizer runs on SkiaSharp, and its natives for Linux, macOS and Windows come with the package, so there is nothing to install and no license key to set.

  • Orientation. The orientation a camera recorded is applied to the pixels, so w is the width as displayed, and ImageOptimizer.GetDimensionsAsync answers the size as displayed.
  • Animation. An animated GIF or WebP is served as it is, with its own content type, as the Next.js optimizer serves one: no encoder here writes frames.
  • The response. It is one of the formats an encoder here writes, so Formats takes image/webp, image/png and image/jpeg, and any other, image/avif included, stops the app at startup. The pixels are sRGB and carry no colour profile, which a browser reads as sRGB.

Options

Option Default Meaning
DefaultQuality 75 Quality when the request does not say.
DeviceSizes 640, 750, 828, 1080, 1200, 1920, 2048, 3840 Widths for full-width images.
ImageSizes 32, 48, 64, 96, 128, 256, 384 Widths for smaller-than-viewport images.
AllowedWidths derived DeviceSizes and ImageSizes, deduplicated and sorted. The validation list.
Formats image/webp Output formats, best first: image/webp, image/png, image/jpeg.
CacheTtlSeconds 14400 What the response tells the browser.
CacheDirectory obj/eQuantic/image-cache Where the encoded results live.
MaxSourceSize 10 MB Above this a source is refused, read by the endpoint or handed over by a caller.

Blur placeholders

BlurPlaceholderGenerator turns a stream or a file into a tiny base64 data URL. Resolved from the services UseImageOptimization registers, or injected, it reads no source past the app's MaxSourceSize:

var placeholders = app.Services.GetRequiredService<BlurPlaceholderGenerator>();
var placeholder = await placeholders.GenerateFromFileAsync("wwwroot/images/hero.jpg");

It is a standalone helper for the same reason the endpoint is: the vocabulary has no placeholder property to hand the result to.

See Also

  • Components — the rest of the vocabulary
  • Design System — tokens, including the ColorToken pair DarkSource mirrors
  • Styling — HtmlElement and the web-only escape hatch

Clone this wiki locally