iOS 사진의 EXIF 방향과 픽셀 정규화 검증하기

iOS 사진의 EXIF 방향과 픽셀 정규화 검증하기

사진 선택 화면에서는 세로로 보이던 사진이 업로드 후 썸네일에서는 가로로 누워 보일 수 있습니다. 이런 문제를 맡았다면 자르기 비율부터 바꾸지 마세요. JPEG는 픽셀을 가로 방향으로 저장하면서, EXIF 방향 정보를 통해 뷰어에 회전 표시를 지시할 수 있습니다. iOS의 UIImage는 사진을 올바르게 그리더라도, 다른 내보내기 코드나 수신 측이 같은 방식으로 처리한다는 보장은 없습니다. 아래에서는 클라우드 Mac의 iOS 테스트 대상에서 재현 가능한 샘플을 만들어, “화면에 올바르게 보이는가”와 “파일 자체가 올바른가”를 따로 검증합니다.

방향 정보와 픽셀을 먼저 구분하기

세 가지 값을 기록합니다. 파일의 EXIF 방향, 디코딩한 UIImage.imageOrientation, 그리고 내부 CGImage의 픽셀 너비와 높이입니다. EXIF 방향 값 6은 표시할 때 시계 방향으로 회전해야 한다는 뜻이지, 픽셀이 이미 회전했다는 뜻은 아닙니다. 내부 픽셀이 너비 120, 높이 80이라면 올바르게 표시된 크기는 너비 80, 높이 120이어야 합니다.

미리보기 스크린샷만 보고 수정이 끝났다고 판단하지 마세요. 미리보기 컴포넌트가 파일의 방향 정보를 적용했더라도, 이후 처리 경로에서는 원래 픽셀이 그대로 전달될 수 있습니다.

먼저 처리 책임을 정합니다. 원본 파일을 보관해야 한다면 원본과 정규화된 결과물을 따로 저장하세요. 업로드 이미지의 방향만 통일하면 된다면 내보내기 전에 다시 그리고, 내보낸 파일을 다시 디코딩해 확인하세요. 원본 파일의 방향 값만 1로 바꿔서는 안 됩니다. 그렇게 해도 픽셀은 움직이지 않습니다.

iOS 테스트에서 재현 가능한 샘플 만들기

다음 테스트를 UIKit을 사용하는 iOS 단위 테스트 대상에 추가합니다. 실행 중에 120×80픽셀 JPEG를 생성하고 방향 값 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 구성하기