Как проверить прозрачность PNG для iOS на облачном Mac
Чёрная кайма вокруг экспортированного стикера обычно возникает не из-за повреждения PNG, а из-за того, что цвета с предумноженной альфой интерпретируются как цвета без предумножения, либо из-за непрозрачного фона, заранее залитого при рисовании. Перенесите проверку на облачный Mac: сначала зафиксируйте контракт кодирования на трёх пикселях, затем проверьте реальные экспортированные файлы. Не начинайте со сравнения полных скриншотов: иначе на результат одновременно повлияют масштабирование, фон отображения и преобразования цвета.
Сначала зафиксируйте пиксельный контракт
Здесь мы проверяем 8-битные PNG в sRGB и SDR. Широкий цветовой охват и HDR не рассматриваются. Подготовьте строку из трёх пикселей: полностью прозрачного, полупрозрачного красного и полностью непрозрачного синего. Размер результата должен оставаться 3×1, а прозрачность — сохраняться после кодирования и повторного чтения.
Важно различать два представления. В PNG используется альфа без предумножения; растровый контекст CoreGraphics может использовать предумноженное представление. В явно заданном предумноженном буфере RGBA у полупрозрачного красного пикселя красный канал и альфа равны примерно 128, а не 255 в красном канале.
Не считайте байты из dataProvider объекта CGImage универсальными данными RGBA. Цветовое пространство, порядок каналов и шаг строки могут отличаться. Сначала отрисуйте изображение в растровый контекст с явно заданными вами параметрами и только потом считывайте байты.
Значения RGB полностью прозрачного пикселя не входят в критерии проверки. Его цвет невидим, а после кодирования и преобразований исходные значения могут не сохраниться. Проверяйте только нулевую альфу и то, что при наложении пиксель не меняет фон.
Создайте минимальный тест повторного чтения на 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). Это контракт для результата композиции в данном контексте, а не универсальная гарантия цвета для всех средств рендеринга.
Проверка только на чёрном фоне пропускает некоторые ошибки. Например, если перед экспортом уже был залит чёрный фон, красный цвет в файле всё ещё будет виден, но прозрачные пиксели станут чёрными непрозрачными. Проверка на белом фоне сразу выявит такое попадание фона в изображение.
Если повторно умножить уже предумноженный красный канал на альфу, красный потемнеет, а вдоль полупрозрачного контура может появиться чёрный ореол. Если же принять цвет без предумножения за предумноженный, края могут оказаться слишком яркими. При диагностике сначала проверьте объявленный формат буфера, затем — места преобразований. Не пытайтесь исправить проблему повышением резкости.
Подключите тестовый образец к реальному пути экспорта iOS
Скрипт выше проверяет надёжность инструментов и проверочных условий, но не подтверждает корректность экспорта в приложении. Следующий шаг — передать тот же трёхцветный образец в рабочую функцию кодирования и декодировать возвращённые ею данные, а не снова вызывать кодировщик из примера.
Отдельно проверьте отрисовку UIKit
Если приложение использует UIGraphicsImageRenderer, явно задайте format.opaque = false в тестовом таргете iOS. Для проверки размера в пикселях также задайте format.scale = 1, иначе логический размер можно спутать с размером файла в пикселях. Если в рабочем коде используется другой масштаб, добавьте отдельный набор проверок соответствующих размеров.
Декодируйте возвращённые данные PNG с помощью ImageIO и отрисуйте их в том же контексте sRGB, чтобы привести к единому представлению. Не считайте UIImage.size непосредственным размером результата в пикселях. Тесты, в которых отрисовка должна выполняться в главном потоке, запускайте на главном акторе, чтобы сам тест не вносил неопределённость в поведение.
Добавьте ещё один тестовый образец — с наклонным краем и сглаживанием. Убедитесь, что на его краях действительно есть пиксели с альфой между нулём и максимальным значением, затем проверьте наложение на чёрный и белый фон. Не привязывайте тест ко всем байтам края, полученным в конкретной версии системы.
При сбое сохраняйте данные для диагностики
При каждом сбое сохраняйте как минимум исходный PNG, размеры после декодирования, заданное цветовое пространство, координаты проверяемых пикселей, ожидаемые и фактические значения RGBA, а также версию инструментальной цепочки. Для тестов достаточно синтетических изображений — загружать пользовательские фотографии не нужно.
Если размеры верны, но все пиксели непрозрачны, сначала проверьте настройку opaque контекста рисования и заливку фона. Если альфа верна, но края темнеют, ищите повторное предумножение. Если цвет смещён по всему изображению, сначала проверьте преобразования цветового пространства и лишь затем обсуждайте допуск для значений пикселей.
Итоговая проверка должна давать три независимых результата: размеры совпадают, прозрачность сохранена, композиция с фоном соответствует контракту. Возможность открыть файл — лишь предварительное условие. Общую пиксельную логику можно запускать автоматически на облачном Mac, но реальный экспорт UIKit и итоговый файл после отправки через меню «Поделиться» всё равно нужно проверять отдельно, чтобы не пропустить повторное кодирование на промежуточных этапах.
Часто задаваемые вопросы
Достаточно ли наличия alpha-канала?
Нет. Канал может содержать только полностью непрозрачные значения. Нужно прочитать прозрачные и полупрозрачные пиксели и проверить результат смешивания.
Заменяет ли тест на Mac проверку на устройстве iOS?
Не полностью. На Mac проверяется общая логика кодирования и содержимое файла. Рендеринг UIKit, цветовой охват и фактический путь передачи требуют тестов iOS.
Выделенный облачный Mac для следующей сборки
Выделенный физический Mac mini, не виртуальная машина. Выберите срок аренды: день, неделя, месяц или квартал. Конфигурация и фактическая доступность указаны в консоли.