feat: 支持按目标大小压缩图片
- 新增 target_size_bytes 参数,支持直接指定压缩后的目标大小(字节) - 实现自动缩放算法:当仅调整质量无法达到目标时,自动缩小图片尺寸 - 前端新增压缩模式切换:百分比模式 / 目标大小模式 - 支持 KB/MB 单位选择 - 优化二分搜索算法,提高目标大小的精准度 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,8 +20,13 @@ interface UploadItem {
|
|||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
|
||||||
|
type CompressionMode = 'percent' | 'size'
|
||||||
|
|
||||||
const options = reactive({
|
const options = reactive({
|
||||||
|
mode: 'percent' as CompressionMode, // 压缩模式:百分比 / 目标大小
|
||||||
compressionRate: 60,
|
compressionRate: 60,
|
||||||
|
targetSize: '' as string, // 目标大小数值
|
||||||
|
targetUnit: 'KB' as 'KB' | 'MB', // 目标大小单位
|
||||||
maxWidth: '' as string,
|
maxWidth: '' as string,
|
||||||
maxHeight: '' as string,
|
maxHeight: '' as string,
|
||||||
})
|
})
|
||||||
@@ -105,18 +110,33 @@ function toInt(v: string): number | undefined {
|
|||||||
return Math.floor(n)
|
return Math.floor(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTargetSizeBytes(): number | undefined {
|
||||||
|
if (options.mode !== 'size') return undefined
|
||||||
|
const size = Number(options.targetSize)
|
||||||
|
if (!Number.isFinite(size) || size <= 0) return undefined
|
||||||
|
return options.targetUnit === 'MB' ? size * 1024 * 1024 : size * 1024
|
||||||
|
}
|
||||||
|
|
||||||
async function runOne(item: UploadItem) {
|
async function runOne(item: UploadItem) {
|
||||||
item.status = 'compressing'
|
item.status = 'compressing'
|
||||||
item.error = undefined
|
item.error = undefined
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const compressOptions = options.mode === 'percent'
|
||||||
|
? {
|
||||||
|
compression_rate: options.compressionRate,
|
||||||
|
max_width: toInt(options.maxWidth),
|
||||||
|
max_height: toInt(options.maxHeight),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
target_size_bytes: getTargetSizeBytes(),
|
||||||
|
max_width: toInt(options.maxWidth),
|
||||||
|
max_height: toInt(options.maxHeight),
|
||||||
|
}
|
||||||
|
|
||||||
const result = await compressFile(
|
const result = await compressFile(
|
||||||
item.file,
|
item.file,
|
||||||
{
|
compressOptions,
|
||||||
compression_rate: options.compressionRate,
|
|
||||||
max_width: toInt(options.maxWidth),
|
|
||||||
max_height: toInt(options.maxHeight),
|
|
||||||
},
|
|
||||||
auth.token,
|
auth.token,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -460,7 +480,31 @@ async function resendVerification() {
|
|||||||
<div class="text-sm font-medium text-slate-900">压缩参数</div>
|
<div class="text-sm font-medium text-slate-900">压缩参数</div>
|
||||||
|
|
||||||
<div class="mt-4 grid grid-cols-1 gap-4">
|
<div class="mt-4 grid grid-cols-1 gap-4">
|
||||||
<label class="space-y-1">
|
<!-- 压缩模式切换 -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="text-xs font-medium text-slate-600">压缩模式</div>
|
||||||
|
<div class="flex rounded-lg border border-slate-200 p-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex-1 rounded-md px-3 py-1.5 text-xs font-medium transition"
|
||||||
|
:class="options.mode === 'percent' ? 'bg-indigo-600 text-white' : 'text-slate-600 hover:bg-slate-100'"
|
||||||
|
@click="options.mode = 'percent'"
|
||||||
|
>
|
||||||
|
按百分比
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex-1 rounded-md px-3 py-1.5 text-xs font-medium transition"
|
||||||
|
:class="options.mode === 'size' ? 'bg-indigo-600 text-white' : 'text-slate-600 hover:bg-slate-100'"
|
||||||
|
@click="options.mode = 'size'"
|
||||||
|
>
|
||||||
|
按目标大小
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 百分比模式 -->
|
||||||
|
<label v-if="options.mode === 'percent'" class="space-y-1">
|
||||||
<div class="flex items-center justify-between text-xs font-medium text-slate-600">
|
<div class="flex items-center justify-between text-xs font-medium text-slate-600">
|
||||||
<span>压缩率</span>
|
<span>压缩率</span>
|
||||||
<span class="text-slate-500">{{ options.compressionRate }}%</span>
|
<span class="text-slate-500">{{ options.compressionRate }}%</span>
|
||||||
@@ -476,6 +520,28 @@ async function resendVerification() {
|
|||||||
<div class="text-xs text-slate-500">数值越小压缩越强,目标为压缩后体积占原图比例(100% 为不压缩)。</div>
|
<div class="text-xs text-slate-500">数值越小压缩越强,目标为压缩后体积占原图比例(100% 为不压缩)。</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<!-- 目标大小模式 -->
|
||||||
|
<div v-else class="space-y-1">
|
||||||
|
<div class="text-xs font-medium text-slate-600">目标大小</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input
|
||||||
|
v-model="options.targetSize"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
placeholder="例如 50"
|
||||||
|
class="flex-1 rounded-md border border-slate-200 bg-white px-3 py-2 text-sm text-slate-800"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
v-model="options.targetUnit"
|
||||||
|
class="rounded-md border border-slate-200 bg-white px-3 py-2 text-sm text-slate-800"
|
||||||
|
>
|
||||||
|
<option value="KB">KB</option>
|
||||||
|
<option value="MB">MB</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-slate-500">直接指定压缩后的目标大小,系统会自动调整质量以逼近目标(仅 JPEG/WebP/AVIF 支持)。</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<label class="space-y-1">
|
<label class="space-y-1">
|
||||||
<div class="text-xs font-medium text-slate-600">最大宽度(px)</div>
|
<div class="text-xs font-medium text-slate-600">最大宽度(px)</div>
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export interface CompressResponse {
|
|||||||
export interface CompressOptions {
|
export interface CompressOptions {
|
||||||
level?: CompressionLevel
|
level?: CompressionLevel
|
||||||
compression_rate?: number
|
compression_rate?: number
|
||||||
|
target_size_bytes?: number // 新增:直接指定目标大小(字节)
|
||||||
output_format?: OutputFormat
|
output_format?: OutputFormat
|
||||||
max_width?: number
|
max_width?: number
|
||||||
max_height?: number
|
max_height?: number
|
||||||
@@ -96,6 +97,7 @@ export async function compressFile(file: File, options: CompressOptions, token?:
|
|||||||
|
|
||||||
if (options.level) form.append('level', options.level)
|
if (options.level) form.append('level', options.level)
|
||||||
if (options.compression_rate) form.append('compression_rate', String(options.compression_rate))
|
if (options.compression_rate) form.append('compression_rate', String(options.compression_rate))
|
||||||
|
if (options.target_size_bytes) form.append('target_size_bytes', String(options.target_size_bytes))
|
||||||
if (options.output_format) form.append('output_format', options.output_format)
|
if (options.output_format) form.append('output_format', options.output_format)
|
||||||
if (options.max_width) form.append('max_width', String(options.max_width))
|
if (options.max_width) form.append('max_width', String(options.max_width))
|
||||||
if (options.max_height) form.append('max_height', String(options.max_height))
|
if (options.max_height) form.append('max_height', String(options.max_height))
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ struct CompressRequest {
|
|||||||
file_bytes: Vec<u8>,
|
file_bytes: Vec<u8>,
|
||||||
level: CompressionLevel,
|
level: CompressionLevel,
|
||||||
compression_rate: Option<u8>,
|
compression_rate: Option<u8>,
|
||||||
|
target_size_bytes: Option<u64>, // 新增:直接指定目标大小(字节)
|
||||||
output_format: Option<ImageFmt>,
|
output_format: Option<ImageFmt>,
|
||||||
max_width: Option<u32>,
|
max_width: Option<u32>,
|
||||||
max_height: Option<u32>,
|
max_height: Option<u32>,
|
||||||
@@ -246,6 +247,7 @@ async fn compress_json(
|
|||||||
format_out,
|
format_out,
|
||||||
effective_level,
|
effective_level,
|
||||||
req.compression_rate,
|
req.compression_rate,
|
||||||
|
req.target_size_bytes, // 新增:目标大小
|
||||||
req.max_width,
|
req.max_width,
|
||||||
req.max_height,
|
req.max_height,
|
||||||
req.preserve_metadata,
|
req.preserve_metadata,
|
||||||
@@ -603,6 +605,7 @@ async fn compress_direct(
|
|||||||
format_out,
|
format_out,
|
||||||
effective_level,
|
effective_level,
|
||||||
req.compression_rate,
|
req.compression_rate,
|
||||||
|
req.target_size_bytes, // 新增:目标大小
|
||||||
req.max_width,
|
req.max_width,
|
||||||
req.max_height,
|
req.max_height,
|
||||||
req.preserve_metadata,
|
req.preserve_metadata,
|
||||||
@@ -835,6 +838,7 @@ async fn parse_single_file_request(multipart: &mut Multipart) -> Result<Compress
|
|||||||
let mut level = CompressionLevel::Medium;
|
let mut level = CompressionLevel::Medium;
|
||||||
let mut output_format: Option<ImageFmt> = None;
|
let mut output_format: Option<ImageFmt> = None;
|
||||||
let mut compression_rate: Option<u8> = None;
|
let mut compression_rate: Option<u8> = None;
|
||||||
|
let mut target_size_bytes: Option<u64> = None; // 新增
|
||||||
let mut max_width: Option<u32> = None;
|
let mut max_width: Option<u32> = None;
|
||||||
let mut max_height: Option<u32> = None;
|
let mut max_height: Option<u32> = None;
|
||||||
let mut preserve_metadata = false;
|
let mut preserve_metadata = false;
|
||||||
@@ -905,6 +909,24 @@ async fn parse_single_file_request(multipart: &mut Multipart) -> Result<Compress
|
|||||||
"1" | "true" | "yes" | "y" | "on"
|
"1" | "true" | "yes" | "y" | "on"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
"target_size_bytes" | "target_size" => {
|
||||||
|
let v = text.trim();
|
||||||
|
if !v.is_empty() {
|
||||||
|
target_size_bytes = Some(
|
||||||
|
v.parse::<u64>()
|
||||||
|
.map_err(|_| AppError::new(ErrorCode::InvalidRequest, "target_size_bytes 格式错误,需为正整数(字节)"))?,
|
||||||
|
);
|
||||||
|
// 最小目标大小限制:1KB
|
||||||
|
if let Some(size) = target_size_bytes {
|
||||||
|
if size < 1024 {
|
||||||
|
return Err(AppError::new(
|
||||||
|
ErrorCode::InvalidRequest,
|
||||||
|
"target_size_bytes 最小为 1024(1KB)",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -917,6 +939,7 @@ async fn parse_single_file_request(multipart: &mut Multipart) -> Result<Compress
|
|||||||
file_bytes,
|
file_bytes,
|
||||||
level,
|
level,
|
||||||
compression_rate,
|
compression_rate,
|
||||||
|
target_size_bytes, // 新增
|
||||||
output_format,
|
output_format,
|
||||||
max_width,
|
max_width,
|
||||||
max_height,
|
max_height,
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ pub async fn compress_image_bytes(
|
|||||||
format_out: ImageFmt,
|
format_out: ImageFmt,
|
||||||
level: CompressionLevel,
|
level: CompressionLevel,
|
||||||
compression_rate: Option<u8>,
|
compression_rate: Option<u8>,
|
||||||
|
target_size_bytes: Option<u64>, // 新增:直接指定目标大小(字节)
|
||||||
max_width: Option<u32>,
|
max_width: Option<u32>,
|
||||||
max_height: Option<u32>,
|
max_height: Option<u32>,
|
||||||
preserve_metadata: bool,
|
preserve_metadata: bool,
|
||||||
@@ -203,7 +204,11 @@ pub async fn compress_image_bytes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let retention_rate = effective_rate(compression_rate, level);
|
let retention_rate = effective_rate(compression_rate, level);
|
||||||
let target_size = compression_rate.map(|value| target_size_from_rate(original_size, value));
|
// 优先使用直接指定的目标大小,其次根据百分比计算
|
||||||
|
let target_size = match target_size_bytes {
|
||||||
|
Some(bytes) => Some(bytes),
|
||||||
|
None => compression_rate.map(|value| target_size_from_rate(original_size, value)),
|
||||||
|
};
|
||||||
|
|
||||||
if compression_rate == Some(100)
|
if compression_rate == Some(100)
|
||||||
&& format_in == format_out
|
&& format_in == format_out
|
||||||
@@ -439,24 +444,157 @@ fn encode_avif_raw(raw: &[u8], w: u32, h: u32, quality: u8) -> Result<Vec<u8>, A
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn encode_jpeg_target(image: DynamicImage, target_size: u64) -> Result<Vec<u8>, AppError> {
|
fn encode_jpeg_target(image: DynamicImage, target_size: u64) -> Result<Vec<u8>, AppError> {
|
||||||
let rgb = image.to_rgb8();
|
encode_with_auto_resize(image, target_size, 1, 95, |img, q| {
|
||||||
let (w, h) = rgb.dimensions();
|
let rgb = img.to_rgb8();
|
||||||
let raw = rgb.into_raw();
|
let (w, h) = rgb.dimensions();
|
||||||
encode_target_quality(35, 95, target_size, |q| encode_jpeg_raw(&raw, w, h, q))
|
encode_jpeg_raw(rgb.as_raw(), w, h, q)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn encode_webp_target(image: DynamicImage, target_size: u64) -> Result<Vec<u8>, AppError> {
|
fn encode_webp_target(image: DynamicImage, target_size: u64) -> Result<Vec<u8>, AppError> {
|
||||||
let rgba = image.to_rgba8();
|
encode_with_auto_resize(image, target_size, 1, 95, |img, q| {
|
||||||
let (w, h) = rgba.dimensions();
|
let rgba = img.to_rgba8();
|
||||||
let raw = rgba.into_raw();
|
let (w, h) = rgba.dimensions();
|
||||||
encode_target_quality(30, 95, target_size, |q| encode_webp_raw(&raw, w, h, q))
|
encode_webp_raw(rgba.as_raw(), w, h, q)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn encode_avif_target(image: DynamicImage, target_size: u64) -> Result<Vec<u8>, AppError> {
|
fn encode_avif_target(image: DynamicImage, target_size: u64) -> Result<Vec<u8>, AppError> {
|
||||||
let rgba = image.to_rgba8();
|
encode_with_auto_resize(image, target_size, 1, 95, |img, q| {
|
||||||
let (w, h) = rgba.dimensions();
|
let rgba = img.to_rgba8();
|
||||||
let raw = rgba.into_raw();
|
let (w, h) = rgba.dimensions();
|
||||||
encode_target_quality(35, 90, target_size, |q| encode_avif_raw(&raw, w, h, q))
|
encode_avif_raw(rgba.as_raw(), w, h, q)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 支持自动缩放尺寸的目标大小压缩
|
||||||
|
/// 当仅调整质量无法达到目标大小时,自动缩小图片尺寸
|
||||||
|
fn encode_with_auto_resize<F>(
|
||||||
|
image: DynamicImage,
|
||||||
|
target_size: u64,
|
||||||
|
min_q: u8,
|
||||||
|
max_q: u8,
|
||||||
|
mut encode_fn: F,
|
||||||
|
) -> Result<Vec<u8>, AppError>
|
||||||
|
where
|
||||||
|
F: FnMut(&DynamicImage, u8) -> Result<Vec<u8>, AppError>,
|
||||||
|
{
|
||||||
|
let (orig_w, orig_h) = image.dimensions();
|
||||||
|
let min_dimension = 16u32; // 最小尺寸限制
|
||||||
|
|
||||||
|
// 首先尝试用最低质量压缩原始尺寸
|
||||||
|
let min_q_result = encode_fn(&image, min_q)?;
|
||||||
|
if min_q_result.len() as u64 <= target_size {
|
||||||
|
// 最低质量已满足,用二分法找最佳质量
|
||||||
|
return encode_target_quality_with_image(&image, min_q, max_q, target_size, &mut encode_fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 需要缩放:根据当前大小和目标大小计算缩放比例
|
||||||
|
let current_size = min_q_result.len() as u64;
|
||||||
|
// 文件大小大致与像素数成正比,所以尺寸缩放系数 = sqrt(目标大小/当前大小)
|
||||||
|
let scale = ((target_size as f64 / current_size as f64).sqrt() * 0.9).min(1.0); // 0.9 为安全系数
|
||||||
|
|
||||||
|
let mut best_result = min_q_result;
|
||||||
|
let mut best_is_under = false;
|
||||||
|
|
||||||
|
// 尝试多个缩放级别
|
||||||
|
let scales = [scale, scale * 0.8, scale * 0.6, scale * 0.4, 0.3, 0.2, 0.1];
|
||||||
|
|
||||||
|
for &s in &scales {
|
||||||
|
let new_w = ((orig_w as f64 * s).round() as u32).max(min_dimension);
|
||||||
|
let new_h = ((orig_h as f64 * s).round() as u32).max(min_dimension);
|
||||||
|
|
||||||
|
if new_w < min_dimension && new_h < min_dimension {
|
||||||
|
break; // 达到最小尺寸
|
||||||
|
}
|
||||||
|
|
||||||
|
let resized = image.resize(new_w, new_h, image::imageops::FilterType::Lanczos3);
|
||||||
|
|
||||||
|
// 对缩放后的图片进行二分质量搜索
|
||||||
|
let result = encode_target_quality_with_image(&resized, min_q, max_q, target_size, &mut encode_fn)?;
|
||||||
|
let result_size = result.len() as u64;
|
||||||
|
|
||||||
|
if result_size <= target_size {
|
||||||
|
// 找到满足条件的结果
|
||||||
|
if !best_is_under || result_size > best_result.len() as u64 {
|
||||||
|
// 优先选择更大的(更接近目标且不超过)
|
||||||
|
best_result = result;
|
||||||
|
best_is_under = true;
|
||||||
|
}
|
||||||
|
break; // 已找到满足条件的最大尺寸
|
||||||
|
} else if !best_is_under && result_size < best_result.len() as u64 {
|
||||||
|
// 还没找到满足条件的,保存最接近的
|
||||||
|
best_result = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(best_result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 对给定图片进行二分质量搜索
|
||||||
|
fn encode_target_quality_with_image<F>(
|
||||||
|
image: &DynamicImage,
|
||||||
|
min_q: u8,
|
||||||
|
max_q: u8,
|
||||||
|
target_size: u64,
|
||||||
|
encode_fn: &mut F,
|
||||||
|
) -> Result<Vec<u8>, AppError>
|
||||||
|
where
|
||||||
|
F: FnMut(&DynamicImage, u8) -> Result<Vec<u8>, AppError>,
|
||||||
|
{
|
||||||
|
let mut best: Option<Vec<u8>> = None;
|
||||||
|
let mut best_diff = u64::MAX;
|
||||||
|
let mut best_is_under = false;
|
||||||
|
let mut best_size = 0u64;
|
||||||
|
|
||||||
|
let mut consider = |bytes: Vec<u8>| {
|
||||||
|
let size = bytes.len() as u64;
|
||||||
|
let is_under = size <= target_size;
|
||||||
|
let diff = if size > target_size {
|
||||||
|
size - target_size
|
||||||
|
} else {
|
||||||
|
target_size - size
|
||||||
|
};
|
||||||
|
|
||||||
|
let should_update = match (best_is_under, is_under) {
|
||||||
|
(false, true) => true,
|
||||||
|
(true, false) => false,
|
||||||
|
_ => diff < best_diff,
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_update {
|
||||||
|
best_diff = diff;
|
||||||
|
best_is_under = is_under;
|
||||||
|
best_size = size;
|
||||||
|
best = Some(bytes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 先尝试两端
|
||||||
|
consider(encode_fn(image, min_q)?);
|
||||||
|
if min_q != max_q {
|
||||||
|
consider(encode_fn(image, max_q)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 二分查找
|
||||||
|
let mut low = min_q;
|
||||||
|
let mut high = max_q;
|
||||||
|
for _ in 0..10 {
|
||||||
|
if low > high {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let mid = (low + high) / 2;
|
||||||
|
let bytes = encode_fn(image, mid)?;
|
||||||
|
let size = bytes.len() as u64;
|
||||||
|
consider(bytes);
|
||||||
|
if size > target_size {
|
||||||
|
high = mid.saturating_sub(1);
|
||||||
|
} else {
|
||||||
|
low = mid.saturating_add(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
best.ok_or_else(|| AppError::new(ErrorCode::CompressionFailed, "压缩失败"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn encode_target_quality<F>(
|
fn encode_target_quality<F>(
|
||||||
@@ -470,35 +608,53 @@ where
|
|||||||
{
|
{
|
||||||
let mut best: Option<Vec<u8>> = None;
|
let mut best: Option<Vec<u8>> = None;
|
||||||
let mut best_diff = u64::MAX;
|
let mut best_diff = u64::MAX;
|
||||||
|
let mut best_is_under = false; // 记录最佳结果是否小于目标
|
||||||
|
let mut best_size = 0u64;
|
||||||
|
|
||||||
let mut consider = |bytes: Vec<u8>| {
|
// 考虑一个候选结果
|
||||||
|
let mut consider = |bytes: Vec<u8>, best: &mut Option<Vec<u8>>, best_diff: &mut u64, best_is_under: &mut bool, best_size: &mut u64| {
|
||||||
let size = bytes.len() as u64;
|
let size = bytes.len() as u64;
|
||||||
|
let is_under = size <= target_size;
|
||||||
let diff = if size > target_size {
|
let diff = if size > target_size {
|
||||||
size - target_size
|
size - target_size
|
||||||
} else {
|
} else {
|
||||||
target_size - size
|
target_size - size
|
||||||
};
|
};
|
||||||
if diff < best_diff {
|
|
||||||
best_diff = diff;
|
// 优先选择不超过目标大小的结果
|
||||||
best = Some(bytes);
|
let should_update = match (*best_is_under, is_under) {
|
||||||
|
(false, true) => true, // 当前小于目标,之前大于目标 -> 更新
|
||||||
|
(true, false) => false, // 当前大于目标,之前小于目标 -> 不更新
|
||||||
|
_ => diff < *best_diff, // 同类情况,选择更接近的
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_update {
|
||||||
|
*best_diff = diff;
|
||||||
|
*best_is_under = is_under;
|
||||||
|
*best_size = size;
|
||||||
|
*best = Some(bytes);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
consider(encode(min_q)?);
|
// 先尝试两端
|
||||||
|
let bytes = encode(min_q)?;
|
||||||
|
consider(bytes, &mut best, &mut best_diff, &mut best_is_under, &mut best_size);
|
||||||
if min_q != max_q {
|
if min_q != max_q {
|
||||||
consider(encode(max_q)?);
|
let bytes = encode(max_q)?;
|
||||||
|
consider(bytes, &mut best, &mut best_diff, &mut best_is_under, &mut best_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 二分查找,增加迭代次数到 12 次以提高精度
|
||||||
let mut low = min_q;
|
let mut low = min_q;
|
||||||
let mut high = max_q;
|
let mut high = max_q;
|
||||||
for _ in 0..7 {
|
for _ in 0..12 {
|
||||||
if low > high {
|
if low > high {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let mid = (low + high) / 2;
|
let mid = (low + high) / 2;
|
||||||
let bytes = encode(mid)?;
|
let bytes = encode(mid)?;
|
||||||
let size = bytes.len() as u64;
|
let size = bytes.len() as u64;
|
||||||
consider(bytes);
|
consider(bytes, &mut best, &mut best_diff, &mut best_is_under, &mut best_size);
|
||||||
if size > target_size {
|
if size > target_size {
|
||||||
high = mid.saturating_sub(1);
|
high = mid.saturating_sub(1);
|
||||||
} else {
|
} else {
|
||||||
@@ -506,6 +662,19 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 精细调整:如果当前结果超出目标太多,尝试更低质量
|
||||||
|
if best_size > target_size {
|
||||||
|
let mut q = min_q;
|
||||||
|
while q <= min_q.saturating_add(5) && q <= max_q {
|
||||||
|
let bytes = encode(q)?;
|
||||||
|
consider(bytes, &mut best, &mut best_diff, &mut best_is_under, &mut best_size);
|
||||||
|
if best_size <= target_size {
|
||||||
|
break; // 已找到满足条件的结果
|
||||||
|
}
|
||||||
|
q = q.saturating_add(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
best.ok_or_else(|| AppError::new(ErrorCode::CompressionFailed, "压缩失败"))
|
best.ok_or_else(|| AppError::new(ErrorCode::CompressionFailed, "压缩失败"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -384,6 +384,7 @@ async fn process_task_file(
|
|||||||
format_out,
|
format_out,
|
||||||
level,
|
level,
|
||||||
compression_rate,
|
compression_rate,
|
||||||
|
None, // target_size_bytes: worker 批量任务不支持精确大小
|
||||||
max_width,
|
max_width,
|
||||||
max_height,
|
max_height,
|
||||||
ctx.preserve_metadata,
|
ctx.preserve_metadata,
|
||||||
|
|||||||
Reference in New Issue
Block a user