Skip to content
macOSDeep Dive Published Updated 7 min readViews unavailable

Image I/O on macOS: Incremental Decoding, Thumbnails, and Metadata

Decode images efficiently with Image I/O by bounding thumbnail memory, handling incremental status, preserving orientation, and separating metadata from pixels.

Image I/O is the system framework for reading and writing image formats and their metadata. A CGImageSource can read from a URL, data object, or provider, expose container and image properties, create decoded images, and generate thumbnails. It is usually preferable to loading a compressed file into a giant bitmap eagerly because the caller can request only the representation and size the UI needs.

The key distinction is compressed bytes versus decoded pixels. A modest JPEG or HEIF file can expand into a large bitmap in memory. A photo that is 6,000 by 4,000 pixels at four bytes per pixel has a raw pixel buffer near 96 MB before other allocations, caches, and color conversions. File size alone is not a safe memory budget.

Ask for a bounded thumbnail

For a gallery, document picker, or preview, create a thumbnail at the target pixel size rather than decoding the full-resolution image and resizing it afterward. kCGImageSourceThumbnailMaxPixelSize bounds the largest thumbnail dimension, and kCGImageSourceCreateThumbnailWithTransform applies the source orientation transform when creating it. Use the source’s actual image count and handle failure from both source creation and thumbnail creation.

import ImageIO
import Foundation

func makeThumbnail(from url: URL, maxPixelSize: Int) -> CGImage? {
    guard maxPixelSize > 0,
          let source = CGImageSourceCreateWithURL(url as CFURL, nil),
          CGImageSourceGetCount(source) > 0 else {
        return nil
    }

    let options: [CFString: Any] = [
        kCGImageSourceCreateThumbnailFromImageAlways: true,
        kCGImageSourceCreateThumbnailWithTransform: true,
        kCGImageSourceThumbnailMaxPixelSize: maxPixelSize
    ]
    return CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary)
}

The function returns the first image’s thumbnail. It does not decide which frame or primary image is appropriate for every container. Multi-image formats and HEIF collections may require selecting a different index or querying the primary image index. A PDF used as an image source has its own thumbnail semantics; do not assume every source represents a single photograph.

The maximum pixel size should reflect the largest rendered point size multiplied by the view’s current backing scale, with a reasonable upper bound. Rebuild or select another cached thumbnail if a window moves to a display with a different scale. Avoid making a thumbnail so small that later UI zoom exposes interpolation blur.

Incremental data and preview lifecycle

Image I/O supports incremental sources for data arriving in chunks. Create an incremental CGImageSource, append each new complete data snapshot with CGImageSourceUpdateData, and mark the final update when the input stream is complete. After an update, query source and image status before attempting to create a preview. Not every format can display useful partial data at every boundary.

Incremental decoding is a preview mechanism, not validation that the final file is complete or safe to save. A partially available image may be decodable while its container is still incomplete. Treat a preview as provisional until final transfer completion and format status are known. If the stream fails, discard or mark the preview incomplete rather than leaving it in the UI as a successful import.

Keep one incremental source per transfer generation. If a user switches to another image, do not send chunks from the new transfer into the old source. Use a transfer ID and verify it when updates arrive. A cancellation should stop the network producer and release references to the incremental source and buffers.

Decode timing and caches

Image I/O options control whether decoded image data is cached and whether decoding happens immediately at image creation. For an interactive scroll view, defer expensive full-resolution work until needed, but measure first-draw latency and decode-on-draw stalls. For an export pipeline, eager decoding can make errors occur in a controlled worker step rather than unpredictably during presentation.

Avoid a cache keyed only by file path. A file at the same URL can be replaced, and metadata or dimensions can change. Use a stable content version such as a file identity plus modification generation or a hash when appropriate, and include thumbnail size, orientation policy, color-management settings, and destination scale in the cache key. Bound both entry count and decoded pixel cost; an LRU count alone can retain a few enormous bitmaps.

Image decoding can be CPU- and memory-intensive. Run bulk decode work away from the UI actor, cap concurrent operations, and publish a CGImage only if its source generation is still current. Do not assume that the framework will keep total application memory below a product-specific limit simply because the request asked for a thumbnail.

Metadata is separate from image content

CGImageSourceCopyProperties and its per-index counterpart expose container and image properties, including pixel dimensions and available metadata. Metadata can be absent, malformed, or sensitive. Read only what the product needs, treat optional fields as optional, and avoid displaying embedded location or device data without a clear user-facing reason.

An orientation property changes how pixels should be interpreted; it does not necessarily mean the source bytes have been rewritten into a rotated pixel array. If a thumbnail uses the transform option, avoid applying the same orientation again in the view. If the app exports a normalized image, make orientation normalization an explicit export step and preserve or intentionally remove metadata according to user expectations.

Color profiles matter too. A bitmap rendered in a different color space can look washed out even when dimensions and pixels appear valid. Preserve color-management behavior for photography, and do not convert every image to device RGB without a measured reason. When comparing output in tests, record color space and rendering target as well as pixel dimensions.

Animated, multi-frame, and high-efficiency formats

CGImageSourceGetCount provides the source’s image count, but the application must decide how to present multiple images. A collection can be an animation, a burst, or a container with a primary image and auxiliary data. Selecting index zero is not a universal “correct preview” rule. Query the format’s supported properties and choose behavior appropriate to the file type.

For animated content, avoid eagerly decoding every frame at full size. Use an animation-oriented API or a bounded frame cache, respect timing metadata, and provide a static fallback when reduced motion or resource pressure makes full playback inappropriate. Auxiliary depth or matte data is distinct from the color image and should not be silently flattened if the product needs those features.

Failure handling and untrusted inputs

Treat any file chosen by a user or received from a network as potentially malformed. Check source creation, count, per-index status, thumbnail creation, and dimensions before allocating downstream resources. Put limits on input byte size, pixel dimensions, frame count, and work duration. A compressed image can have a small file size but enormous dimensions or many frames.

Do not use the filename extension as proof of content type. Inspect the source type identifier and allowed-format policy, then handle unsupported types gracefully. If a decode fails, preserve the original source and return an actionable error. Do not repeatedly retry the same malformed file in a background loop.

If a security-scoped file URL is used, keep access active for the time required to read it and release access when finished. If reading coordinated files, preserve the coordination boundary. Decoding a thumbnail should not bypass the file ownership or access rules by copying the file to an arbitrary temporary location without a documented reason.

Acceptance tests and metrics

Test large JPEG, HEIF with multiple images, animated GIF or APNG if supported, transparent PNG, wide panoramas, rotated EXIF metadata, missing profiles, truncated data, zero-byte files, unsupported types, and a source replaced during decode. Measure peak memory, thumbnail generation latency, first visible frame, cache hit rate, and behavior under memory pressure. Test on both a standard and high-density display.

For incremental transfer, test several chunk boundaries, incomplete final input, cancellation, and out-of-order UI selection. Assert that only a complete successful transfer becomes a committed document and that previews from older transfer generations are discarded. For thumbnails, verify the longest dimension is within the requested limit and that orientation is correct.

Image I/O provides efficient primitives, but the app chooses the image index, memory budget, cache policy, metadata disclosure, and success boundary. Make those choices explicit and measurable, and decoding becomes a controlled pipeline rather than an unpredictable cost hidden inside a view.

Related:

Sources:

Comments