fix: harden compression boundaries and target search
This commit is contained in:
@@ -19,9 +19,10 @@
|
||||
<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 <token></code></li>
|
||||
<li>支持格式:PNG / JPG / JPEG / WebP / AVIF / GIF(静态)/ BMP / TIFF / ICO(支持 output_format 转码)</li>
|
||||
<li>支持格式:PNG / JPG / JPEG / WebP / AVIF / GIF / BMP / TIFF / ICO(仅静态图片,支持 output_format 转码)</li>
|
||||
<li>压缩率:<code>compression_rate</code> 1-100;JPEG/WebP/AVIF 以该比例为体积上限,无损格式为尽力优化</li>
|
||||
<li>计量:成功压缩 1 个文件计 1 次;若体积未变小或压缩率为 100,则不扣额度</li>
|
||||
<li>目标体积:<code>target_size_bytes</code> 仅支持 JPEG/WebP/AVIF,且不能与 <code>compression_rate</code> 同时提交</li>
|
||||
<li>计量:输出体积变小时计 1 次;仅同格式、无缩放的 <code>compression_rate=100</code> 原样请求免计量</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -40,6 +41,7 @@
|
||||
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>。
|
||||
目标小于清晰度保护边界时返回 <code>INVALID_REQUEST</code>,不会返回超过目标的文件。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -95,6 +97,7 @@ curl -H "X-API-Key: if_live_xxx" \
|
||||
<div class="font-medium text-slate-900">错误处理与常见问题</div>
|
||||
<ul class="mt-2 list-disc space-y-1 pl-5">
|
||||
<li><code>INVALID_REQUEST</code>:参数或文件不合法(检查格式与字段)</li>
|
||||
<li><code>UNSUPPORTED_FORMAT</code>:不支持该格式或输入为动画图片</li>
|
||||
<li><code>QUOTA_EXCEEDED</code>:额度不足(升级套餐或等待周期重置)</li>
|
||||
<li><code>FORBIDDEN</code>:无权限下载该任务/文件(认证不一致)</li>
|
||||
<li><code>EMAIL_NOT_VERIFIED</code>:登录用户需先完成邮箱验证</li>
|
||||
|
||||
@@ -3,7 +3,14 @@ import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { zipSync } from 'fflate'
|
||||
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { compressFile, getSubscription, getUsage, sendVerification, type CompressResponse } from '@/services/api'
|
||||
import {
|
||||
compressFile,
|
||||
getSubscription,
|
||||
getUsage,
|
||||
sendVerification,
|
||||
type CompressResponse,
|
||||
type OutputFormat,
|
||||
} from '@/services/api'
|
||||
import { ApiError } from '@/services/http'
|
||||
import { formatBytes } from '@/utils/format'
|
||||
|
||||
@@ -22,6 +29,7 @@ const auth = useAuthStore()
|
||||
|
||||
type CompressionMode = 'percent' | 'size'
|
||||
type OutputFormatOption = 'auto' | 'png' | 'jpeg' | 'webp' | 'avif' | 'gif' | 'bmp' | 'tiff' | 'ico'
|
||||
const targetSizeFormats = new Set<OutputFormat>(['jpeg', 'webp', 'avif'])
|
||||
|
||||
const options = reactive({
|
||||
mode: 'percent' as CompressionMode, // 压缩模式:百分比 / 目标大小
|
||||
@@ -121,6 +129,17 @@ function getTargetSizeBytes(): number | undefined {
|
||||
return Math.round(bytes)
|
||||
}
|
||||
|
||||
function setCompressionMode(mode: CompressionMode) {
|
||||
options.mode = mode
|
||||
if (
|
||||
mode === 'size'
|
||||
&& options.outputFormat !== 'auto'
|
||||
&& !targetSizeFormats.has(options.outputFormat)
|
||||
) {
|
||||
options.outputFormat = 'webp'
|
||||
}
|
||||
}
|
||||
|
||||
async function runOne(item: UploadItem) {
|
||||
item.status = 'compressing'
|
||||
item.error = undefined
|
||||
@@ -133,7 +152,15 @@ async function runOne(item: UploadItem) {
|
||||
return
|
||||
}
|
||||
|
||||
const outputFormat = options.outputFormat === 'auto' ? undefined : options.outputFormat
|
||||
const outputFormat: OutputFormat | undefined = options.outputFormat === 'auto'
|
||||
? (options.mode === 'size' ? 'webp' : undefined)
|
||||
: options.outputFormat
|
||||
|
||||
if (options.mode === 'size' && outputFormat && !targetSizeFormats.has(outputFormat)) {
|
||||
item.status = 'error'
|
||||
item.error = '按目标大小仅支持 JPEG、WebP 或 AVIF 输出'
|
||||
return
|
||||
}
|
||||
|
||||
const compressOptions = options.mode === 'percent'
|
||||
? {
|
||||
@@ -347,7 +374,7 @@ async function resendVerification() {
|
||||
<div class="rounded-xl border border-slate-200 bg-white p-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm font-medium text-slate-900">上传图片</div>
|
||||
<div class="text-xs text-slate-500">支持 PNG / JPG / JPEG / WebP / AVIF / GIF / BMP / TIFF / ICO(GIF 仅静态)</div>
|
||||
<div class="text-xs text-slate-500">支持 PNG / JPG / JPEG / WebP / AVIF / GIF / BMP / TIFF / ICO(仅静态图片)</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-col gap-3">
|
||||
@@ -503,7 +530,7 @@ async function resendVerification() {
|
||||
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'"
|
||||
@click="setCompressionMode('percent')"
|
||||
>
|
||||
按百分比
|
||||
</button>
|
||||
@@ -511,7 +538,7 @@ async function resendVerification() {
|
||||
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'"
|
||||
@click="setCompressionMode('size')"
|
||||
>
|
||||
按目标大小
|
||||
</button>
|
||||
@@ -556,7 +583,9 @@ async function resendVerification() {
|
||||
<option value="MB">MB</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="text-xs text-slate-500">直接指定压缩后的目标大小,系统会自动调整质量以逼近目标(仅 JPEG/WebP/AVIF 支持)。</div>
|
||||
<div class="text-xs text-slate-500">
|
||||
仅支持 JPEG/WebP/AVIF;保持原格式时会自动输出 WebP,过小且无法保证清晰度的目标会被拒绝。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="space-y-1">
|
||||
@@ -565,15 +594,15 @@ async function resendVerification() {
|
||||
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="auto">{{ options.mode === 'size' ? '自动选择 WebP(推荐)' : '保持原格式(推荐)' }}</option>
|
||||
<option value="jpeg">JPEG</option>
|
||||
<option value="png">PNG</option>
|
||||
<option value="png" :disabled="options.mode === 'size'">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>
|
||||
<option value="gif" :disabled="options.mode === 'size'">GIF(仅静态)</option>
|
||||
<option value="bmp" :disabled="options.mode === 'size'">BMP</option>
|
||||
<option value="tiff" :disabled="options.mode === 'size'">TIFF</option>
|
||||
<option value="ico" :disabled="options.mode === 'size'">ICO</option>
|
||||
</select>
|
||||
<div class="text-xs text-slate-500">支持按需转码。目标大小模式建议配合 JPEG/WebP/AVIF。</div>
|
||||
</label>
|
||||
|
||||
Reference in New Issue
Block a user