在扣费前校验 Raw mask 尺寸
解码时保留 image 与 mask 宽高并拒绝不一致输入 同步 Raw 图片编辑请求合同与回归测试
This commit is contained in:
@@ -35,7 +35,7 @@ POST /api/raw/v1/images/edit
|
||||
}
|
||||
```
|
||||
|
||||
`image` 是必填的单图结构 `{ data, mimeType }`;`mask` 可选并使用相同结构。`image` 和 `mask` 的 `mimeType` 必须为 `image/png`,base64 解码后必须是可完整解码的有效 PNG 文件;空数据、非 PNG 字节或 MIME 不匹配均在扣费前返回 400。服务端不把输入格式另建成请求参数。`prompt` 必填。`quality`、`background` 和 `output_format` 采用 GPT Image 模型支持的值。
|
||||
`image` 是必填的单图结构 `{ data, mimeType }`;`mask` 可选并使用相同结构。`image` 和 `mask` 的 `mimeType` 必须为 `image/png`,base64 解码后必须是可完整解码的有效 PNG 文件,提供 mask 时其宽高必须与 image 完全一致;空数据、非 PNG 字节、MIME 不匹配或尺寸不一致均在扣费前返回 400。服务端不把输入格式另建成请求参数。`prompt` 必填。`quality`、`background` 和 `output_format` 采用 GPT Image 模型支持的值。
|
||||
|
||||
`width`、`height` 使用严格输出尺寸规则,均在扣费前校验:
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use axum::{
|
||||
http::StatusCode,
|
||||
};
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
|
||||
use image::{ImageFormat, ImageReader};
|
||||
use image::{GenericImageView, ImageFormat, ImageReader};
|
||||
use platform_image::{
|
||||
RAW_IMAGE_MAX_EDGE, RAW_IMAGE_MAX_PIXELS, RawImageEditOptions, ReferenceImage,
|
||||
create_vector_engine_raw_image_edit, validate_raw_image_edit_dimensions,
|
||||
@@ -176,10 +176,16 @@ fn prepare_request(payload: RawImageEditRequest) -> Result<PreparedRawImageEdit,
|
||||
let quality = normalize_optional(payload.quality);
|
||||
let background = normalize_optional(payload.background);
|
||||
let output_format = normalize_optional(payload.output_format);
|
||||
let image = decode_image(payload.image, "image")?;
|
||||
let (image, image_width, image_height) = decode_image(payload.image, "image")?;
|
||||
let mask = payload
|
||||
.mask
|
||||
.map(|value| decode_image(value, "mask"))
|
||||
.map(|value| {
|
||||
let (mask, mask_width, mask_height) = decode_image(value, "mask")?;
|
||||
if mask_width != image_width || mask_height != image_height {
|
||||
return Err(bad_request("mask 尺寸必须与 image 一致"));
|
||||
}
|
||||
Ok(mask)
|
||||
})
|
||||
.transpose()?;
|
||||
Ok(PreparedRawImageEdit {
|
||||
image,
|
||||
@@ -217,7 +223,7 @@ fn validate_optional_value<const N: usize>(
|
||||
Err(bad_request(format!("{field} 值无效")))
|
||||
}
|
||||
|
||||
fn decode_image(value: RawImageData, field: &str) -> Result<ReferenceImage, AppError> {
|
||||
fn decode_image(value: RawImageData, field: &str) -> Result<(ReferenceImage, u32, u32), AppError> {
|
||||
let mime_type = value.mime_type.trim().to_string();
|
||||
if !mime_type.eq_ignore_ascii_case("image/png") {
|
||||
return Err(bad_request(format!("{field}.mimeType 必须为 image/png")));
|
||||
@@ -239,14 +245,19 @@ fn decode_image(value: RawImageData, field: &str) -> Result<ReferenceImage, AppE
|
||||
if reader.format() != Some(ImageFormat::Png) {
|
||||
return Err(bad_request(format!("{field}.data 必须是有效 PNG 文件")));
|
||||
}
|
||||
reader
|
||||
let decoded = reader
|
||||
.decode()
|
||||
.map_err(|error| map_decode_image_error(field, error))?;
|
||||
Ok(ReferenceImage {
|
||||
bytes,
|
||||
file_name: format!("{field}.png"),
|
||||
mime_type: "image/png".to_string(),
|
||||
})
|
||||
let (width, height) = decoded.dimensions();
|
||||
Ok((
|
||||
ReferenceImage {
|
||||
bytes,
|
||||
file_name: format!("{field}.png"),
|
||||
mime_type: "image/png".to_string(),
|
||||
},
|
||||
width,
|
||||
height,
|
||||
))
|
||||
}
|
||||
|
||||
fn map_decode_image_error(field: &str, error: image::ImageError) -> AppError {
|
||||
@@ -401,4 +412,22 @@ mod tests {
|
||||
|
||||
assert!(format!("{error:?}").contains("超出 PNG 尺寸或解码资源上限"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mask_must_match_source_image_dimensions() {
|
||||
let payload = serde_json::json!({
|
||||
"image": {"data": encoded_png(2, 1), "mimeType": "image/png"},
|
||||
"mask": {"data": encoded_png(1, 1), "mimeType": "image/png"},
|
||||
"prompt": "edit",
|
||||
"width": 1024,
|
||||
"height": 1024
|
||||
});
|
||||
let parsed: RawImageEditRequest = serde_json::from_value(payload).expect("request");
|
||||
let error = match prepare_request(parsed) {
|
||||
Ok(_) => panic!("mismatched mask should fail before billing"),
|
||||
Err(error) => error,
|
||||
};
|
||||
|
||||
assert!(format!("{error:?}").contains("mask 尺寸必须与 image 一致"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user