diff --git a/server-rs/crates/api-server/src/editor_screen_background_filter.rs b/server-rs/crates/api-server/src/editor_screen_background_filter.rs index f5e85b6d9..59c70a521 100644 --- a/server-rs/crates/api-server/src/editor_screen_background_filter.rs +++ b/server-rs/crates/api-server/src/editor_screen_background_filter.rs @@ -32,6 +32,11 @@ const DANGER_MASS_EXCLUSION_RATIO: f64 = 0.005; const BIN_MASS_FLOOR_RATIO: f64 = 0.001; /// 分析用缩略图最长边。 const ANALYSIS_MAX_EDGE: u32 = 192; +/// 参考图解码边长上限:源图直接来自请求体 data URL,缺省 `image::Limits` 无尺寸上限、 +/// 仅 512MiB alloc 兜底,12MB 请求体即可构造巨幅压缩图(解压炸弹)在缩图前撑爆内存。 +const ANALYSIS_SOURCE_MAX_DIMENSION: u32 = 8192; +/// 参考图解码分配上限。与 api-server BgFilter 结果解码口径一致(128MiB)。 +const ANALYSIS_SOURCE_MAX_ALLOC_BYTES: u64 = 128 * 1024 * 1024; /// 不透明图边框背景估计:主色覆盖率达到该值才认为边框是平坦背景。 const BORDER_BACKGROUND_MIN_COVERAGE: f64 = 0.5; /// 不透明图中与边框背景色 ΔE 不超过该值的像素按背景剔除。 @@ -87,6 +92,23 @@ pub(crate) fn decode_image_data_url(value: &str) -> Option> { .filter(|bytes| !bytes.is_empty()) } +/// 有界解码参考图:套上尺寸 / 分配上限,防止解压炸弹在缩图前撑爆内存。 +fn decode_analysis_image_within_limits(bytes: &[u8]) -> Result { + use std::io::Cursor; + + let mut reader = image::ImageReader::new(Cursor::new(bytes)) + .with_guessed_format() + .map_err(|error| format!("参考图无法识别格式:{error}"))?; + let mut limits = image::Limits::default(); + limits.max_image_width = Some(ANALYSIS_SOURCE_MAX_DIMENSION); + limits.max_image_height = Some(ANALYSIS_SOURCE_MAX_DIMENSION); + limits.max_alloc = Some(ANALYSIS_SOURCE_MAX_ALLOC_BYTES); + reader.limits(limits); + reader + .decode() + .map_err(|error| format!("参考图无法解码:{error}")) +} + /// 按参考图前景配色过滤候选背景色。`apply_skin_veto` 为 true(角色/动作类)时,检测到皮肤 bin /// 还会启用 Rule 2/3 皮肤硬否决。失败(图片不可解码、前景不足)返回原因字符串, /// 调用方应降级为不过滤,而不是阻断生成。 @@ -94,8 +116,7 @@ pub(crate) fn filter_editor_screen_background_colors( image_bytes: &[u8], apply_skin_veto: bool, ) -> Result { - let image = image::load_from_memory(image_bytes) - .map_err(|error| format!("参考图无法解码:{error}"))?; + let image = decode_analysis_image_within_limits(image_bytes)?; let (width, height) = image.dimensions(); if width == 0 || height == 0 { return Err("参考图尺寸为空".to_string()); diff --git a/server-rs/crates/platform-matting/src/lib.rs b/server-rs/crates/platform-matting/src/lib.rs index 80bdaf407..d99cee091 100644 --- a/server-rs/crates/platform-matting/src/lib.rs +++ b/server-rs/crates/platform-matting/src/lib.rs @@ -43,9 +43,9 @@ const MIN_INPUT_EDGE: u32 = 32; /// 与 api-server BgFilter 降级路径的 `EDITOR_BACKGROUND_REMOVAL_MAX_RESPONSE_BYTES` 对齐。 const MAX_RESULT_RESPONSE_BYTES: usize = 32 * 1024 * 1024; -/// 抠图结果解码的最大边长像素。防止解码阶段按声明的巨幅尺寸分配像素缓冲。 -/// 与 api-server BgFilter 路径的 `EDITOR_BACKGROUND_REMOVAL_MAX_IMAGE_DIMENSION` 对齐。 -const MAX_RESULT_IMAGE_DIMENSION: u32 = 8192; +/// 图片解码的最大边长像素(源图与抠图结果共用)。防止解码阶段按声明的巨幅尺寸分配 +/// 像素缓冲。与 api-server BgFilter 路径的 `EDITOR_BACKGROUND_REMOVAL_MAX_IMAGE_DIMENSION` 对齐。 +const MAX_DECODE_IMAGE_DIMENSION: u32 = 8192; #[derive(Clone, Debug)] pub struct MattingConfig { @@ -305,9 +305,7 @@ impl MattingClient { bytes: &[u8], file_name: &str, ) -> Result, MattingError> { - let source = image::load_from_memory(bytes).map_err(|error| { - MattingError::InvalidRequest(format!("解析待抠图图片失败:{error}")) - })?; + let source = decode_source_image(bytes)?; let source_rgba = source.to_rgba8(); let (source_width, source_height) = source_rgba.dimensions(); @@ -653,23 +651,34 @@ fn result_response_too_large_error() -> MattingError { )) } +/// 解码待抠图源图时套上尺寸 / 分配上限。源图直接来自请求体 / 生成产物,缺省 +/// `image::Limits` 无尺寸上限、仅 512MiB alloc 兜底,12MB 请求体即可构造巨幅压缩图 +/// (解压炸弹)在缩图前撑爆内存;逐帧动画并发调用时风险叠加。 +fn decode_source_image(bytes: &[u8]) -> Result { + decode_image_within_limits(bytes) + .map_err(|error| MattingError::InvalidRequest(format!("解析待抠图图片失败:{error}"))) +} + /// 解码抠图结果时套上尺寸 / 分配上限,防止上游用巨幅尺寸声明在解码阶段撑爆内存。 fn decode_result_image(bytes: &[u8]) -> Result { + decode_image_within_limits(bytes) + .map_err(|error| MattingError::Upstream(format!("解析抠图结果图片失败:{error}"))) +} + +/// 统一的有界解码:尺寸上限 `MAX_DECODE_IMAGE_DIMENSION`、分配上限 +/// `MAX_RESULT_RESPONSE_BYTES * 4`,覆盖源图与抠图结果两条解码路径。 +fn decode_image_within_limits(bytes: &[u8]) -> Result { use std::io::Cursor; let mut reader = image::ImageReader::new(Cursor::new(bytes)) .with_guessed_format() - .map_err(|error| { - MattingError::Upstream(format!("识别抠图结果格式失败:{error}")) - })?; + .map_err(|error| format!("识别图片格式失败:{error}"))?; let mut limits = image::Limits::default(); - limits.max_image_width = Some(MAX_RESULT_IMAGE_DIMENSION); - limits.max_image_height = Some(MAX_RESULT_IMAGE_DIMENSION); + limits.max_image_width = Some(MAX_DECODE_IMAGE_DIMENSION); + limits.max_image_height = Some(MAX_DECODE_IMAGE_DIMENSION); limits.max_alloc = Some(MAX_RESULT_RESPONSE_BYTES as u64 * 4); reader.limits(limits); - reader.decode().map_err(|error| { - MattingError::Upstream(format!("解析抠图结果图片失败:{error}")) - }) + reader.decode().map_err(|error| error.to_string()) } struct ViapiStsToken {