Which ImageIO thumbnail options matter, and why does a thumbnail come out sideways or at the wrong size?

18.7.1 · L3 · Interview

Predict first

What sizes do the thumbnails have?

import Foundation
import ImageIO
import CoreGraphics
import UniformTypeIdentifiers

// A 400x300 test image, stored as a JPEG whose EXIF orientation says "rotate 90° clockwise" (6).
let space = CGColorSpace(name: CGColorSpace.sRGB)!
let ctx = CGContext(data: nil, width: 400, height: 300, bitsPerComponent: 8, bytesPerRow: 0, space: space,
                    bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!
ctx.setFillColor(red: 0.2, green: 0.5, blue: 0.9, alpha: 1)
ctx.fill(CGRect(x: 0, y: 0, width: 400, height: 300))
let data = NSMutableData()
let dest = CGImageDestinationCreateWithData(data, UTType.jpeg.identifier as CFString, 1, nil)!
CGImageDestinationAddImage(dest, ctx.makeImage()!, [kCGImagePropertyOrientation: 6] as CFDictionary)
CGImageDestinationFinalize(dest)

let source = CGImageSourceCreateWithData(data, nil)!
let props = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as! [CFString: Any]
print("stored pixels:", props[kCGImagePropertyPixelWidth]!, "x", props[kCGImagePropertyPixelHeight]!,
      "| orientation tag:", props[kCGImagePropertyOrientation]!)

func thumb(_ extra: [CFString: Any]) -> CGImage {
    var o: [CFString: Any] = [kCGImageSourceCreateThumbnailFromImageAlways: true, kCGImageSourceThumbnailMaxPixelSize: 100]
    o.merge(extra) { $1 }
    return CGImageSourceCreateThumbnailAtIndex(source, 0, o as CFDictionary)!
}
let plain = thumb([:])
let upright = thumb([kCGImageSourceCreateThumbnailWithTransform: true])
print("thumbnail without transform:", plain.width, "x", plain.height)
print("thumbnail with transform:", upright.width, "x", upright.height)
let big = thumb([kCGImageSourceThumbnailMaxPixelSize: 1000])
print("max pixel size 1000 on a 400x300 source:", big.width, "x", big.height)
print("thumbnail: bytesPerRow", plain.bytesPerRow, "bitsPerPixel", plain.bitsPerPixel)
  • 100 x 75 without the transform, 75 x 100 with it, and no upscaling
  • 75 x 100 in both cases
  • 100 x 75 in both cases, upscaled to 1000

Mental model. A photo in a sleeve labelled “turn me 90°”: shrinking the photo without reading the label gives a small picture that is still on its side.

CGImageSourceCreateThumbnailAtIndex decodes straight to a reduced size without ever building the full bitmap (the point of Q327’s downsampling pipeline). The options that matter: kCGImageSourceThumbnailMaxPixelSize (the longer edge, in pixels, so multiply points by screen scale), kCGImageSourceCreateThumbnailFromImageAlways (ignore any small thumbnail embedded in the file, which may be low quality or stale), and kCGImageSourceCreateThumbnailWithTransform. That last one is the usual bug: a JPEG from a camera stores pixels in sensor orientation plus an EXIF orientation tag. In the check, a 400×300 file tagged orientation 6 produced a 100×75 thumbnail without the transform flag (still sideways, the tag is only metadata) and 75×100 with it. Also observed: a max size of 1000 on a 400×300 source returned 400×300, so ImageIO does not upscale.

Read metadata first with CGImageSourceCopyPropertiesAtIndex (pixel width and height, orientation) to size a placeholder before decoding. The thumbnail is created decoded and, as shown by its bytesPerRow of 400 for 100 pixels, 4 bytes per pixel in memory: file size on disk says nothing about decoded cost. Add kCGImageSourceShouldCacheImmediately when you want the decode to happen on your background queue rather than lazily on first draw. Not run here: HEIC, RAW and multi-image containers, and device GPU behaviour.

The tag changes nothing until the transform flag applies it; the max size is an upper bound.

Common traps

  • Forgetting the transform flag. Portrait photos appear rotated.
  • Passing points instead of pixels. Thumbnails look soft on 3× screens.

They might follow up with

  • What if the file is a GIF? The thumbnail is still frame 0; animation needs one call per index (Q396).

Related questions

  • 12.7.1 How do you design an image loading pipeline: decoding, downsampling, caching and de-duplication?
  • 18.5.1 How do HEIF, JPEG, PNG and animated images differ, and what does ImageIO let you do with them?
  • 18.7.2 How are animated image frames and delays stored, and what happens to a zero or tiny frame delay?

How are animated image frames and delays stored, and what happens to… →