xtandard-image for .NET
Typed URL builders, signing and an async HttpClient-based client for the xtandard-image server.
Xtandard.Image is the .NET client for xtandard-image, an open-source media server that covers your application’s image and video needs, built on libvips and FFmpeg. The package builds and signs the URLs your code sends to the server, and wraps its /info, media and health endpoints; it does not process images itself. To run a server, see Getting started, or start one with signing keys:
export XI_SIGNING_KEY="$(openssl rand -hex 32)" XI_SIGNING_SALT="$(openssl rand -hex 32)"docker run -d --name xtandard-image -p 8080:8080 \ -e XI_SOURCES=web -e XI_SOURCES_WEB_TYPE=http -e XI_SOURCES_WEB_ALLOW=images.unsplash.com \ -e XI_SIGNING_KEYS="$XI_SIGNING_KEY" -e XI_SIGNING_SALTS="$XI_SIGNING_SALT" \ ghcr.io/xantiagoma/xtandard-image:0.1.1Install
Section titled “Install”dotnet add package Xtandard.Imagedotnet add package Xtandard.Image.Extensions # optional: AddXtandardImage() for ASP.NET Core and the Generic HostNot published yet: publishing to NuGet is being set up. Until then, install from the repository:
git clone --depth 1 https://github.com/xantiagoma/xtandard-imagedotnet pack xtandard-image/clients/dotnet/src/Xtandard.Image -c Release -o ./xi-packagesdotnet add package Xtandard.Image --version 0.0.0 --source ./xi-packagesPacking needs the .NET 10 SDK; the package itself runs on .NET 8 and .NET 10. Xtandard.Image.Extensions packs the same way from src/Xtandard.Image.Extensions.
Quick start
Section titled “Quick start”Build a signed URL with the keys the server was started with:
using Xtandard.Image;
var signing = new SigningKey( Environment.GetEnvironmentVariable("XI_SIGNING_KEY")!, Environment.GetEnvironmentVariable("XI_SIGNING_SALT")!);
var img = new ImgproxyUrlBuilder(new ImgproxyConfig { BaseUrl = "http://localhost:8080", Signing = signing });
string url = img.Image("https://images.unsplash.com/photo-1506744038136-46273834b3fb") .Resize("fill", 1200, 630) .Gravity("sm") .Extension("jpg") .Build();// http://localhost:8080/<signature>/rs:fill:1200:630/g:sm/aHR0cHM6….jpgAsk the server about the source with the client:
using var xi = new XtandardClient(new XtandardClientOptions { BaseUrl = "http://localhost:8080", Signing = signing });
InfoResponse info = await xi.InfoAsync("https://images.unsplash.com/photo-1506744038136-46273834b3fb");Console.WriteLine($"{info.Format} {info.Width} {info.Height}"); // jpeg 7360 4912Sign URLs on a server only: the signing key signs every URL, and the encryption key decrypts every enc/ source.
Building URLs
Section titled “Building URLs”There are two equivalent forms, an options collection (names or aliases with their arguments, or typed records) and a fluent chain with one method per option name and alias:
using static Xtandard.Image.ProcessingOption; // the typed option records: Resize, Gravity, Quality, …
var img = new ImgproxyUrlBuilder(new ImgproxyConfig { BaseUrl = "https://img.example.com", Signing = signing });
img.Url("s3://photos/cat.jpg", new() { { "rs", "fill", 1200, 630 }, { "g", "sm" }, { "q", 80 } }, extension: "webp");img.Url("s3://photos/cat.jpg", new() { new Resize("fill", 1200, 630), new Gravity("sm"), new Quality(80) }, extension: "webp");// both: https://img.example.com/<signature>/rs:fill:1200:630/g:sm/q:80/czM6Ly9waG90b3MvY2F0LmpwZw.webpimg.Image(Sources.XiSource("photos", "2026/cat.jpg")).Resize("fit", 300, 300).Blur(2).Extension("png").Build();The methods and records are generated from the server’s catalogues, with typed arguments and XML docs. OptionArg.Null writes an empty argument (rs:fill:300::1). Builders are immutable and thread-safe. The options themselves are documented in URL dialects §3.2.
Chained pipelines
Section titled “Chained pipelines”ChainedUrl, or Pipeline() in the fluent form, writes /-/ between pipelines:
img.ChainedUrl("s3://photos/cat.jpg", [new() { { "rs", "fit", 500, 500 } }, new() { { "bl", 2 } }]);img.Image("s3://photos/cat.jpg").Resize("fit", 500, 500).Pipeline().Blur(2).Build();Sources
Section titled “Sources”Sources are base64url by default (SplitSource splits them into segments). SourceEncoding.Plain writes /plain/…, escaped for you, and SourceEncoding.Encrypted writes /enc/<token> (it needs Encryption, below). Set it on ImgproxyConfig or per call:
img.Url("https://example.com/a b.jpg", new() { { "w", 300 } }, sourceEncoding: SourceEncoding.Plain);// …/plain/https://example.com/a%20b.jpgSources.XiSource("photos", "2026/cat.jpg") writes a named source (xi://photos/2026/cat.jpg) and Sources.SourceUrl("s3", "bucket", "cat.jpg") a storage URL.
img.InfoUrl("s3://photos/cat.jpg", new() { { "size", true }, { "format", true }, { "dimensions", true } });Video and streams
Section titled “Video and streams”/video URLs use the video option catalogue; the extension is the container (mp4, webm, mp3, png for a waveform, …). StreamUrl builds HLS and DASH manifests, whose segments a player fetches without any key:
img.Video("s3://media/talk.mov").Start(12).Duration(8).Mute(true).Extension("mp4").Build();img.StreamUrl("s3://media/talk.mov", StreamProtocol.Hls, renditions: [360, 720], segmentDuration: 4);The server needs the full or gpl image for video. Options and containers: URL dialects §3.8 and §3.10.
Query dialect
Section titled “Query dialect”QueryUrlBuilder builds /q/ URLs (native keys plus imgproxy options in o=), /q/video and imaginary endpoints, signed with xi_sig. The server needs XI_DIALECTS_QUERY_ENABLED=true.
var q = new QueryUrlBuilder(new QueryConfig { BaseUrl = "https://img.example.com", Signing = signing });q.Url("s3://photos/cat.jpg", new() { { QueryKeys.W, 300 }, { QueryKeys.F, "webp" } }, new() { new Blur(2) });// https://img.example.com/q/?url=s3://photos/cat.jpg&w=300&f=webp&o=bl:2&xi_sig=…q.ImaginaryUrl(ImaginaryEndpoints.Resize, "s3://photos/cat.jpg", new() { { "width", 300 } });See URL dialects §4. The client can also send query URLs with the HTTP QUERY method (QueryMethodRequest, XtandardClientOptions.QueryMethod), off by default.
Signing and encryption
Section titled “Signing and encryption”SigningKey takes the server’s hex key and salt (XI_SIGNING_KEYS / XI_SIGNING_SALTS, or imgproxy’s IMGPROXY_KEY / IMGPROXY_SALT). EncryptionKey takes the first XI_ENCRYPTION_KEYS entry, for enc/ sources and password-protected PDFs:
var img = new ImgproxyUrlBuilder(new ImgproxyConfig{ BaseUrl = "https://img.example.com", Signing = signing, Encryption = new EncryptionKey(Environment.GetEnvironmentVariable("XI_ENCRYPTION_KEY")!),});img.Url("s3://private/cat.jpg", new() { { "w", 300 } }, sourceEncoding: SourceEncoding.Encrypted);img.Url("s3://docs/report.pdf", new() { { "rs", "fit", 800, 0 } }, extension: "png", pdfPassword: "s3cret");Signing.SignXiUrl adds an xi_sig to a URL of any other dialect. Schemes: URL dialects §2.
HTTP client
Section titled “HTTP client”XtandardClient has HealthzAsync, ReadyzAsync, InfoAsync, ImageAsync, VideoAsync, StreamAsync, MediaAsync (with Range and HEAD) and PlaygroundMetaAsync. Every method takes a CancellationToken; media methods return the HttpResponseMessage, which the caller disposes:
using var image = await xi.ImageAsync("s3://photos/cat.jpg", new() { { "w", 300 } }, request: new() { Accept = "image/avif,image/webp" });using var part = await xi.VideoAsync("s3://media/talk.mov", new() { { "vd", 5 } }, "webm", request: new() { Range = ByteRange.Of(0, 1023) });new XtandardClient(options) owns its HttpClient; new XtandardClient(httpClient, options) uses one it does not own. With Xtandard.Image.Extensions, register it through IHttpClientFactory:
builder.Services.AddXtandardImage(o =>{ o.BaseUrl = "https://img.example.com"; o.Signing = new SigningKey(builder.Configuration["Xi:SigningKey"]!, builder.Configuration["Xi:SigningSalt"]!);}); // returns the IHttpClientBuilder: add handlers or resilience here// Inject XtandardClient (a typed client), ImgproxyUrlBuilder or QueryUrlBuilder (singletons).Errors
Section titled “Errors”An invalid option, argument, source, extension or key throws UrlBuildException, whose Code (UrlBuildErrorCode.UnknownOption, …) and CodeName (unknown_option, …) are the codes every official client shares. A non-2xx answer throws an XtandardHttpException subclass chosen by status (ForbiddenException, NotFoundException, TooManyRequestsException, ServiceUnavailableException, …), with the server’s Status, Kind and RequestId. Connection failures and timeouts throw XtandardTransportException. All of them derive from XtandardException.
try{ await xi.InfoAsync("https://example.com/a.jpg");}catch (XtandardHttpException e){ Console.WriteLine($"{(int)e.Status} {e.Kind} {e.RequestId}"); // 403 forbidden …}Compatibility
Section titled “Compatibility”- .NET 8 and .NET 10 (
net8.0,net10.0). Nullable annotations, trimming- and AOT-compatible (a source-generatedSystem.Text.Jsoncontext, no reflection). Xtandard.Imagehas no package dependencies.Xtandard.Image.Extensionsdepends onMicrosoft.Extensions.Http(MIT).- The package version is the server version:
Xtandard.Image0.2.1 matchesghcr.io/xantiagoma/xtandard-image:0.2.1. Versions follow ZeroVer: a minor bump may break the API, a patch does not.
- xtandard-image and Getting started
- URL dialects: every option and signing scheme
- Playground: build URLs in a browser and copy the C# code
- Changelog
- Contributing and the official clients
- Security policy
- Licence: MIT
A personal project, provided as is under the MIT licence: see the disclaimer.
Design: every official client is generated from one spec and passes one conformance fixture (ADR 0013).