为 Raw 编辑建立独立字节类型

恢复旧图片链路的 Vec 字节契约

让新增 raw_edit 输入和结果类型独立于既有 ReferenceImage
This commit is contained in:
2026-09-12 11:13:27 +08:00
parent 7edf4a8f8b
commit d05440dcca
9 changed files with 65 additions and 46 deletions
@@ -2653,7 +2653,7 @@ pub(crate) fn editor_icon_spritesheet_warning_after_persist_error(
}
fn validate_editor_icon_spritesheet_source(source: &DownloadedImage) -> Result<(), AppError> {
let reader = image::ImageReader::new(Cursor::new(source.bytes.as_ref()))
let reader = image::ImageReader::new(Cursor::new(source.bytes.as_slice()))
.with_guessed_format()
.map_err(|error| {
AppError::from_status(StatusCode::UNPROCESSABLE_ENTITY).with_details(json!({
@@ -2931,7 +2931,7 @@ mod tests {
.write_to(&mut std::io::Cursor::new(&mut bytes), ImageFormat::Png)
.expect("fixture png should encode");
let source = DownloadedImage {
bytes: bytes.into(),
bytes,
mime_type: "image/png".to_string(),
extension: "png".to_string(),
};
+7 -4
View File
@@ -7,7 +7,7 @@ use bytes::Bytes;
use image::{GenericImageView, ImageFormat, ImageReader};
use platform_image::{
GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD, RAW_IMAGE_MAX_EDGE, RAW_IMAGE_MAX_PIXELS,
RawImageEditOptions, ReferenceImage, create_vector_engine_raw_image_edit,
RawImageEditImage, RawImageEditOptions, create_vector_engine_raw_image_edit,
validate_raw_image_edit_dimensions,
};
use serde::Serialize;
@@ -162,7 +162,7 @@ pub(crate) async fn edit_raw_image(
}
struct PreparedRawImageEdit {
image: ReferenceImage,
image: RawImageEditImage,
prompt: String,
options: RawImageEditOptions,
width: u32,
@@ -367,7 +367,10 @@ fn validate_optional_value<const N: usize>(
Err(bad_request(format!("{field} 值无效")))
}
fn decode_image(value: RawImageData, field: &str) -> Result<(ReferenceImage, u32, u32), AppError> {
fn decode_image(
value: RawImageData,
field: &str,
) -> Result<(RawImageEditImage, 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!(
@@ -405,7 +408,7 @@ fn decode_image(value: RawImageData, field: &str) -> Result<(ReferenceImage, u32
}
let (width, height) = decoded.dimensions();
Ok((
ReferenceImage {
RawImageEditImage {
bytes,
file_name: value.file_name,
mime_type,
@@ -34,7 +34,7 @@ pub fn slice_generated_asset_sheet(
let grid_size_u32 = u32::try_from(grid_size).map_err(|_| {
GeneratedAssetSheetError::invalid_request("系列素材图集的 n 超出可支持范围。")
})?;
let source = image::load_from_memory(image.bytes.as_ref()).map_err(|error| {
let source = image::load_from_memory(image.bytes.as_slice()).map_err(|error| {
GeneratedAssetSheetError::decode_image(format!("系列素材图集解码失败:{error}"))
})?;
let source = apply_generated_asset_sheet_green_screen_alpha(source);
@@ -94,7 +94,7 @@ pub fn slice_generated_asset_sheet_two_items_per_row(
let grid_size_u32 = u32::try_from(grid_size).map_err(|_| {
GeneratedAssetSheetError::invalid_request("系列素材图集的 n 超出可支持范围。")
})?;
let source = image::load_from_memory(image.bytes.as_ref()).map_err(|error| {
let source = image::load_from_memory(image.bytes.as_slice()).map_err(|error| {
GeneratedAssetSheetError::decode_image(format!("系列素材图集解码失败:{error}"))
})?;
let source = apply_generated_asset_sheet_green_screen_alpha(source);
@@ -243,7 +243,7 @@ pub fn prepare_generated_icon_spritesheet_all_by_connected_components(
"图标 spritesheet 累计裁剪像素上限必须大于 0。",
));
}
let source = image::load_from_memory(image.bytes.as_ref()).map_err(|error| {
let source = image::load_from_memory(image.bytes.as_slice()).map_err(|error| {
GeneratedAssetSheetError::decode_image(format!("图标 spritesheet 解码失败:{error}"))
})?;
let source = apply_generated_asset_sheet_green_screen_alpha(source);
@@ -263,7 +263,7 @@ pub fn prepare_generated_icon_spritesheet_all_by_connected_components(
pub fn prepare_generated_icon_spritesheet_grid_2x2(
image: &crate::DownloadedImage,
) -> Result<GeneratedAssetSheetConnectedIconPlan, GeneratedAssetSheetError> {
let source = image::load_from_memory(image.bytes.as_ref()).map_err(|error| {
let source = image::load_from_memory(image.bytes.as_slice()).map_err(|error| {
GeneratedAssetSheetError::decode_image(format!("图标 spritesheet 解码失败:{error}"))
})?;
let source = apply_generated_asset_sheet_green_screen_alpha(source).into_rgba8();
@@ -942,7 +942,7 @@ mod tests {
}
}
crate::DownloadedImage {
bytes: encode_png(sheet).into(),
bytes: encode_png(sheet),
mime_type: "image/png".to_string(),
extension: "png".to_string(),
}
@@ -1031,7 +1031,7 @@ mod tests {
sheet.put_pixel(x0 + 40, y0 + 8, Rgba(color));
}
let source = crate::DownloadedImage {
bytes: encode_png(sheet).into(),
bytes: encode_png(sheet),
mime_type: "image/png".to_string(),
extension: "png".to_string(),
};
@@ -1063,7 +1063,7 @@ mod tests {
}
let source = crate::DownloadedImage {
bytes: encode_png(sheet).into(),
bytes: encode_png(sheet),
mime_type: "image/png".to_string(),
extension: "png".to_string(),
};
@@ -1095,7 +1095,7 @@ mod tests {
}
let source = crate::DownloadedImage {
bytes: encode_png(sheet).into(),
bytes: encode_png(sheet),
mime_type: "image/png".to_string(),
extension: "png".to_string(),
};
@@ -1137,7 +1137,7 @@ mod tests {
}
let source = crate::DownloadedImage {
bytes: encode_png(sheet).into(),
bytes: encode_png(sheet),
mime_type: "image/png".to_string(),
extension: "png".to_string(),
};
@@ -1165,7 +1165,7 @@ mod tests {
}
let source = crate::DownloadedImage {
bytes: encode_png(sheet).into(),
bytes: encode_png(sheet),
mime_type: "image/png".to_string(),
extension: "png".to_string(),
};
@@ -1206,7 +1206,7 @@ mod tests {
}
let source = crate::DownloadedImage {
bytes: encode_png(sheet).into(),
bytes: encode_png(sheet),
mime_type: "image/png".to_string(),
extension: "png".to_string(),
};
@@ -1240,7 +1240,7 @@ mod tests {
}
let source = crate::DownloadedImage {
bytes: encode_png(sheet).into(),
bytes: encode_png(sheet),
mime_type: "image/png".to_string(),
extension: "png".to_string(),
};
@@ -1272,7 +1272,7 @@ mod tests {
}
let source = crate::DownloadedImage {
bytes: encode_png(sheet).into(),
bytes: encode_png(sheet),
mime_type: "image/png".to_string(),
extension: "png".to_string(),
};
@@ -1303,7 +1303,7 @@ mod tests {
}
let source = crate::DownloadedImage {
bytes: encode_png(sheet).into(),
bytes: encode_png(sheet),
mime_type: "image/png".to_string(),
extension: "png".to_string(),
};
+3 -3
View File
@@ -12,9 +12,9 @@ pub use vector_engine::{
DownloadedImage, GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD, GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_MODEL,
GeneratedImages, NANOBANANA_2_MODEL, PlatformImageError, PlatformImageFailureAudit,
PlatformImageStatusHint, RAW_IMAGE_DIMENSION_ALIGNMENT, RAW_IMAGE_MAX_EDGE,
RAW_IMAGE_MAX_PIXELS, RAW_IMAGE_MIN_PIXELS, RawImageEditDimensionError, RawImageEditOptions,
RawImageEditResult, ReferenceImage, VECTOR_ENGINE_GPT_IMAGE_2_MODEL, VECTOR_ENGINE_PROVIDER,
VectorEngineImageSettings, build_vector_engine_image_http_client,
RAW_IMAGE_MAX_PIXELS, RAW_IMAGE_MIN_PIXELS, RawImageEditDimensionError, RawImageEditImage,
RawImageEditOptions, RawImageEditResult, ReferenceImage, VECTOR_ENGINE_GPT_IMAGE_2_MODEL,
VECTOR_ENGINE_PROVIDER, VectorEngineImageSettings, build_vector_engine_image_http_client,
build_vector_engine_image_request_body,
build_vector_engine_nanobanana_generate_content_request_body, create_vector_engine_image_edit,
create_vector_engine_image_edit_with_references,
@@ -266,7 +266,7 @@ fn decode_rgba_source(
return Err(PixelArtSnapError::InvalidInput(format!("{input} 为空")));
}
let dimension_reader = image::ImageReader::new(Cursor::new(source.bytes.as_ref()))
let dimension_reader = image::ImageReader::new(Cursor::new(source.bytes.as_slice()))
.with_guessed_format()
.map_err(|error| PixelArtSnapError::Decode {
input,
@@ -281,7 +281,7 @@ fn decode_rgba_source(
})?;
validate_dimensions(width, height, input)?;
let mut reader = image::ImageReader::new(Cursor::new(source.bytes.as_ref()))
let mut reader = image::ImageReader::new(Cursor::new(source.bytes.as_slice()))
.with_guessed_format()
.map_err(|error| PixelArtSnapError::Decode {
input,
@@ -465,7 +465,7 @@ fn encode_png(image: RgbaImage) -> Result<DownloadedImage, PixelArtSnapError> {
.write_to(&mut cursor, ImageFormat::Png)
.map_err(|error| PixelArtSnapError::Encode(error.to_string()))?;
Ok(DownloadedImage {
bytes: cursor.into_inner().into(),
bytes: cursor.into_inner(),
mime_type: "image/png".to_string(),
extension: "png".to_string(),
})
@@ -21,14 +21,15 @@ pub use client::{
create_vector_engine_nanobanana_generate_content,
};
pub use constants::{
GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD, GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL,
VECTOR_ENGINE_GPT_IMAGE_2_MODEL, VECTOR_ENGINE_PROVIDER,
GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL, VECTOR_ENGINE_GPT_IMAGE_2_MODEL,
VECTOR_ENGINE_PROVIDER,
};
pub use error::{PlatformImageError, PlatformImageStatusHint};
pub use image_source::download_remote_image;
pub use raw_edit::{
RAW_IMAGE_DIMENSION_ALIGNMENT, RAW_IMAGE_MAX_EDGE, RAW_IMAGE_MAX_PIXELS, RAW_IMAGE_MIN_PIXELS,
RawImageEditDimensionError, RawImageEditOptions, create_vector_engine_raw_image_edit,
GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD, RAW_IMAGE_DIMENSION_ALIGNMENT, RAW_IMAGE_MAX_EDGE,
RAW_IMAGE_MAX_PIXELS, RAW_IMAGE_MIN_PIXELS, RawImageEditDimensionError, RawImageEditImage,
RawImageEditOptions, RawImageEditResult, create_vector_engine_raw_image_edit,
validate_raw_image_edit_dimensions,
};
pub use request::{
@@ -38,6 +39,4 @@ pub use request::{
vector_engine_nanobanana_generate_content_url,
};
pub use transport::build_vector_engine_image_http_client;
pub use types::{
DownloadedImage, GeneratedImages, RawImageEditResult, ReferenceImage, VectorEngineImageSettings,
};
pub use types::{DownloadedImage, GeneratedImages, ReferenceImage, VectorEngineImageSettings};
@@ -1,18 +1,16 @@
use std::time::{Duration, Instant};
use bytes::Bytes;
use reqwest::multipart::{Form, Part};
use serde::Deserialize;
use super::{
audit::build_failure_audit,
budget::{effective_request_timeout_ms, request_budget_exhausted_error},
constants::{
GPT_IMAGE_2_DIMENSION_ALIGNMENT, GPT_IMAGE_2_MAX_EDGE, GPT_IMAGE_2_MAX_PIXELS,
GPT_IMAGE_2_MIN_PIXELS, GPT_IMAGE_2_MODEL, VECTOR_ENGINE_PROVIDER,
},
constants::{GPT_IMAGE_2_MODEL, VECTOR_ENGINE_PROVIDER},
error::PlatformImageError,
request::vector_engine_images_edit_url,
types::{RawImageEditResult, ReferenceImage, VectorEngineImageSettings},
types::VectorEngineImageSettings,
util::truncate_raw,
};
@@ -23,9 +21,27 @@ pub struct RawImageEditOptions {
pub output_format: Option<String>,
pub width: u32,
pub height: u32,
pub mask: Option<ReferenceImage>,
pub mask: Option<RawImageEditImage>,
}
#[derive(Clone, Debug)]
pub struct RawImageEditImage {
pub bytes: Bytes,
pub mime_type: String,
pub file_name: String,
}
#[derive(Clone, Debug)]
pub struct RawImageEditResult {
pub b64_images: Vec<String>,
pub recovered_failure_audits: Vec<super::audit::PlatformImageFailureAudit>,
}
const GPT_IMAGE_2_MIN_PIXELS: u64 = 655_360;
const GPT_IMAGE_2_MAX_PIXELS: u64 = 8_294_400;
const GPT_IMAGE_2_MAX_EDGE: u32 = 3_840;
const GPT_IMAGE_2_DIMENSION_ALIGNMENT: u32 = 16;
#[derive(Debug, Deserialize)]
struct RawImageEditResponsePayload {
data: Vec<RawImageEditResponseEntry>,
@@ -40,6 +56,7 @@ pub const RAW_IMAGE_MAX_EDGE: u32 = GPT_IMAGE_2_MAX_EDGE;
pub const RAW_IMAGE_DIMENSION_ALIGNMENT: u32 = GPT_IMAGE_2_DIMENSION_ALIGNMENT;
pub const RAW_IMAGE_MIN_PIXELS: u64 = GPT_IMAGE_2_MIN_PIXELS;
pub const RAW_IMAGE_MAX_PIXELS: u64 = GPT_IMAGE_2_MAX_PIXELS;
pub const GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD: u32 = 1536;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RawImageEditDimensionError {
@@ -104,7 +121,7 @@ pub fn validate_raw_image_edit_dimensions(
pub async fn create_vector_engine_raw_image_edit(
settings: &VectorEngineImageSettings,
prompt: &str,
image: ReferenceImage,
image: RawImageEditImage,
options: RawImageEditOptions,
failure_context: &str,
) -> Result<RawImageEditResult, PlatformImageError> {
@@ -126,7 +143,7 @@ pub async fn create_vector_engine_raw_image_edit(
reference_image_count,
));
};
let ReferenceImage {
let RawImageEditImage {
bytes: image_bytes,
file_name: image_file_name,
mime_type: image_mime_type,
@@ -139,7 +156,7 @@ pub async fn create_vector_engine_raw_image_edit(
// GPT-Image-2 原生只返回 b64_json;不要添加 URL 下载或 response_format 兼容分支。
.part(
"image",
Part::bytes(image_bytes.to_vec())
Part::stream(reqwest::Body::from(image_bytes))
.file_name(image_file_name)
.mime_str(image_mime_type.as_str())
.map_err(|error| invalid_request(failure_context, error.to_string()))?,
@@ -156,7 +173,7 @@ pub async fn create_vector_engine_raw_image_edit(
if let Some(mask) = options.mask {
form = form.part(
"mask",
Part::bytes(mask.bytes.to_vec())
Part::stream(reqwest::Body::from(mask.bytes))
.file_name(mask.file_name)
.mime_str(mask.mime_type.as_str())
.map_err(|error| invalid_request(failure_context, error.to_string()))?,
@@ -42,7 +42,7 @@ fn build_test_sheet(width: u32, height: u32) -> DownloadedImage {
}
DownloadedImage {
bytes: encode_image(sheet).into(),
bytes: encode_image(sheet),
mime_type: "image/png".to_string(),
extension: "png".to_string(),
}
@@ -259,7 +259,7 @@ async fn vector_engine_image_edit_retries_send_timeout_once_and_succeeds() {
let http_client =
build_vector_engine_image_http_client(&settings).expect("client should build");
let reference_image = ReferenceImage {
bytes: bytes::Bytes::from_static(b"reference"),
bytes: b"reference".to_vec(),
mime_type: "image/png".to_string(),
file_name: "reference.png".to_string(),
};
@@ -598,7 +598,7 @@ async fn vector_engine_image_edit_falls_back_when_preferred_model_is_unsupported
let http_client =
build_vector_engine_image_http_client(&settings).expect("client should build");
let reference = ReferenceImage {
bytes: bytes::Bytes::from_static(b"reference"),
bytes: b"reference".to_vec(),
mime_type: "image/png".to_string(),
file_name: "reference.png".to_string(),
};