Как проверить EXIF-ориентацию и нормализацию пикселей в iOS

Как проверить EXIF-ориентацию и нормализацию пикселей в iOS

На экране выбора фотография отображается вертикально, а после загрузки миниатюра оказывается горизонтальной. Не спешите менять пропорции обрезки: в JPEG пиксели могут быть расположены горизонтально, а метка ориентации EXIF предписывает просмотрщику повернуть изображение. UIImage в iOS может показывать его правильно, но код экспорта или принимающая сторона не обязательно обработают его так же. Ниже мы создадим воспроизводимый образец в тестовой цели iOS на облачном Mac и отдельно проверим, правильно ли выглядит изображение и правильно ли устроен экспортированный файл.

Сначала отделите метку ориентации от пикселей

Зафиксируйте три значения: ориентацию EXIF в файле, UIImage.imageOrientation после декодирования и ширину и высоту пиксельного изображения в базовом CGImage. Значение ориентации EXIF 6 означает, что при отображении изображение нужно повернуть по часовой стрелке; сами пиксели от этого не меняют положения. Если ширина базового изображения составляет 120 пикселей, а высота — 80, при правильном отображении его ширина должна быть 80, а высота — 120.

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

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

Создайте воспроизводимый образец в тесте iOS

Добавьте следующий тест в цель модульного тестирования iOS с поддержкой UIKit. Во время выполнения он создаёт JPEG размером 120×80 пикселей и записывает значение ориентации 6. Тесту не нужны фотографии с компьютера разработчика, а реальные снимки и пользовательские данные не попадают в репозиторий. Код также проверяет метку, прочитанную через ImageIO, и ориентацию изображения после декодирования в UIKit.

import XCTest
import UIKit
import ImageIO
import UniformTypeIdentifiers

final class ImageOrientationTests: XCTestCase {
    func testOrientationSurvivesExport() throws {
        let format = UIGraphicsImageRendererFormat()
        format.scale = 1
        format.opaque = true

        let source = UIGraphicsImageRenderer(
            size: CGSize(width: 120, height: 80),
            format: format
        ).image { _ in
            UIColor.red.setFill()
            UIRectFill(CGRect(x: 0, y: 0, width: 60, height: 80))
            UIColor.blue.setFill()
            UIRectFill(CGRect(x: 60, y: 0, width: 60, height: 80))
        }

        let jpeg = NSMutableData()
        let destination = try XCTUnwrap(
            CGImageDestinationCreateWithData(
                jpeg, UTType.jpeg.identifier as CFString, 1, nil
            )
        )
        CGImageDestinationAddImage(
            destination,
            try XCTUnwrap(source.cgImage),
            [kCGImagePropertyOrientation: 6] as CFDictionary
        )
        XCTAssertTrue(CGImageDestinationFinalize(destination))

        let input = jpeg as Data
        let imageSource = try XCTUnwrap(
            CGImageSourceCreateWithData(input as CFData, nil)
        )
        let properties = try XCTUnwrap(
            CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil)
                as? [CFString: Any]
        )
        XCTAssertEqual(
            (properties[kCGImagePropertyOrientation] as? NSNumber)?.intValue,
            6
        )

        let decoded = try XCTUnwrap(UIImage(data: input))
        XCTAssertEqual(decoded.imageOrientation, .right)
        XCTAssertEqual(decoded.cgImage?.width, 120)
        XCTAssertEqual(decoded.cgImage?.height, 80)

        let upright = UIGraphicsImageRenderer(
            size: CGSize(width: 80, height: 120),
            format: format
        ).image { _ in
            decoded.draw(in: CGRect(x: 0, y: 0, width: 80, height: 120))
        }
        XCTAssertEqual(upright.imageOrientation, .up)

        let exported = try XCTUnwrap(upright.pngData())
        let reopened = try XCTUnwrap(UIImage(data: exported))
        XCTAssertEqual(reopened.imageOrientation, .up)
        XCTAssertEqual(reopened.cgImage?.width, 80)
        XCTAssertEqual(reopened.cgImage?.height, 120)
    }
}

Перед запуском убедитесь, что этот Swift-файл включён в тестовую цель, и выберите симулятор iOS, поддерживаемый проектом. При запуске из командной строки укажите в -workspace, -scheme и -destination реальные значения из проекта. Не считайте устройство, приведённое в примере команды, доступным на каждой машине.

Подключите проверку к реальному пути экспорта

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

Явно задайте контракт размеров для функции приложения

Сначала договоритесь, что функция принимает данные исходного файла и возвращает данные с определённой ориентацией отображения. Для образца с ориентацией 6 размер выходного пиксельного изображения должен измениться с 120×80 на 80×120. Если приложение также масштабирует изображение, проверяйте соотношение сторон и точный целевой размер после масштабирования, а правила масштабирования тестируйте отдельно. Не объединяйте ошибки поворота, сжатия и обрезки в одну проверку.

Два цвета — слева и справа — помогают при визуальной проверке заметить зеркальное отражение содержимого. Если автоматическая проверка должна учитывать расположение цветов, сравнивайте пиксели с допуском: JPEG использует сжатие с потерями, из-за чего цвета вблизи границы меняются. Требовать побитного совпадения каждого канала нельзя.

Найдите этап, на котором предпросмотр и экспорт расходятся

Сначала выясните, где изображение меняет представление: данные файла, UIImage, CGImage, графический контекст или файл после повторного кодирования. Записывайте ориентацию и размеры в пикселях при каждом преобразовании. Так часто удаётся обнаружить развилку вроде «извлечь cgImage и сразу закодировать»: cgImage предоставляет пиксели, и нельзя считать, что поворот при отрисовке UIImage уже записан в них.

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

Контрольный список перед слиянием

Как минимум проверьте обычное изображение с ориентацией 1, повёрнутое изображение с ориентацией 6 и зеркальные ориентации, которые реально допускает приложение. Для каждого образца проверяйте входную метку, ширину и высоту пиксельного изображения после нормализации и ориентацию отображения после повторного декодирования. Если рабочий код допускает обрезку, добавьте ещё один непрямоугольный образец: у квадратного изображения проверка ширины и высоты не выявит ошибочный поворот.

В конце запустите тест в той же тестовой цели iOS, что используется в процессе поставки. При сбое достаточно сохранить входную ориентацию, выходные размеры и этап обработки; не записывайте исходные пользовательские фотографии в общедоступные журналы. Тогда результат регрессионного теста укажет на этап обработки файла, а не просто сообщит, что «какая-то фотография выглядит неправильно».

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

Почему правильно отображаемое в UIImage фото поворачивается после экспорта?

При рисовании UIImage учитывает метаданные ориентации, а получатель файла может обработать их иначе. Повторно декодируйте экспортированный файл и проверьте пиксели.

Можно ли нормализовать снимок, просто установив ориентацию 1?

Нет. Если пиксели остаются на прежних местах, изменение одной метки изменит вид снимка. Сначала перерисуйте его с учётом исходной ориентации.

Доверьте следующую сборку выделенному облачному Mac.

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

Настроить облачный Mac

Проверьте параметры устройства и срок аренды.

Три модели доступны с оплатой за день, неделю, месяц или квартал. Перед заказом ознакомьтесь со всеми тарифами.

Модели и цены Настроить облачный Mac