雲端 Mac 上驗證 iOS JPEG 輸出容量上限:品質搜尋與失敗邊界

雲端 Mac 上驗證 iOS JPEG 輸出容量上限:品質搜尋與失敗邊界

上傳頭像時,本機預覽正常,伺服器卻拒收檔案:問題往往不在副檔名,而是 JPEG 編碼後的位元組數超過 API 限制。將品質參數設為某個固定值,只能改善部分圖片,無法保證每張都符合限制。在雲端 Mac 上,可以先將輸出邏輯做成獨立的驗收指令稿,再把同一組驗收契約移入 iOS 測試目標。

先定義容量上限,別把品質當成比例

如果 API 以位元組數限制容量,用戶端也應以整數儲存上限,避免混用十進位與二進位單位。品質值是編碼器的輸入參數,不是輸出大小的百分比;即使像素尺寸相同,純色背景與細密紋理的壓縮結果仍會有明顯差異。

本文的前提是:輸入影像已完成方向正規化、透明背景處理與色彩空間選擇,輸出階段不改變像素尺寸。依品質由高到低逐一嘗試有限的級距,回傳第一個符合容量上限的結果。這個結果只是「候選級距中優先順序最高的合格項目」,不代表已找到連續區間內的最佳品質。

檔案大小是必須檢查的輸出條件,畫面品質則是另一項驗收條件。符合前者,不代表文字邊緣、膚色或漸層已經合格。

在遠端 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 上的測試通過就能取代 iOS 驗證嗎?

不能。Mac 適合快速檢查搜尋邏輯,目標 iOS 執行環境仍須跑相同契約測試;不應要求不同系統產生逐位元組相同的 JPEG。

用實際工作,測試遠端 Mac。

租用一台專屬使用的 Mac mini M4,透過 macOS 圖形介面與命令列操作。適合先測試連線和工作流程;若需要長時間離線運作或連接專業硬體,也可以評估自行購買。

租用專屬 Mac mini

先租一天,再決定要不要買。

實際測試連線體驗和工作任務,再選擇適合自己的使用週期。