雲端 Mac 上的 iOS PNG 透明度匯出驗收

雲端 Mac 上的 iOS PNG 透明度匯出驗收

雲端 Mac 上的 iOS PNG 透明度匯出驗收

貼圖匯出後出現黑邊,通常不是 PNG 損毀,而是將預乘色彩誤當成非預乘色彩,或在繪製時先填入了不透明背景。在雲端 Mac 上驗收時,先用三個像素確立編碼契約,再檢查實際匯出的檔案。不要一開始就比較整張螢幕截圖,否則縮放、顯示背景與色彩轉換都會干擾判斷。

先確立像素契約

本文驗收的範圍是 SDR、8 位元、sRGB 的 PNG,不涵蓋廣色域與 HDR。準備一列三個像素:全透明、半透明紅色、完全不透明藍色。輸出尺寸必須維持 3×1,透明度也不能在編碼後重新讀取時遺失。

必須區分兩層表示方式。PNG 採用非預乘 Alpha 表示法;CoreGraphics 點陣圖繪圖環境則可以採用預乘表示法。在明確指定為預乘格式的 RGBA 緩衝區中,半透明紅色的紅色通道約為 128,Alpha 約為 128,而不是紅色通道為 255。

不要直接把 CGImage 的 dataProvider 位元組當成統一的 RGBA 格式。色彩空間、通道順序與每列位元組數都可能不同。應先繪製到自行定義的點陣圖繪圖環境,再讀取位元組。

全透明像素的 RGB 不列入驗收條件。它的色彩不可見,經過編碼與轉換後也不一定保留原值;只需檢查 Alpha 為零,以及合成後不會改變背景。

用 Swift 建立最小編碼回讀測試

在已安裝完整 Xcode 的節點上,將以下程式碼儲存為 alpha-check.swift。它只使用系統框架,產生 PNG 後再重新解碼,並明確設定位元組順序、色彩空間與預乘格式。輸出會寫入目前目錄,執行前請先切換至獨立的測試目錄。

import Foundation
import CoreGraphics
import ImageIO
import UniformTypeIdentifiers

let width = 3
let height = 1
let space = CGColorSpace(name: CGColorSpace.sRGB)!
let info = CGBitmapInfo.byteOrder32Big.rawValue
    | CGImageAlphaInfo.premultipliedLast.rawValue

func context() -> CGContext {
    CGContext(
        data: nil,
        width: width,
        height: height,
        bitsPerComponent: 8,
        bytesPerRow: width * 4,
        space: space,
        bitmapInfo: info
    )!
}

let source = context()
source.clear(CGRect(x: 0, y: 0, width: width, height: height))
source.setFillColor(red: 1, green: 0, blue: 0, alpha: 0.5)
source.fill(CGRect(x: 1, y: 0, width: 1, height: 1))
source.setFillColor(red: 0, green: 0, blue: 1, alpha: 1)
source.fill(CGRect(x: 2, y: 0, width: 1, height: 1))

let url = URL(fileURLWithPath: "alpha-fixture.png")
let destination = CGImageDestinationCreateWithURL(
    url as CFURL, UTType.png.identifier as CFString, 1, nil
)!
CGImageDestinationAddImage(destination, source.makeImage()!, nil)
precondition(CGImageDestinationFinalize(destination), "PNG write failed")

let reader = CGImageSourceCreateWithURL(url as CFURL, nil)!
let image = CGImageSourceCreateImageAtIndex(reader, 0, nil)!
precondition(image.width == width && image.height == height)

func pixels(background: CGFloat?) -> [UInt8] {
    let target = context()
    let bounds = CGRect(x: 0, y: 0, width: width, height: height)
    target.clear(bounds)
    if let value = background {
        target.setFillColor(
            red: value, green: value, blue: value, alpha: 1
        )
        target.fill(bounds)
    }
    target.interpolationQuality = .none
    target.draw(image, in: bounds)
    return Array(UnsafeBufferPointer(
        start: target.data!.assumingMemoryBound(to: UInt8.self),
        count: width * 4
    ))
}

func near(_ actual: UInt8, _ expected: Int) -> Bool {
    abs(Int(actual) - expected) <= 2
}

let rgba = pixels(background: nil)
precondition(rgba[3] == 0)
precondition(near(rgba[4], 128) && near(rgba[7], 128))
precondition(rgba[8] == 0 && rgba[10] == 255 && rgba[11] == 255)

let black = pixels(background: 0)
let white = pixels(background: 1)
precondition(near(black[4], 128) && black[5] == 0 && black[6] == 0)
precondition(white[4] == 255 && near(white[5], 127))
precondition(near(white[6], 127))
precondition(Array(white[0...3]) == [255, 255, 255, 255])
precondition(Array(black[0...3]) == [0, 0, 0, 255])

執行時請保留結束碼,不要透過圖片檢視器的背景顏色判斷是否成功:

mkdir -p alpha-check
cd alpha-check
xcrun swift ../alpha-check.swift

斷言失敗時,程式會以非零狀態結束。執行這份驗收指令碼時,不要使用會移除斷言檢查的最佳化選項。兩個色階的容許誤差是為了容納 8 位元量化誤差,不代表可以任意放寬誤差範圍。

用黑白背景區分兩類錯誤

在上述點陣圖繪圖環境中,半透明紅色疊到黑底上,預期約為 (128,0,0);疊到白底上,預期約為 (255,127,127)。這裡定義的是該繪圖環境的合成結果,不是對所有算繪器的通用色彩保證。

只測試黑底會漏掉某些問題。例如,匯出前若已填入黑色背景,檔案仍會顯示紅色,但透明像素已變成不透明的黑色像素。白底檢查能立刻揭露這類背景污染。

若將已預乘的紅色再乘一次 Alpha,紅色就會變暗,半透明輪廓也容易出現黑圈。反過來,若將非預乘色彩誤當成預乘色彩,邊緣又可能過亮。排查時應先確認緩衝區宣告,再檢查轉換發生的位置,不要試圖提高銳利度來補救。

將測試資料接入真正的 iOS 匯出流程

上述指令碼驗證的是工具與斷言是否可靠,不代表應用程式的匯出邏輯已通過驗收。下一步應將相同的三色測試資料送入正式環境使用的編碼函式,再讀回該函式傳回的資料,而不是再次呼叫範例編碼器。

單獨驗收 UIKit 繪製

若應用程式使用 UIGraphicsImageRenderer,請在 iOS 測試目標中明確設定 format.opaque = false。測試像素尺寸時,也應設定 format.scale = 1,避免混淆邏輯尺寸與檔案的像素尺寸。正式環境若使用其他縮放比例,請另外加入一組對應尺寸的斷言。

使用 ImageIO 解碼傳回的 PNG 資料,再統一繪製到相同的 sRGB 繪圖環境中。不要直接將 UIImage.size 當成輸出像素數。涉及主執行緒繪製的測試應在主執行者上執行,避免測試本身引入不確定行為。

再補上一張帶有反鋸齒斜邊的測試影像。確認其中確實存在 Alpha 介於零與最大值之間的邊緣像素,再分別合成到黑底與白底上;不要把某個系統版本產生的所有邊緣位元組固定為驗收標準。

失敗時保留足以定位問題的證據

每次失敗,至少應儲存原始 PNG、解碼後的尺寸、指定的色彩空間、取樣座標、預期與實際 RGBA,以及工具鏈版本。測試使用合成素材即可,不需要上傳使用者圖片。

若尺寸正確,但所有像素都不透明,先檢查繪圖環境的 opaque 設定與背景填色;若 Alpha 正確,但邊緣變暗,檢查是否重複預乘;若整體色彩偏移,先核對色彩空間轉換,再討論像素容許誤差。

最終驗收應分成三項獨立結論:尺寸一致、透明度保留、背景合成符合契約。檔案能開啟只是先決條件。共用的像素處理邏輯可以在雲端 Mac 上自動執行,但實際的 UIKit 匯出與分享後的最終檔案,仍須分別驗收,以防中間環節重新編碼。

常見問題

PNG 具備 Alpha 通道,就能證明透明背景沒有遺失嗎?

不能。通道可能存在,但所有像素的 Alpha 都是最大值。必須回讀透明與半透明樣本,檢查數值及背景合成結果。

雲端 Mac 的像素測試能取代 iOS 實機驗收嗎?

無法完全取代。它適合驗證共用編碼邏輯與檔案像素;UIKit 繪製、顯示色域及實際分享流程仍須透過 iOS 測試確認。

為下一次建置租用一台雲端 Mac

獨享實體 Mac mini,非虛擬機器。可選擇日租、週租、月租或季租;實際配置與可用狀態請以控制台資訊為準。