To turn synthetic raw pixels into a reliable CGImage, define their dimensions, row stride, channel order, alpha convention, color space, and ownership together. This guide builds a checked BGRA converter, then gives the result an explicit AppKit display size.
The reader task is creating an in-memory image for a Mac app’s generated swatch, diagram, or preview. The implementation below accepts one deliberately narrow format. A separate decision sequence explains how synthetic Metal output can reach the same conversion boundary.
What must the conversion contract specify?
Apple’s raw-data CGImage initializer expects the provider’s bytes to match the supplied image parameters. Its width and height are pixels; bytesPerRow describes each horizontal row’s memory layout. These parameters describe the input buffer, so establish them before constructing the image. Apple’s bitmap image initializer documentation.
Use this application-level contract for the converter in this article. Its strict validation rules are design choices that make the accepted input unambiguous.
| Property | Accepted input or behavior |
|---|---|
| Dimensions | Positive integer width and height, measured in pixels |
| Components | Four unsigned 8-bit components per pixel |
| Memory order | Consecutive bytes are blue, green, red, alpha |
| Alpha | RGB components are already premultiplied by alpha |
| Color interpretation | sRGB |
| Row stride | At least width * 4 bytes and divisible by four |
| Buffer length | Exactly bytesPerRow * height, including final-row padding |
| Row sequence | Preserved exactly; no automatic vertical reversal |
| Ownership | Copy input into immutable owned storage |
| Failure | Throw an error for invalid input or failed construction |
| Presentation | Choose AppKit point dimensions separately |
For example, a two-pixel row contains eight active bytes. A stride of twelve adds four padding bytes after each row. Two such rows occupy twenty-four bytes. The first pixel of row two begins at byte twelve. This worked layout is intentionally padded: a tightly packed fixture would fail to reveal a converter that incorrectly substitutes width * 4 for the supplied stride.
Requiring complete padding after the final row simplifies the ownership boundary. A producer using a shorter final-row allocation must normalize its output before calling this converter. Rejecting a mismatched length also prevents accidental acceptance of a larger buffer whose intended image region is unclear.
How do BGRA, byte order, alpha, and color space fit together?
For this format, combine CGImageAlphaInfo.premultipliedFirst with CGBitmapInfo.byteOrder32Little. Apple defines the former as alpha in the most significant bits with color components already multiplied by alpha, and the latter as a little-endian 32-bit layout. Together, those definitions yield the increasing-address byte sequence BGRA. Premultiplied-first alpha, 32-bit little-endian bitmap order.
The word “first” therefore needs the byte-order setting to determine where alpha appears in memory. For opaque red, the four bytes used here are 0, 0, 255, 255. Supplying RGBA bytes with these settings exchanges red and blue. Changing metadata cannot rearrange an existing buffer.
Premultiplication is equally concrete. In this illustrative 8-bit convention, a full-red color with alpha 128 becomes approximately 0, 0, 128, 128 in BGRA memory. The converter checks that each stored color component is no greater than alpha. This catches impossible values for its chosen normalized premultiplied format, although it cannot prove how a producer calculated every component.
Assign CGColorSpace.sRGB only when the bytes have that intended interpretation. Apple specifies both sRGB colorimetry and its nonlinear transfer function. A color-space label alone does not transform linear RGB component values into sRGB values. Apple’s sRGB definition.
This converter performs validation and image construction. If a producer emits straight alpha, RGBA order, floating-point components, or linear color values, add an explicit normalization step before this boundary, or implement a separately documented contract for that format.
How can Swift construct the image with owned storage?
Use a copying boundary for a small generated desktop image. Apple documents that CFDataCreate always copies the supplied buffer into internal storage, and CGDataProvider(data:) reads a CFData object. The CGImage initializer retains its provider. This avoids tying the image to a temporary array pointer. Copying bytes into CFData, creating a CFData-backed provider.
The following is a complete macOS Swift command-line program. Save it as main.swift and run swift main.swift on a Mac with the developer tools installed. It constructs both image objects and prints their dimensions; it does not create a window. The Swift program was inspected but could not be compiled in this Linux environment. Its fixture’s byte count, offsets, and premultiplied-component bounds were checked separately with 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)")
}
The checked multiplications run before any indexing or buffer copying. Once the length and stride checks succeed, each active pixel lies inside the supplied array. Padding is retained but excluded from pixel validation. Error cases distinguish malformed dimensions, stride, length, alpha values, and construction failures; a real app can present or log those errors through its existing error handling.
The fixture’s first stored row contains opaque red and opaque green. Its second contains opaque blue and approximately half-transparent red. decode: nil requests no decode remapping, while shouldInterpolate: false requests construction without pixel smoothing. These parameters do not resize or rewrite the input. Apple’s image construction parameters.
For a later optimization, Apple also provides a direct-access provider initializer with a release callback. That path requires a separate allocation-lifetime contract: keep the allocation valid while Core Graphics uses it, and release it through the callback. Treat it as a different implementation whose ownership must be designed explicitly. Direct-access data provider initializer.
How should pixel dimensions become an AppKit display size?
Keep raster dimensions and logical dimensions as separate inputs. Apple distinguishes representation sizes measured in points from widths and heights measured in pixels. NSImage(cgImage:size:) accepts the desired image size; passing .zero makes it adopt the source image’s pixel dimensions numerically. Image representation dimensions, AppKit’s CGImage initializer.
As an illustrative sizing decision, a 400 × 200 pixel raster intended to supply two pixels per point should have an NSImage size of 200 × 100 points. That calculation expresses the intended logical size. The underlying CGImage remains 400 × 200 pixels; the wrapper does not create additional samples.
The program chooses two points for each dimension of its two-pixel fixture. For a larger preview, choose an explicit logical size suitable for the view. Keep the CGImage available when a downstream operation needs exact raster dimensions, because Apple advises against assuming details of the NSImage wrapper beyond drawing equivalence with its source.
What changes when synthetic pixels come from Metal?
Metal adds a readiness contract before the CPU converter can run. Apple defines .bgra8Unorm as four normalized unsigned 8-bit components in BGRA order. Its .bgra8Unorm_srgb counterpart also specifies conversion between sRGB and linear space. Matching channel order alone therefore does not establish that a texture’s contents satisfy this article’s color and premultiplied-alpha conventions. BGRA8 normalized format, BGRA8 sRGB format.
Use this sequence for a synthetic, single-sample 2D texture at mip level zero:
- Establish the texture’s pixel format, color interpretation, alpha convention, and intended row orientation.
- Finish GPU work that writes the texture and verify successful completion before accepting its output.
- If the texture uses managed storage, encode the required GPU-to-CPU synchronization and wait for that synchronization to complete.
- If the texture uses private storage, blit it into a suitable CPU-accessible texture before reading it.
- Allocate a checked destination buffer, copy the requested region using its actual destination stride, then pass the resulting bytes through the CPU contract.
These steps reflect two distinct documented requirements. getBytes immediately copies on the CPU but does not synchronize with GPU texture operations, and Apple explicitly disallows using it directly on private textures. Separately, synchronize(resource:) updates the CPU copy of a managed resource; CPU access follows completion of that command. Texture readback requirements, managed-resource synchronization.
Choose the destination stride for the specific readback operation and preserve it through conversion. For the narrow BGRA contract, each active row needs four bytes per pixel. A blit staging layout may require different padding from a direct CPU readback layout; record the stride at each boundary instead of assuming every buffer shares one pitch.
How can you diagnose a contract mismatch?
Use the four-pixel fixture as a decision aid before testing a larger generated graphic. The following diagnoses are inferences from its known values and layout, rather than measured results from an app.
| Observation | Check first |
|---|---|
| Red and blue exchange places | Actual channel bytes versus alpha and byte-order flags |
| First row looks plausible; later rows shift | Source stride, destination stride, and padding |
| Translucent red looks too bright or too dark | Premultiplication and color interpretation |
| The two row patterns exchange vertical positions | Producer row order and drawing transforms |
| Raster is correct but logical size is wrong | AppKit point dimensions |
| CPU-generated input works; GPU-generated input does not | GPU completion, storage mode, synchronization, and readback layout |
For orientation, this converter promises byte-for-byte row preservation. Inspect the asymmetric fixture in the actual destination view. If the result is vertically reversed relative to the intended design, correct row order at a documented normalization boundary or correct the drawing transform. Keep that decision explicit so two stages do not both reverse the image.
A complete raw-pixel conversion contract lets you identify what each stage receives and guarantees: the producer supplies defined pixels, the converter validates their layout and owns a copy, and AppKit receives a deliberate logical size. Establish those boundaries with a small synthetic fixture, then carry the same dimensions, stride, color, alpha, and lifetime rules into larger images.
Sources
- 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