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