源图解码加尺寸/分配上限,堵住解压炸弹风险
两处在缩图前直接 image::load_from_memory:背景色过滤的参考图(请求体 data URL) 与阿里云抠图源图。缺省 image::Limits 无尺寸上限、仅 512MiB alloc 兜底,12MB 请求体 即可构造巨幅压缩图在解码阶段撑爆内存,逐帧动画并发时叠加。 改用带 Limits 的 ImageReader(边长 ≤8192、alloc ≤128MiB),与结果解码同口径。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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<Vec<u8>> {
|
||||
.filter(|bytes| !bytes.is_empty())
|
||||
}
|
||||
|
||||
/// 有界解码参考图:套上尺寸 / 分配上限,防止解压炸弹在缩图前撑爆内存。
|
||||
fn decode_analysis_image_within_limits(bytes: &[u8]) -> Result<image::DynamicImage, String> {
|
||||
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<ScreenBackgroundColorSafetyReport, String> {
|
||||
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());
|
||||
|
||||
@@ -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<Vec<u8>, 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<image::DynamicImage, MattingError> {
|
||||
decode_image_within_limits(bytes)
|
||||
.map_err(|error| MattingError::InvalidRequest(format!("解析待抠图图片失败:{error}")))
|
||||
}
|
||||
|
||||
/// 解码抠图结果时套上尺寸 / 分配上限,防止上游用巨幅尺寸声明在解码阶段撑爆内存。
|
||||
fn decode_result_image(bytes: &[u8]) -> Result<image::DynamicImage, MattingError> {
|
||||
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<image::DynamicImage, String> {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user