From fa64bfb4759cc883a282c425bcf063600a6bb466 Mon Sep 17 00:00:00 2001 From: Linghong Date: Fri, 10 Jul 2026 08:38:07 +0000 Subject: [PATCH] =?UTF-8?q?=E6=BA=90=E5=9B=BE=E8=A7=A3=E7=A0=81=E5=8A=A0?= =?UTF-8?q?=E5=B0=BA=E5=AF=B8/=E5=88=86=E9=85=8D=E4=B8=8A=E9=99=90?= =?UTF-8?q?=EF=BC=8C=E5=A0=B5=E4=BD=8F=E8=A7=A3=E5=8E=8B=E7=82=B8=E5=BC=B9?= =?UTF-8?q?=E9=A3=8E=E9=99=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 两处在缩图前直接 image::load_from_memory:背景色过滤的参考图(请求体 data URL) 与阿里云抠图源图。缺省 image::Limits 无尺寸上限、仅 512MiB alloc 兜底,12MB 请求体 即可构造巨幅压缩图在解码阶段撑爆内存,逐帧动画并发时叠加。 改用带 Limits 的 ImageReader(边长 ≤8192、alloc ≤128MiB),与结果解码同口径。 Co-Authored-By: Claude Opus 4.8 --- .../src/editor_screen_background_filter.rs | 25 ++++++++++++- server-rs/crates/platform-matting/src/lib.rs | 37 ++++++++++++------- 2 files changed, 46 insertions(+), 16 deletions(-) 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 {