Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b7rnC7jppGGdHGESvy
4.4 KiB
image-gen
Companion code for Image Generation and Vision in Spring AI: ImageModel, Prompt Options and Safe Storage, part of the Spring AI series on ankurm.com. It is the generation half of Multimodal Spring AI: Extract Structured Data from Images.
A prompt goes through our own checks, into ImageModel, and the bytes that come back are verified and written to an S3-compatible bucket under a key we choose.
No image provider was called. The tests use two hand-written fakes -- ScriptedImageModel (an ImageModel that returns what it was told to) and ScriptedModerationModel -- and, for the wire tests, the real OpenAiImageModel against a local mock of /v1/images/generations (FakeImagesServer) that returns a fixed 64x64 PNG whatever the prompt says. Nothing here measures image quality, latency, or whether a real model would refuse a prompt. The storage is real: the ImageStore tests run against a MinIO server in Docker through Testcontainers (the image is chainguard/minio, an upstream MinIO build, because minio/minio and quay.io/minio/minio could not be pulled from the machine these were captured on; the exact version is in output/05-store-minio.txt). Prices in CostModel were copied from the providers' pages on 2026-10-28 and are not checked by any test.
| Spring Boot | 4.1.2 (parent; GA on Maven Central 2026-10-22) |
| Spring AI | 2.0.2 (spring-ai-model, spring-ai-openai, spring-ai-stability-ai, spring-ai-google-genai-image) |
| AWS SDK for Java | 2.57.5 (s3) |
| Testcontainers | 2.0.5 |
| Java | 25 |
| Run | scripts/run-all.sh (about 20 s, needs Docker for the MinIO tests) |
Source
| File | What it is |
|---|---|
ImageService |
prompt gate -> ImageModel -> bytes -> verify -> store; one bad image in a batch stores nothing |
PromptGate |
length and control-character checks, then ModerationModel; fails closed when moderation is down |
OpenAiOptionsFactory |
size / quality / n checked against allow-lists, then OpenAiImageOptions with the model set explicitly |
ImageSniffer, ImageVerifier, ImagePolicy |
magic-number sniffing, size cap, pixel cap read from the header, a real decode, allow-list of types |
RemoteFetcher |
downloads a provider-returned URL: https and listed hosts only, no redirects, hard byte cap |
ImageStore |
S3/MinIO writer: server-chosen key, pinned Content-Type, Content-Disposition: attachment, presigned GET |
CostModel |
per-image prices with the date they were read and the page they came from |
Captured output (output/)
| File | Shows |
|---|---|
01-api-shape.txt |
class names, portable vs provider options, what is not there (moderation option, image moderation) |
02-wire-requests.txt |
JSON body sent by the real OpenAiImageModel for six option shapes, including the model reset and the ImagePrompt(List) failure |
03-wire-response.txt |
what ImageResponse carries back (b64 or URL, revised prompt, no usage) |
04-wire-refusal.txt |
a provider 400 content_policy_violation arrives as an SDK exception |
05-store-minio.txt, 05b-presigned.txt |
the object as MinIO holds it; presigned URL behaviour |
06-verifier.txt, 07-trailing-bytes.txt, 08-base64-cap.txt |
twelve inputs against the verifier; the limit of sniffing; base64 refused before decoding |
09-fetcher.txt |
redirect, scheme, host list, endless body |
10-keys.txt, 11-naive-vs-pinned.txt |
owner and id validation; trusting the sender's Content-Type |
12-service-happy.txt .. 15-url-results.txt |
the service end to end; refusals before any paid call; a batch with one bad image; URL results |
16-cost.txt |
the per-image price table |
Keys, ports and timestamps differ between runs; everything else is stable.