贴纸导出后出现黑边,常见原因不是 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,非虚拟机。按天、周、月、季选择租期,配置与实际可用状态以控制台返回为准。