RawParserKit is a Swift package for RAW-file parser and preview extraction logic used by RawCull. It keeps camera-vendor binary knowledge in one focused module: MakerNote focus-location parsing, embedded JPEG location discovery, thumbnail extraction, full-preview JPEG extraction, and cancellation-safe ImageIO work.
The package is intentionally independent of RawCull view models, SwiftUI views, persistence, and cache layers. It is suitable for unit testing with synthetic RAW-like TIFF data and for reuse from a macOS app target.
- Sony
.arw - Nikon
.nef - Rendered-image helpers for
.jpg,.jpeg,.png,.tif, and.tiff
- Swift 6.2
- macOS 26 or newer
- Apple platforms with Foundation, AppKit, CoreGraphics, ImageIO, CoreImage, and OSLog
Use these APIs when you want vendor-neutral RAW handling.
RawFormat: protocol implemented by each vendor format. It exposesextensions,displayName,extractThumbnail(from:maxDimension:qualityCost:),extractEmbeddedPreview(from:fullSize:),focusLocation(from:),rawFileTypeString(compressionCode:),sizeClassThresholds(camera:), andrawSizeClass(width:height:camera:).RawFormatRegistry.all: registered format types.RawFormatRegistry.allExtensions: union of supported RAW extensions.RawFormatRegistry.format(for:): resolves a file URL toSonyRawFormat,NikonRawFormat, ornil.SonyRawFormat:RawFormatconformer for.arw.NikonRawFormat:RawFormatconformer for.nef.
import RawParserKit
let url = URL(fileURLWithPath: "/photos/frame.ARW")
if let format = RawFormatRegistry.format(for: url) {
let thumbnail = try await format.extractThumbnail(
from: url,
maxDimension: 512,
qualityCost: 4
)
let preview = await format.extractEmbeddedPreview(from: url, fullSize: false)
let focus = format.focusLocation(from: url)
let rawType = format.rawFileTypeString(compressionCode: 7)
let sizeClass = format.rawSizeClass(width: 9504, height: 6336, camera: "ILCE-7RM5")
}Use RawImageLoader.shared for app/browser-style loading with task deduplication and concurrency limits.
thumbnail(for:maxPixelSize:) async -> NSImage?: loads a rendered-image or RAW thumbnail.thumbnailCGImage(for:maxPixelSize:) async -> CGImage?: loads the same thumbnail as aCGImage.previewImage(for:) async -> CGImage?: loads a sidecar JPG when present, otherwise extracts an embedded RAW preview.metadata(for:) async -> RawImageMetadata?: reads display-ready EXIF rows, RAW metadata, and focus point information.
let image = await RawImageLoader.shared.thumbnail(for: url, maxPixelSize: 240)
let cgImage = await RawImageLoader.shared.thumbnailCGImage(for: url, maxPixelSize: 240)
let preview = await RawImageLoader.shared.previewImage(for: url)
let metadata = await RawImageLoader.shared.metadata(for: url)RawImageMetadata contains optional camera, lens, exposure, aperture, apertureValue, focalLength, iso, isoValue, capturedAt, dimensions, focusPoint, rawFileType, rawSizeClass, pixelWidth, and pixelHeight fields. Its rows property returns display labels and values for non-empty fields, and isEmpty reports whether there is anything to show.
RawFocusPoint stores normalizedX and normalizedY coordinates. It can also parse the MakerNote focus-location string returned by focusLocation(from:).
The older thumbnail200px(for:targetSize:), extractembeddedJPG(for:), exifInfo(for:), BrowserExifInfo, and BrowserFocusPoint names remain available as deprecated compatibility shims.
Use the format-neutral RawFormat APIs where possible. The vendor-specific extractors are public when callers need direct access.
SonyThumbnailExtractor.extractSonyThumbnail(from:maxDimension:qualityCost:) async throws -> CGImageNikonThumbnailExtractor.extractNikonThumbnail(from:maxDimension:qualityCost:) async throws -> CGImageSonyEmbeddedJPEGExtractor.extractEmbeddedJPEG(from:fullSize:limiter:) async -> CGImage?NikonEmbeddedJPEGExtractor.extractEmbeddedJPEG(from:fullSize:limiter:) async -> CGImage?ThumbnailSharpener.sharpenedPreview(from:maxDimension:amount:) -> CGImage?
let thumbnail = try await SonyThumbnailExtractor.extractSonyThumbnail(
from: url,
maxDimension: 512
)
let embeddedPreview = await SonyEmbeddedJPEGExtractor.extractEmbeddedJPEG(
from: url,
fullSize: true
)fullSize: true allows previews up to 8640 px on the longest edge. fullSize: false downsamples large embedded previews to 4320 px.
The older JPGSonyARWExtractor.jpgSonyARWExtractor(...) and JPGNikonNEFExtractor.jpgNikonNEFExtractor(...) entrypoints remain available as deprecated compatibility shims.
SonyRawFormat.createFullSizeJPEG(from:quality:) develops Sony ARW sensor data through macOS CIRAWFilter and encodes it as sRGB JPEG data.
let jpegData = try await SonyRawFormat.createFullSizeJPEG(
from: url,
quality: 1.0
)This does not use the camera's embedded JPEG. Camera and compression-mode support follows the RAW decoder installed with macOS. Unsupported files throw SonyJPEGCreationError.unsupportedOrInvalidRAW.
SonyJPEGCreationError cases:
invalidQuality(Double)unsupportedOrInvalidRAWencodingFailed
Focus-location APIs return a string in RawCull's existing shape:
"imageWidth imageHeight focusX focusY"
Public parser APIs:
SonyMakerNoteParser.focusLocation(from:) -> String?SonyMakerNoteParser.focusLocationDiagnostics(from:) -> RawParserDiagnostics<String>SonyMakerNoteParser.embeddedJPEGLocations(from:) -> EmbeddedJPEGLocations?SonyMakerNoteParser.embeddedJPEGLocationsDiagnostics(from:) -> RawParserDiagnostics<EmbeddedJPEGLocations>SonyMakerNoteParser.readEmbeddedJPEGData(at:from:) -> Data?NikonMakerNoteParser.focusLocation(from:) -> String?NikonMakerNoteParser.focusLocationDiagnostics(from:) -> RawParserDiagnostics<String>NikonMakerNoteParser.embeddedJPEGLocations(from:) -> NEFEmbeddedJPEGLocations?NikonMakerNoteParser.embeddedJPEGLocationsDiagnostics(from:) -> RawParserDiagnostics<NEFEmbeddedJPEGLocations>NikonMakerNoteParser.readEmbeddedJPEGData(at:from:) -> Data?
let sonyFocus = SonyMakerNoteParser.focusLocation(from: url)
let nikonFocus = NikonMakerNoteParser.focusLocation(from: url)
let diagnostics = SonyMakerNoteParser.focusLocationDiagnostics(from: url)
if let focus = diagnostics.value {
print("Focus:", focus)
} else {
print("Failure:", diagnostics.failure ?? "unknown")
print(diagnostics.trace.joined(separator: "\n"))
}Sony embedded JPEG locations are represented by EmbeddedJPEGLocations with thumbnail, preview, and fullJPEG fields. Nikon embedded JPEG locations are represented by NEFEmbeddedJPEGLocations with preview and ifd1JPEG fields. Each location has an absolute file offset and byte length.
if let locations = SonyMakerNoteParser.embeddedJPEGLocations(from: url),
let location = locations.preview ?? locations.fullJPEG,
let data = SonyMakerNoteParser.readEmbeddedJPEGData(at: location, from: url) {
print("JPEG bytes:", data.count)
}RawParserDiagnostics<Value> contains:
value: Value?trace: [String]failure: String?
OrientationNormalizedImageLoader provides lower-level ImageIO helpers for rendered images and embedded previews.
loadCGImage(from url:) -> CGImage?loadCGImage(from data:) -> CGImage?loadThumbnail(from:maxPixelSize:) -> CGImage?loadEmbeddedThumbnail(from:maxPixelSize:) -> CGImage?applyingSourceOrientation(to:from:) -> CGImage?loadSonyEmbeddedPreview(from:) -> CGImage?loadEmbeddedPreview(from:sourceURL:) -> CGImage?
SupportedFileType enumerates .arw, .nef, .jpeg, .jpg, .png, .tif, and .tiff.
CancellableImageIOWork.run(qos:_:) async throws -> Success: runs synchronous ImageIO work on a global queue with a cooperative cancellation token.CancellableImageIOWork.runReturningNilOnCancellation(qos:_:) async -> Success?: converts cancellation and thrown errors tonil.ImageIOCancellationToken.cancel(),isCancelled, andcheckCancellation().DecodeConcurrencyLimiter(maxConcurrent:): actor that bounds expensive async decode work.DecodeConcurrencyLimiter.run(_:) async -> T?: runs work once a slot is available and returnsnilif cancelled before or during execution.
ThumbnailError cases:
invalidSourcegenerationFailedcontextCreationFailed
SonyJPEGCreationError cases are listed in the Sony full-size JPEG section.
The public APIs are non-UI and do not depend on SwiftUI, @Observable, RawCull view models, or app state. ImageIO thumbnail and JPEG work is wrapped by CancellableImageIOWork, which schedules synchronous ImageIO operations on a global queue and resumes the async caller when the work completes or cancellation is observed.
The parser code itself is pure Foundation-based binary reading. It reads either a fast-path prefix window or a full-file fallback, then walks TIFF IFD structures and vendor MakerNote data.
Build the package:
swift buildRun tests:
swift testRun tests with code coverage:
swift test --enable-code-coverageThe test suite uses Swift Testing and synthetic binary data. It does not require real ARW or NEF files. The tests cover:
- Sony focus-location MakerNote variants
- Nikon focus-location MakerNote variants
- Embedded JPEG offset discovery and JPEG byte reading
- Thumbnail and JPEG extraction cancellation behavior
- Raw format registry extension matching
Add a new RawFormat conformer for the vendor, implement its parser/extractor entrypoints, and register the conformer in RawFormatRegistry.all. Prefer synthetic binary fixtures for parser tests so the package remains fast and deterministic.