1use serde::Serialize;
2use validator::Validate;
3
4use super::super::traits::*;
5
6#[derive(Clone, Serialize, Validate)]
8#[validate(schema(function = "validate_image_body"))]
9pub struct ImageGenBody<N>
10where
11 N: ImageGen,
12{
13 pub model: N,
15 #[serde(skip_serializing_if = "Option::is_none")]
17 #[validate(length(min = 1))]
18 pub prompt: Option<String>,
19 #[serde(skip_serializing_if = "Option::is_none")]
24 pub quality: Option<ImageQuality>,
25 #[serde(skip_serializing_if = "Option::is_none")]
29 pub size: Option<ImageSize>,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub watermark_enabled: Option<bool>,
33 #[serde(skip_serializing_if = "Option::is_none")]
36 #[validate(length(min = 6, max = 128))]
37 pub user_id: Option<String>,
38}
39
40impl<N> std::fmt::Debug for ImageGenBody<N>
41where
42 N: ImageGen + std::fmt::Debug,
43{
44 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 formatter
46 .debug_struct("ImageGenBody")
47 .field("model", &self.model)
48 .field("prompt_configured", &self.prompt.is_some())
49 .field("quality", &self.quality)
50 .field("size", &self.size)
51 .field("watermark_enabled", &self.watermark_enabled)
52 .field("user_id", &self.user_id.as_ref().map(|_| "[REDACTED]"))
53 .finish()
54 }
55}
56
57fn validate_image_body<N>(body: &ImageGenBody<N>) -> Result<(), validator::ValidationError>
58where
59 N: ImageGen,
60{
61 if body
62 .prompt
63 .as_deref()
64 .is_none_or(|prompt| prompt.trim().is_empty())
65 {
66 return Err(validator::ValidationError::new("prompt_required"));
67 }
68 let model = N::NAME;
69
70 if model == "glm-image" && matches!(body.quality, Some(ImageQuality::Standard)) {
71 return Err(validator::ValidationError::new(
72 "quality_not_supported_by_model",
73 ));
74 }
75 if body
76 .size
77 .as_ref()
78 .is_some_and(|size| !size.is_valid_for_model(model))
79 {
80 return Err(validator::ValidationError::new("invalid_image_size"));
81 }
82 Ok(())
83}
84
85impl<N> ImageGenBody<N>
86where
87 N: ImageGen,
88{
89 pub fn new(model: N) -> Self {
91 Self {
92 model,
93 prompt: None,
94 quality: None,
95 size: None,
96 watermark_enabled: None,
97 user_id: None,
98 }
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
104pub enum ImageQuality {
105 #[serde(rename = "hd")]
107 Hd,
108 #[serde(rename = "standard")]
110 Standard,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
119pub enum ImageSize {
120 Size1280x1280,
122 Size1568x1056,
124 Size1056x1568,
126 Size1472x1088,
128 Size1088x1472,
130 Size1728x960,
132 Size960x1728,
134 Size1024x1024,
136 Size768x1344,
138 Size864x1152,
140 Size1344x768,
142 Size1152x864,
144 Size1440x720,
146 Size720x1440,
148 Custom {
150 width: u32,
152 height: u32,
154 },
155}
156
157impl serde::Serialize for ImageSize {
158 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
159 where
160 S: serde::Serializer,
161 {
162 let dimensions = match self {
163 ImageSize::Size1280x1280 => "1280x1280",
164 ImageSize::Size1568x1056 => "1568x1056",
165 ImageSize::Size1056x1568 => "1056x1568",
166 ImageSize::Size1472x1088 => "1472x1088",
167 ImageSize::Size1088x1472 => "1088x1472",
168 ImageSize::Size1728x960 => "1728x960",
169 ImageSize::Size960x1728 => "960x1728",
170 ImageSize::Size1024x1024 => "1024x1024",
171 ImageSize::Size768x1344 => "768x1344",
172 ImageSize::Size864x1152 => "864x1152",
173 ImageSize::Size1344x768 => "1344x768",
174 ImageSize::Size1152x864 => "1152x864",
175 ImageSize::Size1440x720 => "1440x720",
176 ImageSize::Size720x1440 => "720x1440",
177 ImageSize::Custom { width, height } => {
178 return serializer.collect_str(&format_args!("{width}x{height}"));
179 },
180 };
181 serializer.serialize_str(dimensions)
182 }
183}
184
185impl ImageSize {
186 pub fn is_valid(&self) -> bool {
191 let (width, height) = self.dimensions();
192 dimensions_are_valid(width, height, 512, 16, 1 << 21)
193 }
194
195 fn is_valid_for_model(&self, model: &str) -> bool {
196 if model == "glm-image"
197 && matches!(
198 self,
199 Self::Size1280x1280
200 | Self::Size1568x1056
201 | Self::Size1056x1568
202 | Self::Size1472x1088
203 | Self::Size1088x1472
204 | Self::Size1728x960
205 | Self::Size960x1728
206 )
207 {
208 return true;
209 }
210 let (width, height) = self.dimensions();
211 if model == "glm-image" {
212 dimensions_are_valid(width, height, 1024, 32, 1 << 22)
213 } else {
214 dimensions_are_valid(width, height, 512, 16, 1 << 21)
215 }
216 }
217
218 pub fn dimensions(&self) -> (u32, u32) {
220 match self {
221 ImageSize::Size1280x1280 => (1280, 1280),
222 ImageSize::Size1568x1056 => (1568, 1056),
223 ImageSize::Size1056x1568 => (1056, 1568),
224 ImageSize::Size1472x1088 => (1472, 1088),
225 ImageSize::Size1088x1472 => (1088, 1472),
226 ImageSize::Size1728x960 => (1728, 960),
227 ImageSize::Size960x1728 => (960, 1728),
228 ImageSize::Size1024x1024 => (1024, 1024),
229 ImageSize::Size768x1344 => (768, 1344),
230 ImageSize::Size864x1152 => (864, 1152),
231 ImageSize::Size1344x768 => (1344, 768),
232 ImageSize::Size1152x864 => (1152, 864),
233 ImageSize::Size1440x720 => (1440, 720),
234 ImageSize::Size720x1440 => (720, 1440),
235 ImageSize::Custom { width, height } => (*width, *height),
236 }
237 }
238}
239
240fn dimensions_are_valid(
241 width: u32,
242 height: u32,
243 minimum: u32,
244 divisor: u32,
245 maximum_pixels: u64,
246) -> bool {
247 (minimum..=2048).contains(&width)
248 && (minimum..=2048).contains(&height)
249 && width.is_multiple_of(divisor)
250 && height.is_multiple_of(divisor)
251 && u64::from(width) * u64::from(height) <= maximum_pixels
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use crate::model::gen_image::{CogView4, GlmImage};
258
259 #[test]
260 fn body_validation_requires_prompt_and_valid_custom_size() {
261 assert!(ImageGenBody::new(CogView4 {}).validate().is_err());
262 let mut body = ImageGenBody::new(CogView4 {});
263 body.prompt = Some("image".into());
264 assert!(body.validate().is_ok());
265 body.size = Some(ImageSize::Custom {
266 width: 513,
267 height: 512,
268 });
269 assert!(body.validate().is_err());
270 }
271
272 #[test]
273 fn body_validation_applies_model_specific_size_and_quality_rules() {
274 let mut glm = ImageGenBody::new(GlmImage {});
275 glm.prompt = Some("image".into());
276 glm.size = Some(ImageSize::Size960x1728);
277 glm.quality = Some(ImageQuality::Hd);
278 assert!(glm.validate().is_ok());
279
280 glm.quality = Some(ImageQuality::Standard);
281 assert!(glm.validate().is_err());
282
283 let mut cogview = ImageGenBody::new(CogView4 {});
284 cogview.prompt = Some("image".into());
285 cogview.size = Some(ImageSize::Size1568x1056);
286 assert!(cogview.validate().is_ok());
287 cogview.size = Some(ImageSize::Size1280x1280);
288 assert!(cogview.validate().is_ok());
289 }
290
291 #[test]
292 fn debug_redacts_prompt_and_user_identifier() {
293 let mut body = ImageGenBody::new(GlmImage {});
294 body.prompt = Some("private image prompt".to_owned());
295 body.user_id = Some("private-user".to_owned());
296 let debug = format!("{body:?}");
297 assert!(!debug.contains("private image prompt"));
298 assert!(!debug.contains("private-user"));
299 assert!(debug.contains("prompt_configured: true"));
300 }
301}