Add image-gen module: Spring AI ImageModel options on the wire, a moderation gate, and safe storage of generated images in MinIO (sniffed content type, byte and pixel caps, server-chosen keys, capped URL fetcher); per-provider cost table dated 2026-10-28. ImageModel and ModerationModel are hand-written fakes; MinIO is real via Testcontainers; no image provider called.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b7rnC7jppGGdHGESvy
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
package com.ankurm.imagegen;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
/**
|
||||
* Applies the policy to bytes: size cap, then sniff, then allowed type, then pixel cap read from
|
||||
* the header (before any decoder allocates a bitmap), then a real decode for the formats the JDK
|
||||
* can read. WebP is sniffed only, because the JDK has no WebP reader.
|
||||
*/
|
||||
public final class ImageVerifier {
|
||||
|
||||
private final ImagePolicy policy;
|
||||
|
||||
public ImageVerifier(ImagePolicy policy) {
|
||||
this.policy = policy;
|
||||
}
|
||||
|
||||
public Sniffed verify(byte[] bytes) {
|
||||
if (bytes.length > policy.maxBytes()) {
|
||||
throw new RejectedException("too-large", bytes.length + " bytes > cap " + policy.maxBytes());
|
||||
}
|
||||
Sniffed s = ImageSniffer.sniff(bytes)
|
||||
.orElseThrow(() -> new RejectedException("not-an-image", "no known image signature in the first bytes"));
|
||||
if (!policy.allowedTypes().contains(s.contentType())) {
|
||||
throw new RejectedException("type-not-allowed", s.contentType());
|
||||
}
|
||||
if (s.pixels() > policy.maxPixels()) {
|
||||
throw new RejectedException("too-many-pixels", s.width() + "x" + s.height() + " = " + s.pixels() + " > cap " + policy.maxPixels());
|
||||
}
|
||||
if (!s.contentType().equals("image/webp")) {
|
||||
try {
|
||||
BufferedImage img = ImageIO.read(new ByteArrayInputStream(bytes));
|
||||
if (img == null) {
|
||||
throw new RejectedException("undecodable", "signature says " + s.contentType() + " but no decoder accepts it");
|
||||
}
|
||||
} catch (IOException | RuntimeException e) {
|
||||
if (e instanceof RejectedException r) throw r;
|
||||
throw new RejectedException("undecodable", e.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user