xtandard-image for Java
Typed URL builder, signing and java.net.http client for the xtandard-image server.
io.github.xantiagoma:xtandard-image is the Java client for xtandard-image, an open-source media server that covers your application’s image and video needs, built on libvips and FFmpeg. The library builds and signs the URLs your code sends to the server, and wraps its /info 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”Maven:
<dependency> <groupId>io.github.xantiagoma</groupId> <artifactId>xtandard-image</artifactId> <version>0.x.y</version></dependency>Gradle:
implementation("io.github.xantiagoma:xtandard-image:0.x.y")Not published yet: publishing to Maven Central is being set up. Until then, install from the repository:
git clone --depth 1 https://github.com/xantiagoma/xtandard-imagecd xtandard-image/clients/java && mvn install -DskipTests # JDK 21 and Maven 3.9 (or `mise install` there)That puts 0.1.0-SNAPSHOT in your local Maven repository; depend on that version, with mavenLocal() in Gradle. JPMS users add requires io.github.xantiagoma.xtandard.image;.
Quick start
Section titled “Quick start”Build a signed URL with the keys the server was started with:
import io.github.xantiagoma.xtandard.image.*;
SigningKey signing = SigningKey.of(System.getenv("XI_SIGNING_KEY"), System.getenv("XI_SIGNING_SALT"));ImgproxyUrlBuilder img = ImgproxyUrlBuilder.builder().baseUrl("http://localhost:8080").signing(signing).build();
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 HTTP client:
XtandardClient xi = XtandardClient.builder("http://localhost:8080").signing(signing).build();
InfoResponse info = xi.info("https://images.unsplash.com/photo-1506744038136-46273834b3fb");System.out.println(info.format() + " " + info.width() + " " + info.height()); // jpeg 7360 4912Sign URLs on a server only: the signing key signs any URL.
Building URLs
Section titled “Building URLs”There are two equivalent forms, an options list and a fluent chain. Both have one typed method per option name and alias:
ImgproxyUrlBuilder img = ImgproxyUrlBuilder.builder().baseUrl("https://img.example.com").build();
img.url("s3://photos/cat.jpg", ProcessingOptions.create().resize("fill", 300, 400).gravity("sm").quality(80), "webp");img.image("s3://photos/cat.jpg").resize("fill", 300, 400).g("sm").quality(80).extension("webp").build();// both: https://img.example.com/insecure/rs:fill:300:400/g:sm/q:80/czM6Ly9waG90b3MvY2F0LmpwZw.webpThe option methods are generated from the server’s catalogue, with one overload per argument shape; null leaves an argument empty, and option(name, args...) adds any option by name. Each method’s Javadoc lists the URL names, the argument values and ranges. The options themselves are documented in URL dialects §3.2. Builders and keys are immutable and thread-safe; options lists and chains are mutable, like StringBuilder.
Builder settings: separator (default :), unsignedSignature (insecure, unsafe or _), optionNames (SHORT by default, CANONICAL or GIVEN) and splitSource.
Chained pipelines
Section titled “Chained pipelines”A list of options lists, or pipeline() in the fluent form, writes /-/ between pipelines:
img.url("s3://photos/cat.jpg", List.of(ProcessingOptions.create().rs("fit", 500, 500), ProcessingOptions.create().bl(2)), UrlParams.of());img.image("s3://photos/cat.jpg").rs("fit", 500, 500).pipeline().bl(2).build();Sources
Section titled “Sources”SourceEncoding |
URL |
|---|---|
BASE64 (default) |
/…/czM6Ly9waG90b3MvY2F0LmpwZw.webp |
PLAIN |
/…/plain/https://example.com/a%20b.jpg, escaped for you |
ENCRYPTED |
/…/enc/<token>; needs encryption (below) |
Set it on the builder (sourceEncoding(…)) or per call (UrlParams.of().withSourceEncoding(SourceEncoding.PLAIN)). Sources.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", InfoOptions.create().size(true).format(true));img.info("s3://photos/cat.jpg").size(true).format(true).build();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").rs("fill", 640, 360).vs(12).vd(5).extension("webm").build();img.streamUrl("s3://media/talk.mov", StreamParams.hls().withRenditions(360, 720).withSegmentDuration(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.
QueryUrlBuilder q = QueryUrlBuilder.builder().baseUrl("https://img.example.com").signing(signing).build();q.url("s3://photos/cat.jpg", QueryParams.create().w(300).fit("cover"), List.of(ProcessingOptions.create().bl(2)), UrlParams.ofExtension("webp"));// https://img.example.com/q/?url=s3://photos/cat.jpg&w=300&fit=cover&o=bl:2&format=webp&xi_sig=…q.imaginaryUrl(ImaginaryEndpoint.SMARTCROP, "s3://photos/cat.jpg", ImaginaryParams.create().width(300).height(300));See URL dialects §4.
Signing and encryption
Section titled “Signing and encryption”SigningKey.of(key, salt) takes the server’s hex key and salt (XI_SIGNING_KEYS / XI_SIGNING_SALTS, or imgproxy’s IMGPROXY_KEY / IMGPROXY_SALT). EncryptionKey.of(key) takes the first XI_ENCRYPTION_KEYS entry, for enc/ sources and password-protected PDFs:
ImgproxyUrlBuilder img = ImgproxyUrlBuilder.builder().baseUrl(baseUrl).signing(signing) .encryption(EncryptionKey.of(System.getenv("XI_ENCRYPTION_KEY"))).build();img.url("s3://private/cat.jpg", ProcessingOptions.create().w(300), UrlParams.of().withSourceEncoding(SourceEncoding.ENCRYPTED));img.url("s3://docs/contract.pdf", ProcessingOptions.create().rs("fit", 800, 0), UrlParams.of().withPdfPassword("s3cret"));Each enc/ token gets a random IV. Signing (sign, signImgproxyPath, signXiUrl, signXiQuery) and Encryption (encryptSource, encryptPdfPassword) expose the primitives; Signing.signXiUrl adds an xi_sig to a URL of any other dialect. Schemes: URL dialects §2.
HTTP client
Section titled “HTTP client”XtandardClient runs on java.net.http.HttpClient (pass your own with .httpClient(…)). It has healthz, readyz, info, image, video, stream, media (with Range and HEAD) and playgroundMeta, and xi.async() has the same calls returning CompletableFutures:
MediaResponse<byte[]> res = xi.image("s3://photos/cat.jpg", ProcessingOptions.create().w(300), UrlParams.of(), MediaRequest.get().withAccept("image/avif,image/webp"));xi.async().info("s3://photos/cat.jpg").thenAccept(i -> System.out.println(i.width()));Errors
Section titled “Errors”The builders throw UrlBuildException, whose code() (ErrorCode.UNKNOWN_OPTION, INVALID_ARGUMENT, INVALID_SOURCE, …) names the fault. The client throws an HttpException subclass by status (HttpException.Forbidden, NotFound, TooManyRequests, …) with the server’s status(), kind() and requestId(), and TransportException when the connection fails. All three extend the sealed, unchecked XtandardException.
try { xi.info("https://example.com/a.jpg");} catch (HttpException e) { System.out.println(e.status() + " " + e.kind() + " " + e.requestId()); // 403 forbidden …}Compatibility
Section titled “Compatibility”- Java 17 or later (tested on 17, 21 and 25). A JPMS module with no runtime dependencies:
javax.crypto,java.net.httpand a small internal JSON parser. JSpecify nullness annotations are compile-only, so Kotlin sees non-platform types. - On Android and Kotlin/Native, use the Kotlin Multiplatform client: Android has no
java.net.http. - The library version is the server version:
0.2.1matchesghcr.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 Java 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).