package vip.mate.tool.image;
import lombok.Builder;
import lombok.Data;
import java.util.List;
import java.util.Set;
/**
* Image generation provider capability declaration.
*
*
The flat top-level fields ({@code supportedSizes}, {@code aspectRatios},
* {@code maxCount}, {@code modes}) describe the provider's combined surface
* area and remain in use by callers that don't need per-mode granularity.
* Newer code should consult the structured {@link Generate} / {@link Edit} /
* {@link Geometry} / {@link Output} fields, which let the picker show
* "edit supports up to N reference images" or "generate accepts these
* formats" without conflating the two modes.
*
* @author MateClaw Team
*/
@Data
@Builder
public class ImageProviderCapabilities {
/** Combined modes the provider supports across all its models. */
@Builder.Default
private Set modes = Set.of(ImageCapability.TEXT_TO_IMAGE);
/** Union of pixel sizes accepted by any model under this provider. */
@Builder.Default
private List supportedSizes = List.of("1024x1024");
/** Union of aspect ratio presets accepted by any model under this provider. */
@Builder.Default
private List aspectRatios = List.of("1:1", "16:9", "9:16");
/** Largest {@code n} (image count) any model under this provider accepts. */
@Builder.Default
private int maxCount = 1;
/** Default model id. */
private String defaultModel;
/** All callable model ids. */
@Builder.Default
private List models = List.of();
/** Per-mode generate capabilities. Optional — falls back to flat fields when absent. */
private Generate generate;
/** Per-mode edit capabilities. {@code null} or {@code enabled=false} means edits unsupported. */
private Edit edit;
/** Geometry surface (sizes / aspect ratios). Optional. */
private Geometry geometry;
/** Output knobs (formats, qualities, backgrounds). Optional. */
private Output output;
@Data
@Builder
public static class Generate {
@Builder.Default
private int maxCount = 1;
@Builder.Default
private boolean supportsSize = true;
@Builder.Default
private boolean supportsAspectRatio = true;
}
@Data
@Builder
public static class Edit {
@Builder.Default
private boolean enabled = false;
@Builder.Default
private int maxCount = 1;
@Builder.Default
private int maxInputImages = 1;
@Builder.Default
private boolean supportsSize = true;
@Builder.Default
private boolean supportsAspectRatio = true;
}
@Data
@Builder
public static class Geometry {
@Builder.Default
private List sizes = List.of();
@Builder.Default
private List aspectRatios = List.of();
}
@Data
@Builder
public static class Output {
@Builder.Default
private List formats = List.of();
@Builder.Default
private List qualities = List.of();
@Builder.Default
private List backgrounds = List.of();
}
/**
* Match the requested size against supported sizes by area only.
* Orientation-blind — prefer {@link #normalizeSize(String, String)} when an
* aspect ratio is available so portrait/landscape intent is preserved.
*/
public String normalizeSize(String requested) {
return normalizeSize(requested, null);
}
/**
* Match the requested size against supported sizes, preserving orientation.
* Resolution order:
*
* - If {@code requestedSize} is already in {@code supportedSizes}, return it.
* - If {@code requestedAspectRatio} is given, narrow {@code supportedSizes}
* to those whose orientation matches (portrait / landscape / square),
* then pick by closest area.
* - Otherwise pick by closest area across all supported sizes.
*
*/
public String normalizeSize(String requestedSize, String requestedAspectRatio) {
if (supportedSizes.isEmpty()) {
return "1024x1024";
}
if (requestedSize != null && supportedSizes.contains(requestedSize)) {
return requestedSize;
}
Orientation targetOrientation = orientationFor(requestedAspectRatio);
List candidates = supportedSizes;
if (targetOrientation != null) {
List matching = supportedSizes.stream()
.filter(s -> orientationOf(s) == targetOrientation)
.toList();
if (!matching.isEmpty()) {
candidates = matching;
}
}
long reqArea = (requestedSize == null || requestedSize.isBlank())
? 1024L * 1024L
: parseArea(requestedSize);
String closest = candidates.get(0);
long minDiff = Math.abs(reqArea - parseArea(closest));
for (String s : candidates) {
long diff = Math.abs(reqArea - parseArea(s));
if (diff < minDiff) {
minDiff = diff;
closest = s;
}
}
return closest;
}
private enum Orientation { PORTRAIT, LANDSCAPE, SQUARE }
private static Orientation orientationFor(String aspectRatio) {
if (aspectRatio == null || aspectRatio.isBlank()) return null;
String[] parts = aspectRatio.split(":");
if (parts.length != 2) return null;
try {
double w = Double.parseDouble(parts[0].trim());
double h = Double.parseDouble(parts[1].trim());
if (w == h) return Orientation.SQUARE;
return w > h ? Orientation.LANDSCAPE : Orientation.PORTRAIT;
} catch (NumberFormatException e) {
return null;
}
}
private static Orientation orientationOf(String size) {
try {
String[] parts = size.toLowerCase().split("x");
long w = Long.parseLong(parts[0].trim());
long h = Long.parseLong(parts[1].trim());
if (w == h) return Orientation.SQUARE;
return w > h ? Orientation.LANDSCAPE : Orientation.PORTRAIT;
} catch (Exception e) {
return Orientation.SQUARE;
}
}
/**
* 将请求的 aspectRatio 就近匹配或回退到默认
*/
public String normalizeAspectRatio(String requested) {
if (aspectRatios.contains(requested)) {
return requested;
}
return aspectRatios.isEmpty() ? "1:1" : aspectRatios.get(0);
}
/**
* 将请求的 count 限制在 provider 支持范围内
*/
public int normalizeCount(int requested) {
return Math.min(Math.max(requested, 1), maxCount);
}
private long parseArea(String size) {
try {
String[] parts = size.toLowerCase().split("x");
return Long.parseLong(parts[0].trim()) * Long.parseLong(parts[1].trim());
} catch (Exception e) {
return 1024L * 1024L;
}
}
}