云端 Mac 上验收 iOS JPEG 导出体积预算:质量搜索与失败边界

云端 Mac 上验收 iOS JPEG 导出体积预算:质量搜索与失败边界

上传头像时,本地预览正常,服务端却拒绝文件:问题往往不是扩展名,而是 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

先租一天,再决定要不要买。

把连接手感和实际任务一起验证,再选择适合自己的使用周期。