上传头像时,本地预览正常,服务端却拒绝文件:问题往往不是扩展名,而是 JPEG 编码后的字节数超过接口限制。把质量参数改成某个固定值只能改善部分图片,无法形成保证。在云端 Mac 上,可以先把导出逻辑做成独立验收脚本,再将同一组契约迁入 iOS 测试目标。
先定义预算,不把质量当比例
接口如果按字节限额,客户端也应使用整数保存上限,避免把十进制单位与二进制单位混用。质量值是编码器的输入,不是输出大小的百分比;同样尺寸下,纯色背景与细密纹理的压缩结果会明显不同。
本文约定:输入已经完成方向归一化、透明背景处理与色彩空间选择,导出阶段不改变像素尺寸。按质量从高到低遍历有限档位,返回首个满足预算的结果。这个结果只是“候选档位中优先级最高的合格项”,不宣称找到连续区间里的最优质量。
文件大小是必须检查的输出条件,画面质量是另一项验收条件。满足前者,不代表文字边缘、肤色或渐变已经合格。
用有限搜索替代无限降质
在远程 macOS 终端中保存以下内容为 jpeg-budget.swift。需要已配置可用的 Swift 工具链,脚本只使用系统框架,不读取私人照片,也不修改机器设置。
import Foundation
import CoreGraphics
import ImageIO
import UniformTypeIdentifiers
enum ExportError: Error {
case invalidBudget
case encoding
case budgetUnreachable
case decoding
}
func encodeJPEG(_ image: CGImage, quality: Double) throws -> Data {
let buffer = NSMutableData()
guard let destination = CGImageDestinationCreateWithData(
buffer as CFMutableData,
UTType.jpeg.identifier as CFString,
1,
nil
) else {
throw ExportError.encoding
}
let properties = [
kCGImageDestinationLossyCompressionQuality: quality
] as CFDictionary
CGImageDestinationAddImage(destination, image, properties)
guard CGImageDestinationFinalize(destination) else {
throw ExportError.encoding
}
return buffer as Data
}
func exportJPEG(
_ image: CGImage,
maxBytes: Int
) throws -> (data: Data, quality: Double) {
guard maxBytes > 0 else {
throw ExportError.invalidBudget
}
let levels = [0.9, 0.8, 0.7, 0.6, 0.5]
for quality in levels {
let data = try encodeJPEG(image, quality: quality)
if data.count <= maxBytes {
return (data, quality)
}
}
throw ExportError.budgetUnreachable
}
func verify(_ data: Data, width: Int, height: Int) throws {
guard let source = CGImageSourceCreateWithData(
data as CFData, nil
),
CGImageSourceGetType(source) == UTType.jpeg.identifier as CFString,
let decoded = CGImageSourceCreateImageAtIndex(source, 0, nil),
decoded.width == width,
decoded.height == height else {
throw ExportError.decoding
}
}
let space = CGColorSpace(name: CGColorSpace.sRGB)!
let context = CGContext(
data: nil,
width: 320,
height: 240,
bitsPerComponent: 8,
bytesPerRow: 0,
space: space,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
)!
context.setFillColor(
CGColor(red: 0.2, green: 0.4, blue: 0.6, alpha: 1)
)
context.fill(CGRect(x: 0, y: 0, width: 320, height: 240))
let image = context.makeImage()!
let result = try exportJPEG(image, maxBytes: 1_000_000)
precondition(result.data.count <= 1_000_000)
try verify(result.data, width: 320, height: 240)
do {
_ = try exportJPEG(image, maxBytes: 1)
preconditionFailure("Expected budget rejection")
} catch ExportError.budgetUnreachable {
}
执行 xcrun swift jpeg-budget.swift,成功时没有输出,失败时以非零状态结束。这里用纯色图验证流程,不把它作为真实压缩能力样本。代码中的强制解包只用于固定测试夹具;接收外部图片的应用代码应返回明确错误。
检查失败路径,而不只检查成功文件
脚本区分了非法预算、编码失败和预算不可达。三者不能统一变成空文件:非法预算属于调用错误,编码失败需要调查输入或编码器,而预算不可达应交给产品策略处理。
补充测试时,至少覆盖零与负数预算、恰好等于输出字节数的预算,以及所有档位都无法通过的情形。等号必须允许通过。不要直接断言“比最高质量结果少一个字节就一定失败”,因为后续档位仍可能满足条件。
单独验证搜索分支
把实际编码操作抽成可注入函数,用预设长度返回值验证搜索流程:第一档合格时只调用一次,中间档合格时停止继续编码,全部超限时尝试次数等于候选数。这样无需依赖某张图片恰好产生某个大小,也能确认提前退出没有失效。
不采用未经验证的二分搜索,是为了避免把尺寸严格单调作为隐藏前提。有限遍历的代价明确,但大图会重复编码,因此还应设置输入像素上限,并避免同时处理过多图片。
建立能暴露问题的图片夹具
测试集应同时包含纯色、细线文字、渐变和高细节纹理。可以程序化生成,也可以使用团队获准保存的固定样本;每份样本记录尺寸和内容摘要,禁止每次随机生成后不保留种子。
除检查 Data.count,还要回读确认格式和像素尺寸。损坏文件可能很小,却不是合格结果。JPEG 有损,不能断言解码后像素逐点相等;若项目需要自动评估视觉误差,应固定计算空间与容差,并保留人工复核样本。
同一图片在不同系统版本下不必产生相同哈希。更稳妥的断言是格式正确、尺寸不变、字节数合格和搜索次数有界,再记录实际质量档位供调查。
接回 iOS 导出流程与持续验收
Mac 脚本通过后,将函数放入应用可复用模块,并在目标 iOS 运行环境执行相同夹具。系统框架的编码结果可能变化,不能把 macOS 的成功直接当成 iOS 的证明。
如果全部档位失败,界面应明确提示无法按当前条件导出。允许缩图时,另设尺寸阶梯和最小边长,并明确告知用户;不要在质量搜索里偷偷改变尺寸。上传前若还会包装请求,应分别核对图片文件限额与整个请求体限额。
最终保留输入夹具标识、系统版本、候选档位、选中质量和输出字节数即可,不必记录原始私人图片。让预算约束、视觉要求与失败行为各有归属,导出问题才不会被一个不断降低的质量数字掩盖。
常见问题
把 JPEG 质量设为 0.5,文件就会缩小到一半吗?
不会。质量值控制编码取舍,不表示压缩比例。是否满足预算必须在编码完成后检查 Data.count,不能根据质量值推算。
最低质量仍超过预算时,应该继续降低到零吗?
不应无限尝试。用产品允许的有限质量档位搜索,全部失败就返回预算不可达;如果允许缩小像素尺寸,应进入另一个有尺寸下限的处理流程。
云端 Mac 上的 JPEG 测试通过,能替代 iOS 验收吗?
不能。Mac 脚本适合快速验证搜索与失败逻辑,同一组契约测试仍应在目标 iOS 运行环境执行,不应要求不同系统版本生成逐字节相同的 JPEG。
用自己的任务,验证远程 Mac。
租用一台整机独享的 Mac mini M4,使用 macOS 图形界面与命令行。适合先验证连接和工作流;需要长期离线作业或外接专业硬件时,也应比较自购。
租用独享 Mac mini