From 40316d82c4b1e8ade506115a4665de0b2ffd7855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 9 Sep 2026 11:45:44 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9C=A8=E6=89=A3=E8=B4=B9=E5=89=8D=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=20Raw=20mask=20=E5=B0=BA=E5=AF=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 解码时保留 image 与 mask 宽高并拒绝不一致输入 同步 Raw 图片编辑请求合同与回归测试 --- ...案】Raw GPT Image 2图片编辑代理-2026-09-07.md | 2 +- server-rs/crates/api-server/src/raw_image.rs | 49 +++++++++++++++---- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/docs/technical/【技术方案】Raw GPT Image 2图片编辑代理-2026-09-07.md b/docs/technical/【技术方案】Raw GPT Image 2图片编辑代理-2026-09-07.md index 12b1f0331..c5dd38c37 100644 --- a/docs/technical/【技术方案】Raw GPT Image 2图片编辑代理-2026-09-07.md +++ b/docs/technical/【技术方案】Raw GPT Image 2图片编辑代理-2026-09-07.md @@ -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` 使用严格输出尺寸规则,均在扣费前校验: diff --git a/server-rs/crates/api-server/src/raw_image.rs b/server-rs/crates/api-server/src/raw_image.rs index 456dcca69..e0e88a876 100644 --- a/server-rs/crates/api-server/src/raw_image.rs +++ b/server-rs/crates/api-server/src/raw_image.rs @@ -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( Err(bad_request(format!("{field} 值无效"))) } -fn decode_image(value: RawImageData, field: &str) -> Result { +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 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 一致")); + } }