iOS 이미지 자르기 좌표 검증: 화면 선택 영역에서 픽셀 경계까지

iOS 이미지 자르기 좌표 검증: 화면 선택 영역에서 픽셀 경계까지

iOS 이미지 자르기 좌표 검증: 화면 선택 영역에서 픽셀 경계까지

4000×3000픽셀 이미지를 300×300포인트 크기의 자르기 뷰에 넣고 중앙을 선택했는데, 내보낸 결과에서 인물이 위로 치우쳐 보일 수 있습니다. 이럴 때는 제스처 코드부터 고치지 마세요. 이미지를 종횡비 유지 맞춤(aspect fit)으로 표시하면 위아래에 각각 37.5포인트의 여백이 생깁니다. 뷰 전체를 기준으로 좌표를 환산하면 선택 영역의 시작점과 높이가 모두 잘못됩니다. 클라우드 Mac에서 순수 Swift로 좌표를 먼저 검증한 뒤 UI를 연결하면 계산 오류와 렌더링 오류를 분리할 수 있습니다.

세 가지 좌표의 기준 확정하기

먼저 세 가지 대상을 명확히 구분합니다. 원본 이미지의 픽셀 사각형, 뷰 안에서 이미지가 실제로 표시되는 사각형, 사용자가 드래그한 선택 영역입니다. 뒤의 두 대상은 반드시 같은 좌표 공간에 있어야 합니다. 하나는 부모 뷰 기준이고 다른 하나는 이미지 컨테이너 기준인 상태로 계산해서는 안 됩니다.

이 글에서는 종횡비 유지 맞춤 표시만 다루며, 영역을 채우기 위한 잘라내기(aspect fill), 회전, 원근 변환은 다루지 않습니다. 원점은 왼쪽 위이고, 가로축은 오른쪽으로, 세로축은 아래로 증가한다고 가정합니다. 입력 이미지는 표시 방향에 맞게 이미 정규화되어 있어야 하며, 너비와 높이는 이미지 객체의 포인트 크기가 아니라 실제 픽셀 크기를 사용합니다.

화면 배율과 이미지 표시 배율은 다릅니다. 선택 영역에 화면 배율을 곱하는 방식은 크기가 우연히 맞아떨어질 때만 올바른 결과를 내므로, 일반적인 자르기 공식으로 사용할 수 없습니다.

UI에 중첩된 뷰가 있다면 좌표 변환 API로 선택 영역을 이미지 컨테이너의 좌표 공간으로 옮긴 뒤 아래 함수를 호출하세요. 확대·축소 제스처가 적용되어 있다면 이동 및 크기 변환도 먼저 역변환해야 합니다. 변환된 사각형을 정적인 공식에 그대로 넣어서는 안 됩니다.

표시 사각형과 픽셀 좌표 변환 구현하기

다음 코드를 CropCheck.swift로 저장합니다. 전체 Xcode 도구 체인을 사용할 수 있는 환경이라면 터미널에서 xcrun swift CropCheck.swift를 실행하세요. 이 코드는 시스템 Foundation에만 의존하며, 네트워크에 접근하지 않고 시뮬레이터도 필요하지 않습니다.

import Foundation

func cropPixels(
    source: CGSize,
    viewport: CGSize,
    selection: CGRect
) -> CGRect? {
    let values = [
        source.width, source.height,
        viewport.width, viewport.height,
        selection.origin.x, selection.origin.y,
        selection.width, selection.height
    ]
    guard values.allSatisfy({ $0.isFinite }),
          source.width > 0, source.height > 0,
          viewport.width > 0, viewport.height > 0,
          source.width.rounded(.down) == source.width,
          source.height.rounded(.down) == source.height,
          selection.width > 0, selection.height > 0
    else {
        return nil
    }

    let scale = min(
        viewport.width / source.width,
        viewport.height / source.height
    )
    guard scale.isFinite, scale > 0 else {
        return nil
    }

    let display = CGRect(
        x: (viewport.width - source.width * scale) / 2,
        y: (viewport.height - source.height * scale) / 2,
        width: source.width * scale,
        height: source.height * scale
    )
    let clipped = selection.intersection(display)
    guard !clipped.isNull, !clipped.isEmpty else {
        return nil
    }

    let left = max(
        0, floor((clipped.minX - display.minX) / scale)
    )
    let top = max(
        0, floor((clipped.minY - display.minY) / scale)
    )
    let right = min(
        source.width,
        ceil((clipped.maxX - display.minX) / scale)
    )
    let bottom = min(
        source.height,
        ceil((clipped.maxY - display.minY) / scale)
    )
    guard right > left, bottom > top else {
        return nil
    }

    return CGRect(
        x: left, y: top,
        width: right - left, height: bottom - top
    )
}

계산 순서를 바꾸면 안 됩니다. 먼저 선택 영역과 이미지 표시 영역의 교집합을 구하고, 표시 위치의 오프셋을 뺀 다음, 마지막으로 표시 배율로 나눕니다. 뷰 좌표를 먼저 정수로 반올림하면 원본 이미지와 뷰의 크기 비율만큼 오차가 커집니다.

픽셀 경계는 선택 영역이 닿는 픽셀을 모두 포함하도록 바깥쪽으로 내림·올림합니다. 따라서 수학적으로 정의된 선택 영역의 크기가 정확히 유지된다고 보장하지는 않습니다. 제품에서 고정된 출력 크기를 요구한다면 크기를 조정해 내보내는 작업을 별도의 후속 단계로 처리해야 합니다.

직접 계산할 수 있는 예제로 좌표 어긋남 잡기

아래 단언문을 같은 파일에 이어서 추가합니다. 실행에 성공하면 아무것도 출력되지 않으며, 단언문 하나라도 실패하면 프로세스가 실패 상태로 종료됩니다. 이 검증 스크립트는 단언문을 제거하는 최적화 설정으로 실행하지 마세요.

let source = CGSize(width: 4000, height: 3000)
let viewport = CGSize(width: 300, height: 300)

func mapped(_ rectangle: CGRect) -> CGRect? {
    cropPixels(
        source: source,
        viewport: viewport,
        selection: rectangle
    )
}

assert(
    mapped(CGRect(x: 75, y: 75, width: 150, height: 150))
    == CGRect(x: 1000, y: 500, width: 2000, height: 2000)
)

assert(
    mapped(CGRect(x: 0, y: 0, width: 300, height: 300))
    == CGRect(x: 0, y: 0, width: 4000, height: 3000)
)

assert(
    mapped(CGRect(x: 10, y: 0, width: 20, height: 20))
    == nil
)

assert(
    mapped(CGRect(x: 0, y: 0, width: 0, height: 20))
    == nil
)

assert(
    mapped(CGRect(x: 225, y: 225, width: 100, height: 100))
    == CGRect(x: 3000, y: 2500, width: 1000, height: 500)
)

중앙 선택 예제는 손으로도 확인할 수 있습니다. 표시 배율은 0.075이고, 선택 영역의 시작점에서 위쪽 여백을 빼면 세로 좌표가 37.5포인트가 됩니다. 이는 원본 이미지의 세로 좌표 500픽셀에 해당합니다. 마지막 예제는 오른쪽과 아래쪽 경계를 동시에 벗어납니다. 이를 통해 함수가 시작점만 제한하는 것이 아니라 경계 밖으로 나간 부분을 실제로 잘라내는지 확인할 수 있습니다.

세로 이미지, 정사각형이 아닌 컨테이너, 소수 좌표를 가진 선택 영역도 추가로 테스트하세요. 유효한 결과마다 너비와 높이가 0보다 큰지, 경계가 원본 이미지를 벗어나지 않는지, 네 변의 좌표가 모두 정수인지 확인합니다. 이 함수는 너비나 높이가 음수인 입력을 바로 거부하므로, 드래그 방향은 UI 계층에서 미리 정규화해야 합니다.

계산 결과와 실제 내보내기 결과를 따로 검증하기

계산 테스트를 통과했다면 픽셀 크기가 고정되어 있고 네 모서리의 색이 각각 다르며 중앙에 십자 표시가 있는 테스트 이미지를 준비합니다. 손실 압축 때문에 색상 검증에 노이즈가 생기지 않도록 먼저 무손실 형식으로 내보내세요.

확인할 항목은 세 가지입니다. 내보낸 이미지의 너비와 높이가 함수 반환값과 같은지, 오른쪽 아래를 잘랐을 때 왼쪽 위 표식이 섞이지 않는지, 전체 이미지를 선택했을 때 투명한 테두리가 추가되지 않는지 확인합니다. 인코더가 덧붙이는 메타데이터가 달라져도 픽셀은 그대로일 수 있으므로 파일 바이트만 비교해서는 안 됩니다.

자르기 구현이 방향을 정규화한 픽셀 이미지를 직접 사용한다면 이 글의 함수가 반환한 픽셀 사각형을 전달합니다. 그래픽 컨텍스트에서 다시 그리는 방식이라면 원점 방향과 출력 배율 설정을 별도로 확인해야 합니다. 이 단계에서 가장 흔한 오류는 좌표 함수가 이미 픽셀 단위로 반환했는데도 내보내기 코드에서 표시 배율을 한 번 더 곱하는 것입니다.

이미지를 비동기로 불러오는 경우도 검증해야 합니다. 사용자가 영역을 선택한 뒤 크기가 다른 원본 이미지로 바뀌었다면 다시 계산하거나 기존 선택 영역을 무효화해야 합니다. 이전 이미지의 배율과 새 이미지의 픽셀 크기를 섞어 사용하지 마세요.

실패 기록을 재현 가능한 입력으로 남기기

실패할 때마다 원본 이미지의 픽셀 너비와 높이, 컨테이너의 포인트 크기, 선택 사각형, 실제 표시 모드, 계산 결과를 기록합니다. 수치만 남기면 되므로 사용자의 사진을 업로드할 필요는 없습니다. 재현할 때는 같은 크기의 테스트 이미지로 대체하세요.

병합 전에는 최소한 가로 이미지, 세로 이미지, 전체 이미지 선택, 여백만 선택, 가장자리 밖으로 나간 선택, 소수 경계의 여섯 가지 입력 유형을 검증해야 합니다. 레이아웃이 바뀌면 컨테이너 크기를 다시 읽어야 하며, 회전 전에 저장해 둔 크기를 그대로 사용해서는 안 됩니다.

최종 검증은 두 단계로 나눕니다. 순수 함수는 좌표 관계가 맞는지 검증하고, 내보내기 테스트는 픽셀의 방향과 크기가 맞는지 검증합니다. 두 단계가 모두 통과한 뒤 원격 데스크톱에서 드래그 조작감을 확인하세요. 이렇게 하면 UI 조정, 원본 이미지 교체, 내보내기 구현 변경 중 어디에서 문제가 생겼는지 명확히 구분할 수 있습니다.

자주 묻는 질문

자르기 좌표에 화면 배율을 곱하면 되나요?

바로 곱하면 안 됩니다. 원본 픽셀 크기와 이미지가 실제로 표시되는 사각형으로 변환 비율을 구해야 합니다. 화면 배율은 이 비율과 다릅니다.

픽셀 경계를 바깥쪽으로 반올림하는 이유는 무엇인가요?

최소 경계는 내림하고 최대 경계는 올림하면 선택 영역이 닿는 픽셀을 모두 포함할 수 있습니다. 변환한 결과는 원본 이미지 경계 안으로 제한해야 합니다.

전용 물리 Mac에서 프로젝트 실행

클라우드 Mac으로 빌드와 테스트를 진행하고 원격 데스크톱을 이용해 보세요. 프로젝트에 맞는 사양과 요금 주기를 선택할 수 있으며, 실제 이용 가능 여부는 콘솔에 실시간으로 표시됩니다.

다음 단계: 실행 환경 확인

먼저 사양을 비교한 뒤 접속 방법을 선택하세요. SSH 명령줄은 빌드에, VNC 데스크톱은 직접 조작하며 디버깅할 때 유용합니다.