How much memory does a bitmap use, why is bytesPerRow not width × 4, and what is premultiplied alpha?
Predict first
What does the bitmap look like in memory?
import Foundation
import CoreGraphics
let srgb = CGColorSpace(name: CGColorSpace.sRGB)!
let ctx = CGContext(data: nil, width: 3, height: 2, bitsPerComponent: 8, bytesPerRow: 0, space: srgb,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!
print("width 3 needs 12 bytes per row; the context chose bytesPerRow =", ctx.bytesPerRow)
ctx.setFillColor(red: 1, green: 0, blue: 0, alpha: 0.5)
ctx.fill(CGRect(x: 0, y: 0, width: 3, height: 2))
let px = ctx.data!.assumingMemoryBound(to: UInt8.self)
print("50% red stored as (r, g, b, a) =", px[0], px[1], px[2], px[3])
let straight = CGContext(data: nil, width: 3, height: 2, bitsPerComponent: 8, bytesPerRow: 0, space: srgb,
bitmapInfo: CGImageAlphaInfo.last.rawValue)
print("context asking for straight (non-premultiplied) alpha:", straight == nil ? "nil" : "created")
let big = CGContext(data: nil, width: 4000, height: 3000, bitsPerComponent: 8, bytesPerRow: 0, space: srgb,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!
print("12 MP RGBA bitmap in memory:", big.bytesPerRow * big.height / 1_000_000, "MB")
let gray = CGContext(data: nil, width: 4000, height: 3000, bitsPerComponent: 8, bytesPerRow: 0,
space: CGColorSpaceCreateDeviceGray(), bitmapInfo: CGImageAlphaInfo.none.rawValue)!
print("12 MP 8-bit gray bitmap:", gray.bytesPerRow * gray.height / 1_000_000, "MB")
- bytesPerRow 32 for width 3, 50% red as 128 0 0 128, straight alpha nil, 48 MB vs 12 MB
- bytesPerRow 12, 50% red as 255 0 0 128
- straight alpha creates fine and 12 MP is 12 MB
Mental model. A row of seats in a hall: the hall sells them in blocks of eight for the cleaners’ convenience, so a three-seat row still occupies a whole block.
A CGContext bitmap costs bytesPerRow × height. Pass bytesPerRow: 0 and Core Graphics chooses it, rounding each row up for alignment: in the check a 3-pixel-wide RGBA context needed 12 bytes per row and got 32 (the exact padding is a platform detail, so never assume width × 4 when you read raw bytes; always step by bytesPerRow). A 12-megapixel 8-bit RGBA bitmap is 48 MB and the same size in 8-bit gray is 12 MB, whatever the JPEG on disk weighs, which is why large images must be downsampled (Q327) and why drawing into a smaller or one-channel context is a real optimisation.
Alpha is stored premultiplied: filling 50% red wrote bytes (128, 0, 0, 128), not (255, 0, 0, 128). The check also shows that asking for straight alpha (CGImageAlphaInfo.last) when creating a drawing context returns nil, so unsupported combinations fail silently unless you check the result. Premultiplied data blends cheaply and is what the GPU and Core Animation expect; convert only when exporting to a format that wants straight alpha. If you hand a buffer to another API, state the layout (premultipliedFirst with 32-bit little-endian is the usual BGRA) and use CGBitmapInfo byte-order flags to match it.
Rows are padded, colour is premultiplied by alpha, and size is bytesPerRow × height.
Common traps
- Indexing pixels as
y * width * 4. Rows are padded. - Force-unwrapping context creation. An unsupported bitmap layout returns nil.
They might follow up with
- Why does the buffer layout matter for video? It must match the pixel-buffer format you give a writer or a GPU (Q393).
Related questions
- 18.7.2 How are animated image frames and delays stored, and what happens to a zero or tiny frame delay?
- 18.7.4 What happens when you convert between sRGB, Display P3 and linear spaces, and why can a component be above 1…
- 12.7.1 How do you design an image loading pipeline: decoding, downsampling, caching and de-duplication?
- 18.3.2 What are CVPixelBuffer, CMSampleBuffer and IOSurface, and how do you process frames without copying them?
← How are animated image frames and delays stored, and what happens to… What happens when you convert between sRGB, Display P3 and linear… →