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

Image Component

The Image component is a high-performance alternative to the standard HTML <img> tag, heavily inspired by Next.js. It helps optimize images for speed and search engine ranking.

Features

  • Lazy Loading: Images are only loaded when they enter the viewport by default.
  • CLS Prevention: Automatically sets width and height to prevent layout shifts.
  • Layout Modes: Supports standard fixed sizing or a Fill mode for responsive containers.
  • Blur-up Placeholders: Support for low-resolution placeholders while the full image loads.
  • LCP Optimization: Use the Priority flag for critical above-the-fold images.
  • Server-Side Optimization: Auto resize, format conversion (WebP/AVIF), and quality control via eQuantic.UI.Images.

Basic Usage

new Image
{
    Src = "/path/to/image.jpg",
    Alt = "Description",
    Width = 800,
    Height = 600
}

Layout Modes

Fixed (Default)

Requires Width and Height. The image maintains these dimensions.

Fill

The image will fill its parent container. The parent MUST have relative positioning.

new Container
{
    ClassName = "relative h-64",
    Children =
    {
        new Image { Src = "/hero.webp", Fill = true }
    }
}

Placeholders

Use Blur placeholders for a smoother loading experience.

new Image
{
    Src = "/large.jpg",
    Width = 800,
    Height = 600,
    Placeholder = ImagePlaceholder.Blur,
    BlurDataURL = "data:image/webp;base64,..."
}

Server-Side Optimization

The eQuantic.UI.Images package enables Next.js-style server-side image optimization. When enabled, local images are automatically resized, converted to WebP, and served via an optimized endpoint.

How It Works

Image Component (Optimize = true)
    │
    ▼ Generates srcset URLs
/_equantic/image?url=/images/hero.jpg&w=640&q=80
    │
    ▼ Image Optimization Endpoint
    ├── Validates parameters (width in allowed sizes, quality 1-100)
    ├── Checks filesystem cache (obj/eQuantic/image-cache/)
    ├── On miss: resize → format convert → cache → serve
    └── Content negotiation: Accept header → WebP or JPEG fallback

Setup

1. Install the package

<PackageReference Include="eQuantic.UI.Images" />

2. Register services

// Program.cs
builder.Services.AddImageOptimization(opts =>
{
    opts.DefaultQuality = 80;              // 1-100, default: 75
    opts.Formats = ["image/webp"];         // Preferred output formats
    opts.CacheTtlSeconds = 14400;          // Cache TTL (default: 4 hours)
    opts.MaxSourceSize = 10 * 1024 * 1024; // Max source size (default: 10MB)
});

3. Map the endpoint (after UseStaticFiles(), before MapUI())

app.UseStaticFiles();
app.UseImageOptimization(); // Maps /_equantic/image endpoint
app.MapUI();

Optimization Modes

Fixed-Size Images (1x/2x Density Descriptors)

When Width is set and Fill is false, the component generates 1x and 2x density descriptors for retina displays:

new Image
{
    Src = "/images/hero.jpg",
    Alt = "Hero",
    Width = 800,
    Height = 600
}

Generated HTML:

<img
    src="/_equantic/image?url=%2Fimages%2Fhero.jpg&w=828&q=80"
    srcset="/_equantic/image?url=%2Fimages%2Fhero.jpg&w=828&q=80 1x,
            /_equantic/image?url=%2Fimages%2Fhero.jpg&w=1920&q=80 2x"
    width="800" height="600"
    loading="lazy" decoding="async" />

Responsive Images (Width Descriptors)

When using Fill mode or without Width, the component generates a full srcset with all configured sizes:

new Image
{
    Src = "/images/hero.jpg",
    Alt = "Full-width hero",
    Fill = true
}

Generated HTML:

<img
    src="/_equantic/image?url=%2Fimages%2Fhero.jpg&w=3840&q=80"
    srcset="/_equantic/image?url=%2Fimages%2Fhero.jpg&w=32&q=80 32w,
            /_equantic/image?url=%2Fimages%2Fhero.jpg&w=48&q=80 48w,
            ... (all configured sizes) ...
            /_equantic/image?url=%2Fimages%2Fhero.jpg&w=3840&q=80 3840w"
    sizes="100vw"
    loading="lazy" decoding="async" />

Custom Quality

Override the global quality setting per image:

new Image
{
    Src = "/images/background.jpg",
    Alt = "Background",
    Width = 1200,
    Height = 800,
    Quality = 50  // Lower quality for background images
}

Opt-In / Opt-Out

Optimization is controlled at two levels:

Global (UseImageOptimization()) Per-Image (Optimize) Result
Enabled null (default) Optimized
Enabled true Optimized
Enabled false Not optimized
Not called null (default) Not optimized
Not called true Optimized*

*Requires the endpoint to be mapped; otherwise, URLs will be generated but won't resolve.

// Explicit opt-out (e.g., SVGs don't need optimization)
new Image
{
    Src = "/images/logo.svg",
    Alt = "Logo",
    Optimize = false
}

Security

  • Only local images (paths starting with /) are optimized. External URLs are never proxied.
  • Path traversal attacks (..) are blocked.
  • Width must be in the configured allowed sizes (DeviceSizes + ImageSizes).
  • Quality must be 1-100.
  • Source file size is limited by MaxSourceSize (default 10MB).

Content Negotiation

The endpoint reads the browser's Accept header and serves the best supported format:

  1. Checks configured Formats (e.g., ["image/avif", "image/webp"])
  2. Returns the first format the browser supports
  3. Falls back to JPEG if no match

Caching

  • Filesystem cache: Optimized images are cached in obj/eQuantic/image-cache/ (configurable)
  • Cache key: SHA256 of (url, width, quality, format)
  • TTL: Configurable via CacheTtlSeconds (default: 4 hours)
  • Thread-safe: Concurrent requests for the same image are deduplicated
  • Response headers: Cache-Control: public, max-age={ttl}, Vary: Accept

Blur Placeholder Generator

The BlurPlaceholderGenerator service creates tiny 8px-wide JPEG placeholders as base64 data URLs:

// Inject from DI
var generator = app.Services.GetRequiredService<BlurPlaceholderGenerator>();

// Generate from file
var dataUrl = await generator.GenerateFromFileAsync("wwwroot/images/hero.jpg");
// Returns: "data:image/jpeg;base64,/9j/4AAQ..."

Configuration Options

Property Type Default Description
DeviceSizes int[] [640, 750, 828, 1080, 1200, 1920, 2048, 3840] Allowed widths for viewport-sized images.
ImageSizes int[] [32, 48, 64, 96, 128, 256, 384] Allowed widths for smaller images.
Formats string[] ["image/webp"] Preferred output formats (ordered by priority).
DefaultQuality int 75 Default quality (1-100).
CacheTtlSeconds int 14400 Cache TTL in seconds (4 hours).
CacheDirectory string "obj/eQuantic/image-cache" Filesystem cache directory.
MaxSourceSize long 10485760 Max source image size in bytes (10MB).

Properties

Property Type Default Description
Src string - Image source URL.
Alt string - Alternative text for accessibility.
Width int? - Width in pixels.
Height int? - Height in pixels.
Fill bool false If true, fills the parent container.
Loading ImageLoading Lazy Lazy or Eager.
Priority bool false High priority for preloading (LCP).
Placeholder ImagePlaceholder None Blur or Empty.
BlurDataURL string? - Small Data URL for blurred background.
ObjectFit string? "cover" CSS object-fit property (for Fill).
Optimize bool? null Override global optimization setting.
Quality int 0 Image quality (1-100). 0 = use global default.
SrcSet string? - Manual srcset (ignored when optimized).
Sizes string? - Manual sizes attribute.

Architecture

The eQuantic.UI.Images package follows the self-contained package pattern:

eQuantic.UI.Core
    └── ImageOptimizationState (static bridge)
         ↑ read by                ↑ written by
eQuantic.UI.Components     eQuantic.UI.Images
    └── Image component         ├── ImageOptimizer (SixLabors.ImageSharp)
                                ├── ImageCache (filesystem + SemaphoreSlim)
                                ├── ImageOptimizationMiddleware
                                ├── BlurPlaceholderGenerator
                                └── ImageExtensions (DI + endpoint)

This avoids circular dependencies: Core defines the state, Components reads it, Images writes it.

Clone this wiki locally