How much memory does a bitmap use, why is bytesPerRow not width × 4, and what is premultiplied alpha?

18.7.3 · L5 · Interview

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… →