합성 원시 픽셀을 신뢰할 수 있는 CGImage로 변환하려면 크기, 행 간격, 채널 순서, 알파 규칙, 색 공간, 소유권을 함께 정의해야 합니다. 이 가이드에서는 검증을 거치는 BGRA 변환기를 만든 다음, 결과에 명시적인 AppKit 표시 크기를 지정합니다.

이 글의 독자는 Mac 앱에서 생성한 색상 견본, 다이어그램 또는 미리 보기용 인메모리 이미지를 만들려 합니다. 아래 구현은 의도적으로 범위가 좁은 형식 하나만 허용합니다. 별도의 의사 결정 절차에서는 합성 Metal 출력을 같은 변환 경계로 가져오는 방법을 설명합니다.

변환 계약에 무엇을 명시해야 하나요?

Apple의 원시 데이터용 CGImage 이니셜라이저는 제공된 바이트가 지정된 이미지 매개변수와 일치해야 합니다. 너비와 높이는 픽셀 단위이며, bytesPerRow는 각 가로 행의 메모리 레이아웃을 설명합니다. 이 매개변수는 입력 버퍼를 설명하므로 이미지를 만들기 전에 확정해야 합니다. Apple의 비트맵 이미지 이니셜라이저 문서.

이 글의 변환기에는 다음 애플리케이션 수준 계약을 사용합니다. 엄격한 검증 규칙은 허용되는 입력을 모호하지 않게 하는 설계 선택입니다.

속성 허용되는 입력 또는 동작
크기 픽셀 단위로 측정한 양의 정수 너비와 높이
구성 요소 픽셀당 8비트 부호 없는 구성 요소 4개
메모리 순서 연속된 바이트가 파랑, 초록, 빨강, 알파 순서
알파 RGB 구성 요소가 알파에 이미 곱해진 상태
색 해석 sRGB
행 간격 width * 4바이트 이상이며 4의 배수
버퍼 길이 마지막 행 패딩을 포함해 정확히 bytesPerRow * height
행 순서 그대로 유지하며 자동으로 수직 반전하지 않음
소유권 입력을 불변 소유 저장소에 복사
실패 입력이 잘못되었거나 생성에 실패하면 오류를 던짐
표시 AppKit 포인트 크기는 별도로 선택

예를 들어, 2픽셀 행에는 유효 바이트가 8개 있습니다. 간격이 12라면 각 행 뒤에 패딩 바이트 4개가 추가됩니다. 이러한 행이 2개라면 총 24바이트를 차지합니다. 두 번째 행의 첫 픽셀은 12번째 바이트에서 시작합니다. 이 예제 레이아웃에는 의도적으로 패딩이 있습니다. 촘촘하게 채운 테스트 데이터만 사용하면 전달된 간격 대신 width * 4를 잘못 사용하는 변환기를 발견하기 어렵기 때문입니다.

마지막 행까지 패딩을 온전히 요구하면 소유권 경계가 단순해집니다. 마지막 행을 짧게 할당하는 생산자는 이 변환기를 호출하기 전에 출력을 정규화해야 합니다. 길이가 일치하지 않으면 의도한 이미지 영역이 모호한 더 큰 버퍼를 실수로 허용하는 일도 방지할 수 있습니다.

BGRA, 바이트 순서, 알파, 색 공간은 어떻게 연결되나요?

이 형식에서는 CGImageAlphaInfo.premultipliedFirst와 CGBitmapInfo.byteOrder32Little을 함께 사용합니다. Apple은 전자를 색 구성 요소에 이미 알파를 곱한 상태에서 알파가 최상위 비트에 있는 형식으로 정의하고, 후자를 리틀 엔디안 32비트 레이아웃으로 정의합니다. 두 정의를 함께 적용하면 메모리의 바이트 순서는 주소가 증가하는 방향으로 BGRA가 됩니다. premultiplied-first 알파, 32비트 리틀 엔디안 비트맵 순서.

따라서 “first”라는 표현만으로는 메모리에서 알파가 어디에 놓이는지 결정할 수 없으며, 바이트 순서 설정이 필요합니다. 이 형식에서 불투명한 빨강에 사용하는 네 바이트는 0, 0, 255, 255입니다. 이 설정에 RGBA 바이트를 제공하면 빨강과 파랑이 뒤바뀝니다. 메타데이터를 바꿔도 기존 버퍼의 순서는 바뀌지 않습니다.

프리멀티플라이도 구체적으로 다뤄야 합니다. 이 예시의 8비트 규칙에서 알파가 128인 완전한 빨강은 BGRA 메모리에서 대략 0, 0, 128, 128이 됩니다. 변환기는 저장된 각 색 구성 요소가 알파보다 크지 않은지 확인합니다. 이 검사는 선택한 정규화 프리멀티플라이 형식에서 불가능한 값을 찾아내지만, 생산자가 각 구성 요소를 어떻게 계산했는지까지 증명하지는 않습니다.

바이트가 의도한 해석과 일치할 때만 CGColorSpace.sRGB를 지정하세요. Apple은 sRGB 색도와 비선형 전달 함수 모두를 정의합니다. 색 공간 레이블만 지정한다고 해서 선형 RGB 구성 요소 값이 sRGB 값으로 변환되지는 않습니다. Apple의 sRGB 정의.

이 변환기는 검증과 이미지 생성을 담당합니다. 생산자가 스트레이트 알파, RGBA 순서, 부동 소수점 구성 요소 또는 선형 색 값을 출력한다면 이 경계에 앞서 명시적인 정규화 단계를 추가하거나, 해당 형식에 대해 별도로 문서화된 계약을 구현하세요.

Swift에서 소유권이 있는 저장소로 이미지를 어떻게 만들 수 있나요?

작은 데스크톱용 생성 이미지에는 복사 경계를 사용하세요. Apple 문서에 따르면 CFDataCreate는 제공된 버퍼를 내부 저장소로 항상 복사하며, CGDataProvider(data:)는 CFData 객체를 읽습니다. CGImage 이니셜라이저는 프로바이더를 유지합니다. 이렇게 하면 임시 배열 포인터에 이미지의 유효 기간을 묶지 않아도 됩니다. 바이트를 CFData로 복사하기, CFData 기반 프로바이더 만들기.

다음은 완전한 macOS Swift 명령줄 프로그램입니다. main.swift로 저장한 다음 개발자 도구가 설치된 Mac에서 swift main.swift를 실행하세요. 이 프로그램은 두 이미지 객체를 만들고 크기를 출력하지만 창은 생성하지 않습니다. 이 Swift 프로그램은 검토했지만 Linux 환경에서는 컴파일할 수 없었습니다. 테스트 데이터의 바이트 수, 오프셋, 프리멀티플라이 구성 요소 범위는 Python으로 별도 확인했습니다.

import AppKit
import CoreGraphics
import CoreFoundation

 enum PixelError: Error {
    case invalidDimensions, arithmeticOverflow
    case invalidStride, invalidByteCount
    case invalidPremultipliedPixel
    case constructionFailed
}

func makeBGRAImage(
    bytes: [UInt8], width: Int, height: Int, bytesPerRow: Int
) throws -> CGImage {
    guard width > 0, height > 0 else {
        throw PixelError.invalidDimensions
    }
    let (activeRowBytes, rowOverflow) =
        width.multipliedReportingOverflow(by: 4)
    guard !rowOverflow else { throw PixelError.arithmeticOverflow }
    guard bytesPerRow >= activeRowBytes,
          bytesPerRow % 4 == 0 else {
        throw PixelError.invalidStride
    }
    let (requiredBytes, sizeOverflow) =
        bytesPerRow.multipliedReportingOverflow(by: height)
    guard !sizeOverflow else { throw PixelError.arithmeticOverflow }
    guard bytes.count == requiredBytes else {
        throw PixelError.invalidByteCount
    }

    for y in 0..<height {
        for x in 0..<width {
            let i = y * bytesPerRow + x * 4
            let alpha = bytes[i + 3]
            guard bytes[i] <= alpha,
                  bytes[i + 1] <= alpha,
                  bytes[i + 2] <= alpha else {
                throw PixelError.invalidPremultipliedPixel
            }
        }
    }

    let copiedData: CFData? = bytes.withUnsafeBufferPointer { buffer in
        CFDataCreate(nil, buffer.baseAddress, requiredBytes)
    }
    guard let data = copiedData,
          let provider = CGDataProvider(data: data),
          let colorSpace = CGColorSpace(name: CGColorSpace.sRGB) else {
        throw PixelError.constructionFailed
    }
    let info = CGBitmapInfo.byteOrder32Little.union(
        CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue)
    )
    guard let image = CGImage(
        width: width, height: height,
        bitsPerComponent: 8, bitsPerPixel: 32,
        bytesPerRow: bytesPerRow,
        space: colorSpace, bitmapInfo: info,
        provider: provider, decode: nil,
        shouldInterpolate: false, intent: .defaultIntent
    ) else {
        throw PixelError.constructionFailed
    }
    return image
}

// Two active pixels and four padding bytes per row.
let syntheticBytes: [UInt8] = [
    0, 0, 255, 255,  0, 255, 0, 255,  0, 0, 0, 0,
    255, 0, 0, 255,  0, 0, 128, 128,  0, 0, 0, 0
]

do {
    let cgImage = try makeBGRAImage(
        bytes: syntheticBytes, width: 2, height: 2, bytesPerRow: 12
    )
    let appKitImage = NSImage(
        cgImage: cgImage, size: NSSize(width: 2, height: 2)
    )
    precondition(cgImage.width == 2 && cgImage.height == 2)
    print("Pixels: \(cgImage.width) x \(cgImage.height)")
    print("Points: \(appKitImage.size)")
} catch {
    fatalError("Synthetic image conversion failed: \(error)")
}

검사된 곱셈은 인덱싱이나 버퍼 복사보다 먼저 수행됩니다. 길이와 간격 검사가 성공하면 모든 유효 픽셀이 제공된 배열 안에 있습니다. 패딩은 유지되지만 픽셀 검증에서는 제외됩니다. 오류 사례는 잘못된 크기, 간격, 길이, 알파 값, 생성 실패를 구분합니다. 실제 앱에서는 기존 오류 처리 방식으로 이러한 오류를 표시하거나 기록할 수 있습니다.

테스트 데이터의 첫 저장 행에는 불투명한 빨강과 초록이 있습니다. 두 번째 행에는 불투명한 파랑과 대략 절반만 투명한 빨강이 있습니다. decode: nil은 디코딩 재매핑을 요청하지 않으며, shouldInterpolate: false는 픽셀 스무딩 없이 생성하도록 요청합니다. 이러한 매개변수는 입력을 크기 조정하거나 다시 쓰지 않습니다. Apple의 이미지 생성 매개변수.

나중에 최적화할 때는 Apple이 제공하는 릴리스 콜백이 있는 직접 접근 프로바이더 이니셜라이저를 사용할 수도 있습니다. 이 경로에는 별도의 할당 수명 계약이 필요합니다. Core Graphics가 할당된 메모리를 사용하는 동안 유효하게 유지하고, 콜백을 통해 해제해야 합니다. 소유권을 명시적으로 설계해야 하는 별개의 구현 방식으로 다루세요. 직접 접근 데이터 프로바이더 이니셜라이저.

픽셀 크기를 AppKit 표시 크기로 어떻게 바꿔야 하나요?

래스터 크기와 논리적 크기를 별도의 입력으로 유지하세요. Apple은 표현 크기는 포인트 단위로, 너비와 높이는 픽셀 단위로 측정한다고 구분합니다. NSImage(cgImage:size:)은 원하는 이미지 크기를 받습니다. .zero를 전달하면 원본 이미지의 픽셀 크기 숫자를 그대로 사용합니다. 이미지 표현 크기, AppKit의 CGImage 이니셜라이저.

크기 선택의 예로, 포인트당 픽셀 2개를 제공하도록 의도된 400 × 200 픽셀 래스터의 NSImage 크기는 200 × 100포인트여야 합니다. 이 계산은 의도한 논리적 크기를 나타냅니다. 기반이 되는 CGImage는 계속 400 × 200픽셀입니다. 래퍼가 샘플을 추가로 만들지는 않습니다.

이 프로그램은 2픽셀 테스트 이미지의 각 크기에 2포인트를 지정합니다. 더 큰 미리 보기를 만들려면 뷰에 적합한 논리적 크기를 명시적으로 선택하세요. 이후 작업에서 정확한 래스터 크기가 필요할 수 있으므로 CGImage를 계속 사용할 수 있게 두세요. Apple은 NSImage 래퍼가 원본과 동일하게 그려지는 것 외에 어떤 세부 사항도 가정하지 말라고 권고합니다.

합성 픽셀이 Metal에서 오면 무엇이 달라지나요?

Metal에서는 CPU 변환기를 실행하기 전에 준비 상태 계약이 추가됩니다. Apple은 .bgra8Unorm을 BGRA 순서의 정규화된 8비트 부호 없는 구성 요소 4개로 정의합니다. 이에 대응하는 .bgra8Unorm_srgb는 sRGB와 선형 공간 사이의 변환도 지정합니다. 따라서 채널 순서만 일치한다고 해서 텍스처 내용이 이 글의 색 및 프리멀티플라이 알파 규칙을 따른다는 뜻은 아닙니다. BGRA8 정규화 형식, BGRA8 sRGB 형식.

mipmap 레벨 0에 있는 단일 샘플 2D 텍스처의 합성 출력에는 다음 순서를 사용하세요.

  1. 텍스처의 픽셀 형식, 색 해석, 알파 규칙, 의도한 행 방향을 확인합니다.
  2. 텍스처에 쓰는 GPU 작업을 완료하고, 출력을 사용하기 전에 성공적으로 완료되었는지 확인합니다.
  3. 텍스처가 managed 저장소를 사용한다면 GPU에서 CPU로의 필수 동기화를 인코딩하고 동기화가 완료될 때까지 기다립니다.
  4. 텍스처가 private 저장소를 사용한다면 읽기 전에 적합한 CPU 접근 가능 텍스처로 블릿 복사합니다.
  5. 간격을 확인한 대상 버퍼를 할당하고, 실제 대상 간격을 사용해 요청한 영역을 복사한 다음, 결과 바이트를 CPU 계약에 전달합니다.

이 단계는 문서화된 서로 다른 두 요구 사항을 반영합니다. getBytes는 CPU에서 즉시 복사하지만 GPU 텍스처 작업과 동기화하지 않으며, Apple은 private 텍스처에서 직접 사용하는 것을 명시적으로 허용하지 않습니다. 별도로 synchronize(resource:)은 managed 리소스의 CPU 복사본을 업데이트합니다. CPU에서 접근하려면 해당 명령이 완료되어야 합니다. 텍스처 읽기 요구 사항, managed 리소스 동기화.

특정 읽기 작업에 맞춰 대상 간격을 선택하고 변환 과정 내내 유지하세요. 이처럼 범위가 좁은 BGRA 계약에서는 유효한 각 행에 픽셀당 4바이트가 필요합니다. 블릿 스테이징 레이아웃에는 직접 CPU 읽기 레이아웃과 다른 패딩이 필요할 수 있습니다. 모든 버퍼의 피치가 같다고 가정하지 말고 각 경계의 간격을 기록하세요.

계약 불일치를 어떻게 진단할 수 있나요?

더 큰 생성 그래픽을 테스트하기 전에 4픽셀 테스트 데이터를 의사 결정 도구로 사용하세요. 다음 진단은 알려진 값과 레이아웃에서 추론한 것이며, 앱에서 측정한 결과는 아닙니다.

관찰 결과 먼저 확인할 항목
빨강과 파랑의 위치가 바뀜 실제 채널 바이트와 알파 및 바이트 순서 플래그
첫 행은 그럴듯하지만 이후 행이 밀림 원본 간격, 대상 간격, 패딩
반투명한 빨강이 너무 밝거나 어두움 프리멀티플라이 및 색 해석
두 행의 패턴이 세로로 뒤바뀜 생산자의 행 순서와 그리기 변환
래스터는 올바르지만 논리적 크기가 잘못됨 AppKit 포인트 크기
CPU 생성 입력은 작동하지만 GPU 생성 입력은 작동하지 않음 GPU 완료 여부, 저장소 모드, 동기화, 읽기 레이아웃

방향에 관해 이 변환기는 행을 바이트 단위로 그대로 보존한다고 보장합니다. 실제 대상 뷰에서 비대칭 테스트 데이터를 확인하세요. 결과가 의도한 디자인에 비해 수직으로 뒤집혀 있다면 문서화된 정규화 경계에서 행 순서를 수정하거나 그리기 변환을 바로잡으세요. 두 단계가 모두 이미지를 뒤집지 않도록 이 결정을 명시적으로 유지하세요.

완전한 원시 픽셀 변환 계약은 각 단계가 무엇을 받고 무엇을 보장하는지 확인할 수 있게 합니다. 생산자는 정의된 픽셀을 제공하고, 변환기는 레이아웃을 검증해 복사본을 소유하며, AppKit에는 의도적으로 선택한 논리적 크기를 전달합니다. 작은 합성 테스트 데이터로 이러한 경계를 설정한 다음, 동일한 크기, 간격, 색, 알파, 수명 규칙을 더 큰 이미지에도 적용하세요.

출처