Проверка преобразования Display P3 в sRGB на облачном Mac

Проверка преобразования Display P3 в sRGB на облачном Mac

Проверка преобразования Display P3 в sRGB на облачном Mac

Если одна и та же фотография нормально выглядит в «Просмотре», но после экспорта становится темнее, не спешите повышать качество JPEG. Если просто пометить пиксели Display P3 как sRGB, изменится интерпретация их цвета. Если же удалить профиль, принимающей стороне придётся угадывать цветовое пространство. Облачный Mac можно использовать для регрессионного тестирования этой цепочки: создавать фиксированные цветовые образцы, выполнять настоящее преобразование, а затем повторно читать итоговый файл и проверять результат, не полагаясь на картинку в удалённом рабочем столе.

Сначала зафиксируйте требования к результату

В этой статье проверяются только непрозрачные RGB-изображения со стандартным динамическим диапазоном. Целевой формат — JPEG в sRGB с цветовым профилем. Для изображений с прозрачностью, расширенным динамическим диапазоном и снимков экрана нужны отдельные тестовые образцы: в эту базовую проверку они не входят.

Разделяйте три операции: определение исходного цветового пространства, отрисовку в целевом цветовом пространстве и кодирование файла. Первые две определяют, какой цвет представляют пиксели, и только последняя — потери при сжатии. Если у входного изображения нет достоверного профиля, его следует либо отклонить согласно требованиям продукта, либо явно указать исходное пространство на основании известных данных. Нельзя по умолчанию считать «неизвестное» пространство Display P3.

Назначение профиля определяет, как интерпретировать пиксели, а преобразование цвета меняет их числовые значения. Это разные операции, и тесты должны проверять каждую из них отдельно.

Также зафиксируйте метод цветопередачи. Цвета за пределами охвата sRGB невозможно сохранить без изменений, поэтому отсечение само по себе не означает ошибку реализации. Тем более нельзя требовать, чтобы значения RGB оставались одинаковыми в разных цветовых пространствах.

Проверьте преобразование на синтетическом образце

На облачном Mac с уже установленными инструментами разработчика командной строки сохраните следующий код в файл color-check.swift. Скрипт использует только системные фреймворки, не читает личные фотографии и не требует доступа к сети.

import Foundation
import CoreGraphics
import ImageIO
import UniformTypeIdentifiers

let size = 64
let p3 = CGColorSpace(name: CGColorSpace.displayP3)!
let srgb = CGColorSpace(name: CGColorSpace.sRGB)!
let layout = CGBitmapInfo.byteOrder32Big.rawValue
    | CGImageAlphaInfo.premultipliedLast.rawValue

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

func pixel(_ context: CGContext) -> [Int] {
    let bytes = context.data!.assumingMemoryBound(to: UInt8.self)
    let offset = (32 * context.bytesPerRow) + 32 * 4
    return (0..<3).map { Int(bytes[offset + $0]) }
}

let source = context(p3)
source.setFillColor(CGColor(
    colorSpace: p3, components: [0.8, 0.4, 0.2, 1.0]
)!)
source.fill(CGRect(x: 0, y: 0, width: size, height: size))
let original = source.makeImage()!

let converted = context(srgb)
converted.setRenderingIntent(.relativeColorimetric)
converted.draw(
    original, in: CGRect(x: 0, y: 0, width: size, height: size)
)
let reference = pixel(converted)
precondition(reference != pixel(source), "Conversion was not observed")

let url = URL(fileURLWithPath: "srgb-check.jpg")
let output = CGImageDestinationCreateWithURL(
    url as CFURL, UTType.jpeg.identifier as CFString, 1, nil
)!
CGImageDestinationAddImage(output, converted.makeImage()!, [
    kCGImageDestinationLossyCompressionQuality: 0.95
] as CFDictionary)
precondition(CGImageDestinationFinalize(output), "Encoding failed")

let input = CGImageSourceCreateWithURL(url as CFURL, nil)!
let decoded = CGImageSourceCreateImageAtIndex(input, 0, nil)!
precondition(decoded.width == size && decoded.height == size)
precondition(decoded.colorSpace?.model == .rgb)

let readback = context(srgb)
readback.draw(
    decoded, in: CGRect(x: 0, y: 0, width: size, height: size)
)
let actual = pixel(readback)
let error = zip(reference, actual).map { abs($0 - $1) }.max()!
precondition(error <= 8, "JPEG round-trip exceeded tolerance")

let properties = CGImageSourceCopyPropertiesAtIndex(
    input, 0, nil
) as NSDictionary?
let profile = properties?[kCGImagePropertyProfileName] as? String
precondition(profile?.isEmpty == false, "Missing color profile")

Выполните xcrun swift color-check.swift. При успешном завершении код выхода будет нулевым, а в каталоге появится тестовый файл. Отладочного вывода здесь нет: при нарушении проверок выполнение прерывается. Принудительное извлечение значений допустимо для такого контролируемого тестового образца, но в рабочем экспортёре сбои создания объектов или декодирования следует оформлять как явные ошибки.

Оценивайте преобразование и сжатие по отдельности

Цвет образца намеренно выбран так, чтобы изменение числовых значений при переходе из P3 в sRGB было заметным. Первая проверка доказывает только то, что преобразование не свелось к прямому копированию каналов. Она не подтверждает правильность преобразования всех цветов. В полноценный набор тестов нужно добавить нейтральные серые тона, почти чёрные и почти белые оттенки, а также цвета вблизи границы целевого охвата.

Значения из повторно прочитанного JPEG сравниваются не с исходными значениями P3, а с эталоном, уже преобразованным в sRGB. Оба изображения отрисовываются в контексты одинакового формата, после чего считывается центральный пиксель. Это позволяет избежать ложных срабатываний из-за различий в порядке каналов и цветовых пространствах.

Предельная ошибка по каналам в скрипте равна 8. Этот допуск относится только к данному однородному образцу и не является критерием качества изображения. Для реальных фотографий следует собирать статистику по нескольким областям. Не переносите этот порог без изменений на тонкие линии, текст и высокочастотные текстуры. Наличие имени профиля — тоже лишь минимальная проверка: по одному имени нельзя установить, что содержимое профиля корректно.

Подключите настоящий экспортёр приложения

Отдельный скрипт проверяет системную цепочку обработки. Следующий шаг — передать тот же тестовый образец P3 в метод экспорта, который действительно использует приложение. Тест должен читать созданный этим методом файл, а не реализовывать собственный вариант «правильного экспорта».

Рекомендуется сохранить две группы проверок. Структурные проверки контролируют успешное декодирование, размеры, модель RGB и наличие профиля. Пиксельные проверки сопоставляют фиксированные цветовые образцы с эталоном и оценивают ошибку после записи и повторного чтения с потерями. Добавьте отрицательный тест: только переназначьте профиль исходным пикселям, не выполняя преобразование, и убедитесь, что пиксельные проверки обнаруживают подмену. Так контроль не сведётся к проверке расширения файла.

Сначала зафиксируйте параметры масштабирования, обрезки и коррекции ориентации в рабочем коде. На этом этапе меняйте только обработку цвета, иначе изменение размеров и интерполяция могут скрыть причину цветового сдвига. При каждом сбое сохраняйте исходный тестовый образец, выходной файл и конфигурацию теста. Не заменяйте исходные файлы сбоя вложениями, прошедшими повторное сжатие.

Зафиксируйте базовую линию и границы визуальной проверки

Сохраняйте в записи о сборке версию системы, версию инструментов разработчика, исходный и целевой профили, метод цветопередачи и качество кодирования. При обновлении среды выполнения сначала прогоните прежние образцы в старой и новой среде и выясните, связаны ли изменения с преобразованием цвета или с кодированием. Только после этого решайте, нужно ли обновлять эталонные значения. Не расширяйте допуски лишь для того, чтобы убрать падения тестов.

После успешного прохождения автоматических проверок оцените реальные фотографии на устройствах, поддерживающих соответствующий цветовой охват. Удалённый рабочий стол может вносить дополнительное кодирование и обработку цвета, поэтому видимую картинку нельзя считать достоверным отражением значений пикселей. При этом проверка на устройстве не заменяет повторное чтение файла.

Итоговый файл должен одновременно удовлетворять трём условиям: целевой профиль распознаётся, пиксели действительно прошли преобразование, а ошибка сжатия с потерями остаётся в пределах, установленных для образца. Закрепите эти три условия в тестах экспортёра — тогда жалоба «цвета выглядят неправильно» превратится в воспроизводимый инженерный сбой, причину которого можно локализовать.

Часто задаваемые вопросы

Достаточно ли назначить изображению профиль sRGB?

Нет. Назначение профиля меняет трактовку существующих значений. Для преобразования нужно нарисовать исходное изображение в целевом контексте sRGB.

Должны ли пиксели JPEG полностью совпадать после чтения?

Нет. JPEG использует сжатие с потерями. Сравнивайте изображения в одном пространстве декодирования sRGB и задавайте допуск для фиксированных образцов.

Выделите Mac для следующей сборки.

Выберите облачный Mac на физической машине, не меняя привычные процессы Xcode и CI. Подберите конфигурацию под нагрузку проекта; актуальный статус доступности отображается в консоли.

Арендовать облачный Mac

Поручите следующую сборку выделенной физической машине.

Сохраните привычные команды и процессы CI, выбрав облачный Mac под нагрузку проекта.