feat(compress): improve quality guardrails and format conversion

This commit is contained in:
2026-02-08 00:12:27 +08:00
parent f8955d8a6c
commit 65387ca846
8 changed files with 233 additions and 102 deletions

View File

@@ -19,7 +19,7 @@
<ul class="mt-2 list-disc space-y-1 pl-5">
<li>Base URL<code>https://ys.workyai.cn/api/v1</code></li>
<li>认证方式<code>X-API-Key</code>推荐 <code>Authorization: Bearer &lt;token&gt;</code></li>
<li>支持格式PNG / JPG / JPEG / WebP / AVIF / GIF静态/ BMP / TIFF / ICO</li>
<li>支持格式PNG / JPG / JPEG / WebP / AVIF / GIF静态/ BMP / TIFF / ICO支持 output_format 转码</li>
<li>压缩率<code>compression_rate</code> 1-100表示压缩后体积占原图比例100 为不压缩</li>
<li>计量成功压缩 1 个文件计 1 若体积未变小或压缩率为 100则不扣额度</li>
</ul>
@@ -28,14 +28,15 @@
<div class="rounded-xl border border-slate-200 bg-white p-5 text-sm text-slate-700">
<div class="font-medium text-slate-900">同步压缩返回 JSON + 下载链接</div>
<div class="mt-2 text-xs text-slate-500">
<code>POST /compress</code>表单字段<code>file</code><code>compression_rate</code>
<code>max_width</code><code>max_height</code><code>preserve_metadata</code>
<code>POST /compress</code>表单字段<code>file</code><code>compression_rate</code>/<code>target_size_bytes</code>
<code>output_format</code><code>max_width</code><code>max_height</code><code>preserve_metadata</code>
</div>
<pre class="mt-3 overflow-auto rounded-lg bg-slate-950 p-4 text-xs text-slate-100"><code>curl -X POST \\
-H \"X-API-Key: if_live_xxx\" \\
-F \"file=@./demo.jpg\" \\
-F \"compression_rate=20\" \\
-F \"max_width=2000\" \\
<pre class="mt-3 overflow-auto rounded-lg bg-slate-950 p-4 text-xs text-slate-100"><code>curl -X POST \
-H "X-API-Key: if_live_xxx" \
-F "file=@./demo.jpg" \
-F "compression_rate=20" \
-F "output_format=webp" \
-F "max_width=2000" \
https://ys.workyai.cn/api/v1/compress</code></pre>
<div class="mt-3 text-xs text-slate-500">
返回字段包含 <code>download_url</code><code>saved_percent</code> <code>billing.units_charged</code>
@@ -45,12 +46,13 @@
<div class="rounded-xl border border-slate-200 bg-white p-5 text-sm text-slate-700">
<div class="font-medium text-slate-900">同步压缩直接返回二进制</div>
<div class="mt-2 text-xs text-slate-500">
<code>POST /compress/direct</code>响应头包含原/压缩大小与扣费信息
<code>POST /compress/direct</code>支持同样参数 output_formattarget_size_bytes响应头包含原/压缩大小与扣费信息
</div>
<pre class="mt-3 overflow-auto rounded-lg bg-slate-950 p-4 text-xs text-slate-100"><code>curl -X POST \\
-H \"X-API-Key: if_live_xxx\" \\
-F \"file=@./demo.jpg\" \\
-F \"compression_rate=20\" \\
<pre class="mt-3 overflow-auto rounded-lg bg-slate-950 p-4 text-xs text-slate-100"><code>curl -X POST \
-H "X-API-Key: if_live_xxx" \
-F "file=@./demo.jpg" \
-F "target_size_bytes=120000" \
-F "output_format=jpeg" \
https://ys.workyai.cn/api/v1/compress/direct -o out.jpg -D headers.txt</code></pre>
<div class="mt-3 text-xs text-slate-500">
重点响应头<code>ImageForge-Original-Size</code>
@@ -64,16 +66,16 @@
<div class="mt-2 text-xs text-slate-500">
<code>POST /compress/batch</code>上传多文件后返回任务 ID
</div>
<pre class="mt-3 overflow-auto rounded-lg bg-slate-950 p-4 text-xs text-slate-100"><code>curl -X POST \\
-H \"X-API-Key: if_live_xxx\" \\
-F \"files[]=@./a.jpg\" \\
-F \"files[]=@./b.jpg\" \\
-F \"compression_rate=30\" \\
<pre class="mt-3 overflow-auto rounded-lg bg-slate-950 p-4 text-xs text-slate-100"><code>curl -X POST \
-H "X-API-Key: if_live_xxx" \
-F "files[]=@./a.jpg" \
-F "files[]=@./b.jpg" \
-F "compression_rate=30" \
https://ys.workyai.cn/api/v1/compress/batch</code></pre>
<div class="mt-3 text-xs text-slate-500">
轮询任务<code>GET /compress/tasks/&lt;task_id&gt;</code>响应包含每个文件的 <code>download_url</code> <code>download_all_url</code>ZIP
</div>
<pre class="mt-3 overflow-auto rounded-lg bg-slate-950 p-4 text-xs text-slate-100"><code>curl -H \"X-API-Key: if_live_xxx\" \\
<pre class="mt-3 overflow-auto rounded-lg bg-slate-950 p-4 text-xs text-slate-100"><code>curl -H "X-API-Key: if_live_xxx" \
https://ys.workyai.cn/api/v1/compress/tasks/550e8400-e29b-41d4-a716-446655440200</code></pre>
</div>
@@ -82,10 +84,10 @@
<div class="mt-2 text-xs text-slate-500">
下载地址不在 <code>/api/v1</code> 需带同样的认证头
</div>
<pre class="mt-3 overflow-auto rounded-lg bg-slate-950 p-4 text-xs text-slate-100"><code>curl -H \"X-API-Key: if_live_xxx\" \\
<pre class="mt-3 overflow-auto rounded-lg bg-slate-950 p-4 text-xs text-slate-100"><code>curl -H "X-API-Key: if_live_xxx" \
-L https://ys.workyai.cn/downloads/&lt;file_id&gt; -o result.jpg
curl -H \"X-API-Key: if_live_xxx\" \\
curl -H "X-API-Key: if_live_xxx" \
-L https://ys.workyai.cn/downloads/tasks/&lt;task_id&gt; -o batch.zip</code></pre>
</div>

View File

@@ -21,12 +21,14 @@ interface UploadItem {
const auth = useAuthStore()
type CompressionMode = 'percent' | 'size'
type OutputFormatOption = 'auto' | 'png' | 'jpeg' | 'webp' | 'avif' | 'gif' | 'bmp' | 'tiff' | 'ico'
const options = reactive({
mode: 'percent' as CompressionMode, // 压缩模式:百分比 / 目标大小
compressionRate: 60,
targetSize: '' as string, // 目标大小数值
targetUnit: 'KB' as 'KB' | 'MB', // 目标大小单位
outputFormat: 'auto' as OutputFormatOption,
maxWidth: '' as string,
maxHeight: '' as string,
})
@@ -114,7 +116,9 @@ 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
const bytes = options.targetUnit === 'MB' ? size * 1024 * 1024 : size * 1024
if (!Number.isFinite(bytes) || bytes < 1024) return undefined
return Math.round(bytes)
}
async function runOne(item: UploadItem) {
@@ -122,14 +126,25 @@ async function runOne(item: UploadItem) {
item.error = undefined
try {
const targetSizeBytes = getTargetSizeBytes()
if (options.mode === 'size' && !targetSizeBytes) {
item.status = 'error'
item.error = '请填写有效目标大小(至少 1KB'
return
}
const outputFormat = options.outputFormat === 'auto' ? undefined : options.outputFormat
const compressOptions = options.mode === 'percent'
? {
compression_rate: options.compressionRate,
output_format: outputFormat,
max_width: toInt(options.maxWidth),
max_height: toInt(options.maxHeight),
}
: {
target_size_bytes: getTargetSizeBytes(),
target_size_bytes: targetSizeBytes,
output_format: outputFormat,
max_width: toInt(options.maxWidth),
max_height: toInt(options.maxHeight),
}
@@ -542,6 +557,25 @@ async function resendVerification() {
<div class="text-xs text-slate-500">直接指定压缩后的目标大小系统会自动调整质量以逼近目标 JPEG/WebP/AVIF 支持</div>
</div>
<label class="space-y-1">
<div class="text-xs font-medium text-slate-600">输出格式</div>
<select
v-model="options.outputFormat"
class="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm text-slate-800"
>
<option value="auto">保持原格式推荐</option>
<option value="jpeg">JPEG</option>
<option value="png">PNG</option>
<option value="webp">WebP</option>
<option value="avif">AVIF</option>
<option value="gif">GIF仅静态</option>
<option value="bmp">BMP</option>
<option value="tiff">TIFF</option>
<option value="ico">ICO</option>
</select>
<div class="text-xs text-slate-500">支持按需转码目标大小模式建议配合 JPEG/WebP/AVIF</div>
</label>
<div class="grid grid-cols-2 gap-3">
<label class="space-y-1">
<div class="text-xs font-medium text-slate-600">最大宽度px</div>