合成したrawピクセルから信頼性の高いCGImageを作成するには、寸法、行ストライド、チャンネル順、アルファ規約、色空間、所有権をまとめて定義します。このガイドでは、検証機能を備えたBGRAコンバーターを作成し、その結果にAppKitでの表示サイズを明示的に設定します。
想定する読者の作業は、Macアプリで生成するスウォッチ、図、プレビュー向けにメモリ内画像を作成することです。以下の実装が受け付ける形式は、意図的に限定されています。合成したMetal出力を同じ変換境界まで渡す方法は、別の手順で説明します。
変換コントラクトでは何を指定する必要がありますか?
Appleのrawデータ向けCGImageイニシャライザーは、プロバイダーのバイト列が指定された画像パラメーターと一致することを想定しています。幅と高さはピクセル単位で、bytesPerRowは各行のメモリ上のレイアウトを表します。これらのパラメーターは入力バッファーを記述するものなので、画像を構築する前に確定します。Appleのビットマップ画像イニシャライザーのドキュメント。
この記事のコンバーターでは、次のアプリケーションレベルのコントラクトを使用します。厳密な検証規則は、受け付ける入力を明確にするための設計上の選択です。
| プロパティ | 受け付ける入力または動作 |
|---|---|
| 寸法 | ピクセル単位の正の整数の幅と高さ |
| コンポーネント | ピクセルごとに4つの符号なし8ビットコンポーネント |
| メモリ上の順序 | 連続するバイトは青、緑、赤、アルファ |
| アルファ | RGBコンポーネントはすでにアルファで乗算済み |
| 色の解釈 | sRGB |
| 行ストライド | width * 4バイト以上で、4で割り切れる |
| バッファー長 | 最終行のパディングを含め、正確にbytesPerRow * height |
| 行の順序 | そのまま保持し、自動で上下反転しない |
| 所有権 | 入力をコピーして不変の所有ストレージに保存 |
| 失敗時 | 入力が無効、または構築に失敗した場合はエラーをスロー |
| 表示 | AppKitのポイント単位の寸法を別途指定 |
たとえば、2ピクセルの行には有効な8バイトが含まれます。ストライドを12にすると、各行の後に4バイトのパディングが加わります。そのような行が2行あると、合計24バイトです。2行目の先頭ピクセルはバイト位置12から始まります。この例では意図的にパディングを含めています。密に詰めたデータだけをテストすると、コンバーターが指定されたストライドの代わりにwidth * 4を誤って使っていても見つけられません。
最終行にもパディング全体を要求すると、所有権の境界が単純になります。最終行の割り当てが短いプロデューサーは、このコンバーターを呼び出す前に出力を正規化する必要があります。長さが一致しないデータを拒否することで、意図する画像領域が不明な大きいバッファーを誤って受け付けることも防げます。
BGRA、バイト順、アルファ、色空間はどのように組み合わせますか?
この形式では、CGImageAlphaInfo.premultipliedFirstとCGBitmapInfo.byteOrder32Littleを組み合わせます。Appleによると、前者は最上位ビットにアルファがあり、色コンポーネントにはすでにアルファが乗算されていることを示し、後者はリトルエンディアンの32ビットレイアウトを示します。この2つの定義を組み合わせると、メモリ上のアドレスが増える順にBGRAとなります。premultiplied-firstアルファ、32ビット・リトルエンディアンのビットマップ順序。
したがって、「first」がメモリ上のどこにアルファが現れるかを決めるには、バイト順の設定も必要です。ここで使う不透明な赤の4バイトは0, 0, 255, 255です。この設定でRGBAバイトを渡すと、赤と青が入れ替わります。メタデータを変更しても、既存のバッファー内のデータは並べ替えられません。
プリマルチプライの意味も具体的です。この説明用の8ビット規約では、アルファが128の完全な赤色は、BGRAメモリ上でおよそ0, 0, 128, 128になります。コンバーターは、格納された各色コンポーネントがアルファ以下であることを確認します。これにより、選択した正規化済みプリマルチプライ形式として不可能な値を検出できますが、プロデューサーが各コンポーネントをどう計算したかまでは証明できません。
バイトの意図された解釈がsRGBの場合に限り、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を実行してください。2つの画像オブジェクトを構築して寸法を出力します。ウィンドウは作成しません。この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)")
}
範囲外の乗算を検出するチェックは、インデックス参照やバッファーのコピーより前に実行されます。長さとストライドのチェックを通過すれば、有効な各ピクセルは渡された配列の範囲内にあります。パディングは保持されますが、ピクセル検証の対象にはなりません。エラーケースは、不正な寸法、ストライド、長さ、アルファ値、構築失敗を区別します。実際のアプリでは、既存のエラー処理を通じてこれらのエラーを表示または記録できます。
フィクスチャーの最初の行には、不透明な赤と不透明な緑が格納されています。2行目には、不透明な青と、ほぼ半透明の赤が格納されています。decode: nilはデコード時の再マッピングを行わないよう要求し、shouldInterpolate: falseはピクセルの平滑化を行わずに構築するよう要求します。これらのパラメーターは入力をリサイズしたり書き換えたりしません。Appleの画像構築パラメーター。
後で最適化する場合に備えて、Appleは解放コールバック付きのダイレクトアクセス型プロバイダーイニシャライザーも提供しています。この方法では、別途、割り当てのライフタイムに関するコントラクトが必要です。Core Graphicsが使用している間は割り当てを有効な状態に保ち、コールバックで解放してください。所有権を明示的に設計する別の実装として扱います。ダイレクトアクセス型データプロバイダーのイニシャライザー。
ピクセル寸法をAppKitの表示サイズにするには?
ラスター寸法と論理寸法は、別々の入力として扱います。Appleは、ポイント単位で測る表現サイズと、ピクセル単位で測る幅および高さを区別しています。NSImage(cgImage:size:)は希望する画像サイズを受け付けます。.zeroを渡すと、元画像のピクセル寸法が数値としてそのまま採用されます。画像表現の寸法、AppKitのCGImageイニシャライザー。
サイズ指定の例として、2ピクセル対1ポイントで表示する400 × 200ピクセルのラスターには、NSImageのサイズとして200 × 100ポイントを指定します。この計算で意図する論理サイズを表します。基になるCGImageは400 × 200ピクセルのままで、ラッパーがサンプルを追加することはありません。
このプログラムでは、2ピクセルのフィクスチャーの各寸法に2ポイントを割り当てます。より大きなプレビューでは、ビューに適した論理サイズを明示的に選んでください。下流の処理でラスターの正確な寸法が必要な場合は、CGImageを利用できる状態にしておきます。Appleは、元画像と描画上の同等性を除き、NSImageラッパーの詳細を前提にしないよう推奨しています。
合成ピクセルをMetalから取得する場合、何が変わりますか?
CPUコンバーターを実行する前に、Metalでは準備完了に関するコントラクトが必要になります。Appleは.bgra8Unormを、BGRA順の正規化された符号なし8ビットコンポーネント4つとして定義しています。その対応形式である.bgra8Unorm_srgbは、sRGBと線形空間との変換も指定します。そのため、チャンネル順が一致するだけでは、テクスチャーの内容がこの記事の色とプリマルチプライ済みアルファの規約を満たすとは限りません。BGRA8の正規化形式、BGRA8のsRGB形式。
ミップレベル0の単一サンプル2Dテクスチャーには、次の手順を使います。
- テクスチャーのピクセル形式、色の解釈、アルファ規約、意図する行の向きを確定します。
- テクスチャーに書き込むGPU処理を完了させ、出力を受け取る前に成功を確認します。
- テクスチャーがmanagedストレージを使う場合は、GPUからCPUへの必要な同期をエンコードし、その同期が完了するまで待ちます。
- テクスチャーがprivateストレージを使う場合は、読み出し可能なCPUアクセス用テクスチャーにブリットします。
- ストライドを考慮して宛先バッファーを割り当て、実際の宛先ストライドを使って要求範囲をコピーした後、そのバイト列をCPUコントラクトに渡します。
これらの手順は、ドキュメントに記載された2つの異なる要件に基づいています。getBytesはCPU側でただちにコピーしますが、GPUのテクスチャー処理とは同期せず、Appleはprivateテクスチャーに直接使用することを明示的に禁じています。一方、synchronize(resource:)はmanagedリソースのCPUコピーを更新します。CPUからアクセスできるのは、そのコマンドの完了後です。テクスチャーの読み出し要件、managedリソースの同期。
宛先ストライドは、対象の読み出し処理に応じて選び、変換までその値を保持してください。限定されたBGRAコントラクトでは、有効な各行にピクセルあたり4バイトが必要です。ブリット用のステージングレイアウトでは、CPUから直接読み出すレイアウトとは異なるパディングが必要になる場合があります。すべてのバッファーのピッチが同じだと仮定せず、各境界でストライドを記録します。
コントラクトの不一致はどう診断できますか?
大きな生成画像をテストする前に、4ピクセルのフィクスチャーを判断材料として使います。以下は、既知の値とレイアウトに基づく推定であり、アプリで測定した結果ではありません。
| 観察結果 | 最初に確認する項目 |
|---|---|
| 赤と青が入れ替わる | 実際のチャンネルバイトとアルファ・バイト順フラグの組み合わせ |
| 最初の行はもっともらしいが、後続の行がずれる | ソースと宛先のストライド、およびパディング |
| 半透明の赤が明るすぎる、または暗すぎる | プリマルチプライと色の解釈 |
| 2行のパターンの上下位置が入れ替わる | プロデューサーの行順と描画変換 |
| ラスターは正しいが、論理サイズが違う | AppKitのポイント寸法 |
| CPU生成の入力は動くが、GPU生成の入力は動かない | GPU処理の完了、ストレージモード、同期、読み出しレイアウト |
向きについて、このコンバーターはバイト単位で行を保持すると定めています。実際の表示先ビューで、非対称なフィクスチャーを確認してください。意図したデザインに対して画像が上下逆さまなら、仕様を定めた正規化境界で行順を修正するか、描画変換を修正します。2つの段階で両方とも画像を反転しないよう、その判断を明示してください。
rawピクセルの変換コントラクトが完全に定義されていれば、各段階が何を受け取り、何を保証するのかを特定できます。プロデューサーが定義済みのピクセルを提供し、コンバーターがレイアウトを検証してコピーを所有し、AppKitが意図に沿った論理サイズを受け取ります。小さな合成フィクスチャーでこれらの境界を定め、大きな画像でも同じ寸法、ストライド、色、アルファ、ライフタイムの規則を適用してください。
出典
- init(width:height:bitsPerComponent:bitsPerPixel:bytesPerRow:space:bitmapInfo:provider:decode:shouldInterpolate:intent:) — Apple
- CGImageAlphaInfo.premultipliedFirst — Apple
- byteOrder32Little — Apple
- sRGB — Apple
- CFDataCreate(::_:) — Apple
- init(data:) — Apple
- init(dataInfo:data:size:releaseData:) — Apple
- NSImageRep — Apple
- init(cgImage:size:) — Apple
- MTLPixelFormat.bgra8Unorm — Apple
- MTLPixelFormat.bgra8Unorm_srgb — Apple
- getBytes(_:bytesPerRow:from:mipmapLevel:) — Apple
- synchronize(resource:) — Apple