云端 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,否则逻辑尺寸与文件像素尺寸会混淆。生产环境若使用其他比例,另加一组对应尺寸断言。

把返回的 PNG 数据用 ImageIO 解码,统一绘制到相同的 sRGB 上下文。不要把 UIImage.size 直接当作输出像素数。涉及主线程绘制的测试应在主执行器运行,避免测试自身引入不确定行为。

再补一张带抗锯齿斜边的夹具。检查它确实存在 Alpha 介于零和最大值之间的边缘像素,再分别铺到黑白底;不要绑定某个系统版本下的全部边缘字节。

失败时留下能定位的问题证据

每次失败至少保存原始 PNG、解码后的尺寸、指定的颜色空间、采样坐标、预期与实际 RGBA,以及工具链版本。测试用合成素材即可,不需要上传用户图片。

遇到尺寸正确但全部不透明,先检查绘制上下文的 opaque 设置与背景填充;遇到 Alpha 正确但边缘变暗,检查是否重复预乘;遇到整体颜色偏移,先核对颜色空间转换,再讨论像素容差。

最终门禁分成三条独立结论:尺寸一致、透明度保留、背景合成符合契约。文件能打开只是前置条件。共享像素逻辑可以在云端 Mac 自动运行,实际 UIKit 导出与分享后的最终文件仍要单独验收,防止中间环节重新编码。

常见问题

PNG 有 Alpha 通道,就能证明透明背景没有丢失吗?

不能。通道可能存在但全部像素的 Alpha 都为最大值。必须回读透明和半透明样本,并检查其 Alpha 数值与合成结果。

云端 Mac 上的像素测试可以替代 iOS 真机验收吗?

不能完全替代。它适合验证共享编码逻辑和文件像素;涉及 UIKit 渲染、显示色域和实际分享路径时,还需运行 iOS 测试并检查最终交付文件。

给下一次构建分配一台云端 Mac

独享物理 Mac mini,非虚拟机。按天、周、月、季选择租期,配置与实际可用状态以控制台返回为准。