Как проверить прозрачность PNG для iOS на облачном Mac

Как проверить прозрачность PNG для iOS на облачном Mac

Как проверить прозрачность 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, не виртуальная машина. Выберите срок аренды: день, неделя, месяц или квартал. Конфигурация и фактическая доступность указаны в консоли.